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.
Not a coder? No problem
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 custom…
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.
Not a coder? No problem
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/abc123xyzhttps://your-n8n.cloud/webhook/stripe-paymentshttps://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-gatewaycd webhook-gatewaynpm 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 endpointsapp.auth('/webhook/*', (req, res, next) => { next();});// Verified Stripe → Zapier gatewayapp.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 deploycoho set-env STRIPE_SECRET_KEY sk_live_xxxxx --encryptedcoho set-env STRIPE_WEBHOOK_SECRET whsec_xxxxx --encryptedcoho 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 gatewayapp.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 webhooksapp.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 webhooksapp.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 webhooksapp.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 functionasync 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 retriesapp.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:
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 tokenapp.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 callapp.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?
These platforms prioritize ease of use over security. Adding signature verification would require users to configure secrets for each webhook source, and different services use different verification methods (HMAC-SHA256, ECDSA, certificates, etc.). By accepting any request, they reduce setup friction but shift the security burden to users. IFTTT uses a key in the URL for account authentication, but this doesn’t verify the payload source.
Can’t I just keep my webhook URL secret?
Security through obscurity is not reliable. URLs can be leaked through browser history, logs, shared screenshots, or compromised accounts. Anyone who discovers your URL can trigger your automation with arbitrary data. True security requires cryptographic verification.
Does this add latency to my automations?
Minimal. Codehooks adds roughly 50-100ms for signature verification and forwarding. For most automation workflows (sending emails, updating spreadsheets, posting to Slack), this is negligible. The security benefit far outweighs the small latency cost.
What happens if Zapier/Make/n8n/IFTTT is down?
With the queue-based approach shown above, verified events are stored in Codehooks and retried automatically. Without a gateway, the original webhook sender (Stripe, GitHub, etc.) would retry, but you’d have no visibility into failures.
How much does this cost?
Codehooks has a free tier for development and low-volume production. Most webhook gateway use cases fit comfortably in the starter plans. Compare this to the potential cost of a security incident from unverified webhooks.
Can I use this pattern with other automation platforms?
Yes! Any platform that accepts incoming webhooks (IFTTT, Pipedream, Workato, Tray.io, etc.) can receive verified events from your Codehooks gateway. The pattern is the same: verify first, then forward.
Get Started
Deploy your verified webhook gateway in 5 minutes:
coho create webhook-gatewaycd webhook-gatewaynpm install stripe # or other SDKs you needcoho deploy
Configure your secrets:
coho set-env STRIPE_WEBHOOK_SECRET whsec_xxxxx --encryptedcoho 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.
- 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.