Skip to main content

What Are Webhooks? The Complete Guide with Examples

A webhook is an automated message sent from one application to another when a specific event occurs. Instead of constantly asking "did anything happen?" (polling), webhooks push data to you in real-time the moment something happens.

Think of it like this: rather than repeatedly calling a store to check if your order shipped, the store calls you when it ships. That's the webhook model.

How Webhooks Work

Webhooks follow a simple pattern:

  1. You provide a URL — Your application exposes an HTTP endpoint (the "webhook URL") that can receive POST requests
  2. An event occurs — Something happens in the source application (payment received, form submitted, file uploaded)
  3. Data is pushed to you — The source application sends an HTTP POST request to your URL with event data as JSON
┌─────────────────┐         Event occurs          ┌─────────────────┐
│ │ │ │
│ Stripe/GitHub │ ──── HTTP POST + JSON ────▶ │ Your Webhook │
│ Shopify/Slack │ │ Endpoint │
│ │ │ │
└─────────────────┘ └─────────────────┘

Here's what a typical webhook payload looks like (from Stripe):

{
"id": "evt_1234567890",
"type": "payment_intent.succeeded",
"data": {
"object": {
"id": "pi_abc123",
"amount": 2000,
"currency": "usd",
"customer": "cus_xyz789"
}
},
"created": 1699900000
}

Your endpoint receives this data and can immediately take action — update a database, send an email, trigger a workflow.

Webhooks vs APIs: What's the Difference?

Both webhooks and APIs enable applications to communicate, but they work in opposite directions:

AspectAPI (Pull)Webhook (Push)
DirectionYou request dataData is sent to you
TriggerYour application initiatesExternal event initiates
TimingOn-demandReal-time
EfficiencyMay require pollingNo wasted requests
Use caseFetching data, CRUD operationsEvent notifications

The Polling Problem

Without webhooks, you'd need to repeatedly call an API to check for updates:

// ❌ Polling - inefficient, wasteful
setInterval(async () => {
const orders = await fetch('/api/orders?status=new');
// Check if anything changed...
}, 5000); // Every 5 seconds = 17,280 requests/day

With webhooks, you receive data only when something happens:

// ✅ Webhook - efficient, real-time
app.post('/webhooks/orders', (req, res) => {
const order = req.body;
// Process immediately when event occurs
res.status(200).send('OK');
});

Real-World Webhook Examples

Webhooks power countless integrations across the web:

Payment Processing

  • Stripe sends webhooks when payments succeed, fail, or dispute
  • PayPal notifies you of completed transactions
  • Shopify alerts your system about new orders

Developer Tools

  • GitHub triggers webhooks on push, pull request, or issue events
  • GitLab notifies CI/CD pipelines of code changes
  • Jira sends updates when tickets are created or modified

Communication

  • Slack sends webhooks for messages, commands, and events
  • Discord notifies bots of user interactions
  • Twilio pushes incoming SMS and call events

Business Automation

  • Mailchimp sends subscriber activity webhooks
  • Typeform notifies you of form submissions
  • Calendly alerts you when meetings are scheduled

Webhook Payload Structure

Most webhooks send JSON data via HTTP POST. A typical payload includes:

{
"event": "order.created",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"id": "order_123",
"customer_email": "[email protected]",
"total": 99.99,
"items": [
{ "name": "Widget", "quantity": 2, "price": 49.99 }
]
},
"signature": "sha256=abc123..."
}

Key components:

  • Event type — What happened (e.g., order.created, payment.failed)
  • Timestamp — When the event occurred
  • Data — The actual payload with event details
  • Signature — Cryptographic proof the request is authentic

Webhook Security Best Practices

Since webhooks are publicly accessible endpoints, security is critical:

1. Verify Signatures

Most webhook providers sign requests with HMAC. Always verify:

import crypto from 'crypto';

function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');

