Secure Your Automation Webhooks with Signature Verification (Zapier, Make, n8n, IFTTT)
You've built a powerful automation: Stripe payment → Zapier → Slack notification + Google Sheet update + email confirmation. It works beautifully.
But there's a problem: anyone who knows your Zapier webhook URL can trigger your automation with fake data.
The same is true for Make (Integromat), n8n, and IFTTT. These platforms prioritize ease-of-use, but they don't verify that incoming webhooks are actually from Stripe, GitHub, or whoever you think is sending them.
The solution? Use Codehooks as a verified webhook gateway that sits between the webhook sender and your automation platform.
If you're used to no-code tools, the JavaScript examples below might look intimidating. You can use ChatGPT, Claude, or any AI assistant to generate and customize this code for you. Just share the Codehooks ChatGPT prompt or point the AI to codehooks.io/llms.txt — it will understand how to write Codehooks code for your specific use case. AI coding agents like Claude Code can even run the CLI commands to deploy and configure everything for you.

The Security Problem
When you set up a webhook trigger in Zapier, Make, n8n, or IFTTT, you get a URL like:
https://hooks.zapier.com/hooks/catch/123456/abcdef/
https://hook.eu1.make.com/abc123xyz
https://your-n8n.cloud/webhook/stripe-payments
https://maker.ifttt.com/trigger/{event}/with/key/{your_key}
These endpoints accept any HTTP POST request. There's no verification that:
- The request actually came from Stripe/GitHub/etc.
- The payload hasn't been tampered with
- The request isn't a replay attack
IFTTT uses a key embedded in the URL, but this only authenticates your account — it doesn't verify the payload came from a legitimate source like Stripe.
What the platforms say
Zapier: The Zapier Community confirms that "Webhooks by Zapier" trigger does not support signature verification.
Make: Users in the Make Community discuss workarounds like header filtering and API keys, but there's no built-in signature verification for services like Stripe or GitHub.
n8n: There's an active feature request for HMAC verification. Currently, you need to implement it manually with Code nodes.
IFTTT: Uses a secret key embedded in the webhook URL. If someone obtains your key, they can trigger any event on your account. There's even a GitHub project created specifically to address this security gap.
Why this matters
An attacker who discovers your webhook URL could:
- Trigger fake payment notifications
- Create fraudulent orders in your system
- Spam your Slack channels
- Manipulate your database through automations
- Waste your automation platform credits
The Solution: Verified Webhook Gateway
Instead of sending webhooks directly to your automation platform, route them through Codehooks:
Codehooks handles the complex signature verification for each service, then forwards verified events to your automation platform.
Quick Start: Stripe to Zapier
Here's a complete example that verifies Stripe webhooks before forwarding to Zapier:
coho create webhook-gateway
cd webhook-gateway
npm install stripe
Replace index.js with:
import { app, Datastore } from 'codehooks-js';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Bypass JWT for webhook endpoints
app.auth('/webhook/*', (req, res, next) => {
next();
});
// Verified Stripe → Zapier gateway
app.post('/webhook/stripe', async (req, res) => {
const sig = req.headers['stripe-signature'];
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event;
try {
// Verify the webhook is actually from Stripe
event = stripe.webhooks.constructEvent(req.rawBody, sig, webhookSecret);
} catch (err) {
console.error('Stripe signature verification failed:', err.message);
return res.status(400).json({ error: 'Invalid signature' });
}
// Log verified event for audit trail
const conn = await Datastore.open();
await conn.insertOne('verified_events', {
source: 'stripe',
eventId: event.id,
type: event.type,
verified: true,
receivedAt: new Date().toISOString(),
});
console.log(`Verified Stripe event: ${event.type}`);
// Forward to Zapier (now we know it's legitimate)
const zapierUrl = process.env.ZAPIER_WEBHOOK_URL;
try {
await fetch(zapierUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
verified: true,
source: 'stripe',
eventType: event.type,
eventId: event.id,
data: event.data.object,
timestamp: new Date().toISOString(),
}),
});
} catch (err) {
console.error('Failed to forward to Zapier:', err.message);
// Store for retry
await conn.enqueue('retryForward', { event, target: 'zapier' });
}
res.status(200).json({ received: true, verified: true });
});
export default app.init();
Deploy and configure:
coho deploy
coho set-env STRIPE_SECRET_KEY sk_live_xxxxx --encrypted
coho set-env STRIPE_WEBHOOK_SECRET whsec_xxxxx --encrypted
coho set-env ZAPIER_WEBHOOK_URL https://hooks.zapier.com/hooks/catch/xxxxx --encrypted
Now in your Stripe Dashboard, set the webhook URL to your Codehooks endpoint instead of Zapier directly.
GitHub to Make (Integromat)
Verify GitHub webhooks before forwarding to Make:
import { app, Datastore } from 'codehooks-js';
import crypto from 'crypto';
app.auth('/webhook/*', (req, res, next) => {
next();
});
// Verified GitHub → Make gateway
app.post('/webhook/github', async (req, res) => {
const signature = req.headers['x-hub-signature-256'];
const secret = process.env.GITHUB_WEBHOOK_SECRET;
// Verify GitHub signature
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(req.rawBody)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)) {
console.error('GitHub signature verification failed');
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.headers['x-github-event'];
const delivery = req.headers['x-github-delivery'];
// Log verified event
const conn = await Datastore.open();
await conn.insertOne('verified_events', {
source: 'github',
eventId: delivery,
type: event,
verified: true,
receivedAt: new Date().toISOString(),
});
console.log(`Verified GitHub event: ${event}`);
// Forward to Make
const makeUrl = process.env.MAKE_WEBHOOK_URL;
await fetch(makeUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
verified: true,
source: 'github',
event: event,
deliveryId: delivery,
payload: req.body,
timestamp: new Date().toISOString(),
}),
});
res.status(200).json({ received: true });
});
export default app.init();
Multi-Source Gateway
Handle multiple webhook sources in one deployment:
import { app, Datastore } from 'codehooks-js';
import Stripe from 'stripe';
import crypto from 'crypto';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
app.auth('/webhook/*', (req, res, next) => {
next();
});
// Stripe webhooks
app.post('/webhook/stripe', async (req, res) => {
try {
const event = stripe.webhooks.constructEvent(
req.rawBody,
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);
await forwardVerified('stripe', event.type, event.id, event.data.object);
res.status(200).json({ received: true });
} catch (err) {
console.error('Stripe verification failed:', err.message);
res.status(400).json({ error: 'Invalid signature' });
}
});
// GitHub webhooks
app.post('/webhook/github', async (req, res) => {
const signature = req.headers['x-hub-signature-256'];
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET)
.update(req.rawBody)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
await forwardVerified(
'github',
req.headers['x-github-event'],
req.headers['x-github-delivery'],
req.body
);
res.status(200).json({ received: true });
});
// Shopify webhooks
app.post('/webhook/shopify', async (req, res) => {
const hmac = req.headers['x-shopify-hmac-sha256'];
const expected = crypto
.createHmac('sha256', process.env.SHOPIFY_WEBHOOK_SECRET)
.update(req.rawBody)
.digest('base64');
if (hmac !== expected) {
return res.status(401).json({ error: 'Invalid signature' });
}
await forwardVerified(
'shopify',
req.headers['x-shopify-topic'],
req.headers['x-shopify-webhook-id'],
req.body
);
res.status(200).json({ received: true });
});
// Generic forward function
async function forwardVerified(source, eventType, eventId, data) {
const conn = await Datastore.open();
// Audit log
await conn.insertOne('verified_events', {
source,
eventType,
eventId,
verified: true,
receivedAt: new Date().toISOString(),
});
// Forward based on routing rules
const routes = {
stripe: process.env.ZAPIER_STRIPE_URL,
github: process.env.MAKE_GITHUB_URL,
shopify: process.env.N8N_SHOPIFY_URL,
// IFTTT example: https://maker.ifttt.com/trigger/{event}/with/key/{key}
paypal: process.env.IFTTT_PAYPAL_URL,
};
const targetUrl = routes[source];
if (!targetUrl) {
console.log(`No route configured for ${source}`);
return;
}
await fetch(targetUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
verified: true,
source,
eventType,
eventId,
data,
verifiedAt: new Date().toISOString(),
}),
});
console.log(`Forwarded verified ${source} event to automation`);
}
export default app.init();
Advanced: Retry Failed Forwards
If your automation platform is temporarily down, queue events for retry:
import { app, Datastore } from 'codehooks-js';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
app.auth('/webhook/*', (req, res, next) => {
next();
});
app.post('/webhook/stripe', async (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.rawBody,
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).json({ error: 'Invalid signature' });
}
const conn = await Datastore.open();
// Store verified event
await conn.insertOne('verified_events', {
source: 'stripe',
eventId: event.id,
type: event.type,
data: event.data.object,
forwarded: false,
receivedAt: new Date().toISOString(),
});
// Queue for async forwarding (handles retries automatically)
await conn.enqueue('forwardToZapier', {
eventId: event.id,
type: event.type,
data: event.data.object,
});
// Respond immediately to Stripe
res.status(200).json({ received: true });
});
// Worker handles forwarding with retries
app.worker('forwardToZapier', async (req, res) => {
const { eventId, type, data } = req.body.payload;
const response = await fetch(process.env.ZAPIER_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
verified: true,
source: 'stripe',
eventType: type,
eventId,
data,
timestamp: new Date().toISOString(),
}),
});
if (!response.ok) {
throw new Error(`Zapier returned ${response.status}`);
}
// Mark as forwarded
const conn = await Datastore.open();
await conn.updateOne('verified_events', { eventId }, {
$set: { forwarded: true, forwardedAt: new Date().toISOString() }
});
console.log(`Forwarded event ${eventId} to Zapier`);
res.end();
});
export default app.init();
Securing the Forwarding Step
The examples above verify incoming webhooks from Stripe/GitHub/etc., but what about the forwarding to your automation platform? Here are three approaches:
1. Secret Header Authentication
Add a shared secret as a custom header. Configure your automation platform to check for it:
await fetch(zapierUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Gateway-Secret': process.env.GATEWAY_SECRET,
},
body: JSON.stringify({ verified: true, source, data }),
});
In Zapier/Make/n8n, add a filter step that checks X-Gateway-Secret matches your expected value. Requests without the correct header are rejected.
2. Callback Verification
Let your automation platform verify events by calling back to Codehooks:
// Store verified events with a verification token
app.post('/webhook/stripe', async (req, res) => {
const event = stripe.webhooks.constructEvent(/* ... */);
const verificationToken = crypto.randomUUID();
const conn = await Datastore.open();
await conn.insertOne('verified_events', {
eventId: event.id,
token: verificationToken,
data: event.data.object,
expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5 min TTL
});
// Forward with verification token
await fetch(zapierUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
eventId: event.id,
verificationToken,
verifyUrl: `https://your-app.api.codehooks.io/dev/verify/${event.id}`,
data: event.data.object,
}),
});
res.json({ received: true });
});
// Verification endpoint for automation platforms to call
app.get('/verify/:eventId', async (req, res) => {
const { eventId } = req.params;
const { token } = req.query;
const conn = await Datastore.open();
const event = await conn.getOne('verified_events', { eventId, token });
if (!event || new Date(event.expiresAt) < new Date()) {
return res.status(404).json({ verified: false });
}
res.json({ verified: true, eventId });
});
Your automation workflow first calls the verifyUrl to confirm the event is legitimate before processing.
3. Keep URLs Secret
At minimum, treat webhook URLs as secrets:
- Store them with
coho set-env ... --encrypted - Never log or expose them
- Rotate them periodically
- Use HTTPS (always encrypted in transit)
The random tokens in Zapier/Make/n8n URLs provide basic authentication — anyone with the URL can trigger your automation, so protect it accordingly.
Which approach to use? For notifications and logging, secret URLs are sufficient. For business workflows, add header authentication. For financial or security-sensitive operations, consider callback verification.
Benefits of This Approach
| Feature | Direct to Zapier/Make/n8n/IFTTT | Via Codehooks Gateway |
|---|---|---|
| Signature verification | No | Yes |
| Audit trail | No | Yes (database) |
| Retry on failure | Limited | Built-in queues |
| Event replay | No | Yes |
| Rate limiting | No | Configurable |
| Data transformation | Limited | Full code control |
| Cost | Automation credits only | Small Codehooks cost |
When to Use This Pattern
Use a verified gateway when:
- Webhooks trigger financial transactions or sensitive operations
- You need an audit trail for compliance
- You want to transform data before it reaches your automation
- You need retry logic if the automation platform is down
- Security is a priority
Skip it when:
- The webhook source is internal/trusted
- The automation is low-stakes (e.g., logging, notifications)
- You're prototyping and will add security later
Supported Webhook Sources
Codehooks can verify signatures from any service. Here are guides for popular platforms:
- Stripe Webhooks - HMAC-SHA256 signatures
- GitHub Webhooks - HMAC-SHA256 signatures
- Shopify Webhooks - HMAC-SHA256 signatures (Base64)
- PayPal Webhooks - Certificate-based verification
- Twilio Webhooks - HMAC-SHA1 signatures
- SendGrid Webhooks - ECDSA signatures
- OpenAI Webhooks - Standard Webhooks (HMAC-SHA256)
Webhook Gateway FAQ
Common questions about securing automation webhooks
Why don't Zapier/Make/n8n/IFTTT verify webhook signatures?
Can't I just keep my webhook URL secret?
Does this add latency to my automations?
What happens if Zapier/Make/n8n/IFTTT is down?
How much does this cost?
Can I use this pattern with other automation platforms?
Get Started
Deploy your verified webhook gateway in 5 minutes:
coho create webhook-gateway
cd webhook-gateway
npm install stripe # or other SDKs you need
coho deploy
Configure your secrets:
coho set-env STRIPE_WEBHOOK_SECRET whsec_xxxxx --encrypted
coho set-env ZAPIER_WEBHOOK_URL https://hooks.zapier.com/xxxxx --encrypted
Point your webhook sources (Stripe, GitHub, etc.) to your Codehooks URL instead of directly to Zapier/Make/n8n/IFTTT.
Your automations are now protected by cryptographic signature verification.
Related Resources
- What are Webhooks? - Webhook fundamentals
- Webhook Delivery System - Build outgoing webhooks
- Browse All Webhook Examples - Platform-specific guides
- Codehooks Quick Start - Get started with Codehooks
Questions? Open an issue on GitHub or check the Codehooks documentation.