return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expected}`)
);
}

2. Use HTTPS

Always use HTTPS endpoints. Never accept webhooks over plain HTTP.

3. Validate Timestamps

Reject old requests to prevent replay attacks:

const timestamp = req.headers['x-webhook-timestamp'];
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);

if (new Date(timestamp).getTime() < fiveMinutesAgo) {
return res.status(401).send('Request too old');
}

4. Respond Quickly

Return a 200 status immediately, then process asynchronously:

app.post('/webhook', async (req, res) => {
// Acknowledge immediately
res.status(200).send('OK');

// Process in background
await processWebhookAsync(req.body);
});

5. Handle Retries

Webhook providers retry failed deliveries. Make your handler idempotent:

app.post('/webhook', async (req, res) => {
const eventId = req.body.id;

// Check if already processed
const existing = await db.findOne('processed_events', { eventId });
if (existing) {
return res.status(200).send('Already processed');
}

// Process and record
await processEvent(req.body);
await db.insertOne('processed_events', { eventId, processedAt: new Date() });

res.status(200).send('OK');
});

Building a Webhook Endpoint

Here's a complete webhook handler using Codehooks.io:

import { app, Datastore } from 'codehooks-js';
import crypto from 'crypto';

// Webhook endpoint
app.post('/webhooks/orders', async (req, res) => {
// 1. Verify signature
const signature = req.headers['x-webhook-signature'];
const isValid = verifySignature(req.rawBody, signature, process.env.WEBHOOK_SECRET);

if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}

// 2. Parse event
const { event, data } = req.body;

// 3. Handle event types
switch (event) {
case 'order.created':
await handleNewOrder(data);
break;
case 'order.shipped':
await handleShipment(data);
break;
case 'order.cancelled':
await handleCancellation(data);
break;
}

// 4. Acknowledge receipt
res.status(200).json({ received: true });
});

async function handleNewOrder(order) {
const conn = await Datastore.open();
await conn.insertOne('orders', {
...order,
receivedAt: new Date().toISOString()
});

// Trigger downstream actions
await sendConfirmationEmail(order.customer_email);
await updateInventory(order.items);
}

function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');

return crypto.timingSafeEqual(
Buffer.from(signature || ''),
Buffer.from(expected)
);
}

export default app.init();

Deploy in seconds:

coho deploy

Sending Webhooks (Outgoing)

You can also send webhooks from your application to notify others:

Production-Ready Webhook Delivery

For a complete webhook delivery system with retries, queues, and HMAC signing, use our ready-made template: coho create myapp --template webhook-delivery or see the Build a Webhook Delivery System guide.

import { app, Datastore } from 'codehooks-js';
import crypto from 'crypto';

async function sendWebhook(url, event, data, secret) {
const payload = JSON.stringify({ event, data, timestamp: new Date().toISOString() });

// Create signature
const signature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');

const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature
},
body: payload
});

return response.ok;
}

// Example: Notify subscribers when an order is created
app.post('/orders', async (req, res) => {
const order = await createOrder(req.body);

// Get all webhook subscribers
const conn = await Datastore.open();
const subscribers = await conn.getMany('webhook_subscribers', {
event: 'order.created'
}).toArray();

// Send webhooks to all subscribers
for (const sub of subscribers) {
await sendWebhook(sub.url, 'order.created', order, sub.secret);
}

res.json(order);
});

export default app.init();

Testing Webhooks

No Tunneling Needed with Codehooks.io

With Codehooks.io, you don't need ngrok or local tunneling. Just run coho deploy and your webhook endpoint is live instantly. Use coho info to get your public URL.

Local Development

If you're running a local server, use tools like ngrok or localtunnel to expose it:

ngrok http 3000
# Gives you: https://abc123.ngrok.io

Webhook Testing Services

Stripe CLI Example

stripe listen --forward-to localhost:3000/webhooks/stripe
stripe trigger payment_intent.succeeded

Common Webhook Patterns

Event Queuing

For high-volume webhooks, queue events for processing with a worker:

import { app, Datastore } from 'codehooks-js';

// Receive webhook and add to queue
app.post('/webhook', async (req, res) => {
// Quick acknowledgment
res.status(200).send('OK');

// Add to queue for async processing
const conn = await Datastore.open();
await conn.enqueue('webhook-events', req.body);
});

// Queue worker processes events asynchronously
app.worker('webhook-events', async (req, res) => {
const event = req.body.payload;
console.log('Processing webhook:', event.type);

// Handle the event (database updates, notifications, etc.)
await processWebhookEvent(event);

res.end();
});

export default app.init();

For complex multi-step processing, use the Workflow API:

import { app, Datastore } from 'codehooks-js';

// Define a workflow for processing webhooks
const orderWorkflow = app.createWorkflow({
name: 'process-order',
steps: {
validate: async (state, goto) => {
// Validate the order
state.validated = true;
return goto('notify', state);
},
notify: async (state, goto) => {
// Send notifications
await sendNotification(state.orderId);
return goto('sync', state);
},
sync: async (state, goto) => {
// Sync with external systems
await syncOrder(state.orderId);
return goto('$end', state);
}
}
});

app.post('/webhook', async (req, res) => {
res.status(200).send('OK');

// Start workflow for complex processing
await orderWorkflow.start({
orderId: req.body.data.id
});
});

export default app.init();

Webhook Retries with Exponential Backoff

When sending webhooks, retry with increasing delays:

async function sendWithRetry(url, payload, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});

if (response.ok) return true;
} catch (error) {
console.log(`Attempt ${attempt + 1} failed`);
}

// Exponential backoff: 1s, 2s, 4s, 8s, 16s
await sleep(Math.pow(2, attempt) * 1000);
}
return false;
}

Ready-to-Deploy Webhook Templates

Skip the boilerplate and deploy production-ready webhook handlers in minutes using Codehooks.io templates:

Receiving Webhooks (Inbound)

TemplateCommandDescription
Stripecoho create myapp --template webhook-stripe-minimalPayment events with signature verification
GitHubcoho create myapp --template webhook-github-minimalPush, PR, and issue events with HMAC-SHA256
Shopifycoho create myapp --template webhook-shopify-minimalOrders, products, customers with HMAC verification
Slackcoho create myapp --template webhook-slack-minimalEvents and commands with signature verification
Discordcoho create myapp --template webhook-discord-minimalSlash commands with Ed25519 verification
Twiliocoho create myapp --template webhook-twilio-minimalSMS and voice events with TwiML responses
Clerkcoho create myapp --template webhook-clerk-minimalAuth events (signup, login) with Svix verification

Sending Webhooks (Outbound)

TemplateCommandDescription
Webhook Deliverycoho create myapp --template webhook-deliveryFull webhook system with retries, queues, HMAC signing

All templates include signature verification, error handling, and are ready for production use.

Getting Started with Webhooks

Ready to implement webhooks? Here are integration guides for popular services:

Or build your own webhook delivery system:

Webhook FAQ

Common questions about webhooks and how they work

What is a webhook in simple terms?
A webhook is an automatic notification sent from one app to another when something happens. Instead of constantly checking for updates, the app tells you immediately when an event occurs - like getting a text message instead of repeatedly checking your mailbox.
What is the difference between a webhook and an API?
An API requires you to request data (pull), while a webhook sends data to you automatically (push). APIs are for on-demand operations like fetching user data. Webhooks are for real-time event notifications like 'payment received' or 'order shipped'.
How do webhooks work?
1) You provide a URL endpoint to receive data. 2) You subscribe to specific events. 3) When an event occurs, the source app sends an HTTP POST request with JSON data to your URL. 4) Your endpoint processes the data and returns a 200 OK response.
Are webhooks secure?
Webhooks can be secure when implemented correctly. Best practices include: using HTTPS endpoints, verifying request signatures (HMAC), validating timestamps to prevent replay attacks, and processing requests idempotently to handle retries.
What is a webhook URL?
A webhook URL is the HTTP endpoint where you receive webhook data. It's a publicly accessible URL on your server (like https://yourapp.com/webhooks/stripe) that accepts POST requests. The webhook provider sends event data to this URL.
What happens if my webhook endpoint is down?
Most webhook providers retry failed deliveries with exponential backoff. For example, Stripe retries up to 3 days. Your endpoint should be idempotent (handle the same event multiple times safely) since you may receive duplicate events during retries.
What is webhook signature verification?
Signature verification proves a webhook came from the legitimate source, not an attacker. The provider signs the request body with a shared secret using HMAC-SHA256. You verify by computing the same signature and comparing. This prevents forged requests.
Can I send webhooks from my own application?
Yes! You can build outgoing webhooks to notify other systems about events in your app. This involves: storing subscriber URLs, generating signed payloads when events occur, sending HTTP POST requests, and handling retries for failed deliveries.
What format do webhooks use?
Most webhooks send data as JSON via HTTP POST requests. The payload typically includes: event type (what happened), timestamp, event data (the details), and a signature header for verification. Some older systems use XML or form-encoded data.
How do I test webhooks locally?
Use tunneling tools like ngrok or localtunnel to expose your local server to the internet. Run ngrok http 3000 to get a public URL, then configure the webhook provider to send events there. Tools like Webhook.site let you inspect payloads without code.