Webhooks Explained: The Complete Beginner's Guide for 2025

January 22, 2025 · 11 min read

Webhooks are one of the most important patterns in modern web development, yet they remain a source of confusion for many developers. If you have ever wondered how Stripe notifies your app about a payment, how GitHub triggers your CI pipeline, or how Slack posts messages from external services, the answer is webhooks.

This guide covers everything you need to understand and implement webhooks, from the fundamental concepts to production-ready best practices.

What Is a Webhook?

A webhook is an HTTP callback: an HTTP POST request sent to a URL you specify when a specific event occurs. Instead of your application repeatedly asking "has anything changed?" (polling), the source system pushes the update to you when it happens.

Think of it like the difference between constantly refreshing your email versus having push notifications enabled. Both get you the information, but push is more efficient.

// Polling approach (inefficient)
setInterval(async () => {
  const response = await fetch('/api/check-payment-status');
  const data = await response.json();
  if (data.status === 'completed') {
    processPayment(data);
  }
}, 5000); // Check every 5 seconds

// Webhook approach (efficient)
// Your server receives a POST when payment completes
app.post('/webhooks/payment', (req, res) => {
  const event = req.body;
  if (event.type === 'payment.completed') {
    processPayment(event.data);
  }
  res.status(200).send('OK');
});

How Webhooks Work

The webhook flow has four steps:

  1. Registration — You tell the source system your endpoint URL (e.g., https://myapp.com/webhooks/stripe)
  2. Event occurs — Something happens in the source system (a payment, a commit, a form submission)
  3. Delivery — The source system sends an HTTP POST to your URL with event data as JSON
  4. Processing — Your endpoint receives the request, validates it, processes it, and returns a 200 status

The critical detail is step 4: you must return a 200 status quickly. Most webhook providers will retry delivery if they do not receive a 200 within a timeout window (typically 5-30 seconds). If your processing takes longer, acknowledge receipt first and process asynchronously.

When to Use Webhooks

Webhooks are the right choice when:

Webhooks are not ideal when:

Building Your First Webhook Endpoint

Here is a minimal webhook endpoint in Node.js with Express:

const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/events', (req, res) => {
  const event = req.body;

  // Log the event for debugging
  console.log('Received webhook:', event.type);

  // Process based on event type
  switch (event.type) {
    case 'order.created':
      handleNewOrder(event.data);
      break;
    case 'order.shipped':
      handleShipment(event.data);
      break;
    default:
      console.log('Unhandled event type:', event.type);
  }

  // Always respond with 200 quickly
  res.status(200).json({ received: true });
});

app.listen(3000);

And the equivalent in Python with Flask:

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks/events', methods=['POST'])
def handle_webhook():
    event = request.get_json()

    if event['type'] == 'order.created':
        handle_new_order(event['data'])
    elif event['type'] == 'order.shipped':
        handle_shipment(event['data'])

    return jsonify({'received': True}), 200

Security Best Practices

Webhook endpoints are publicly accessible URLs, which means anyone can send requests to them. Security is non-negotiable.

1. Verify Signatures

Most webhook providers sign their payloads with a shared secret. Always verify the signature before processing:

const crypto = require('crypto');

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

app.post('/webhooks/events', (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const isValid = verifySignature(
    JSON.stringify(req.body),
    signature,
    process.env.WEBHOOK_SECRET
  );

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

  // Process the verified event...
});

2. Use HTTPS Only

Never register an HTTP (non-TLS) webhook URL. The payload travels over the network and may contain sensitive data. HTTPS is mandatory for production webhooks.

3. Implement Idempotency

Webhooks can be delivered more than once. Use the event ID to ensure you do not process the same event twice:

const processedEvents = new Set();

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

  if (processedEvents.has(eventId)) {
    return res.status(200).json({ received: true }); // Already processed
  }

  processedEvents.add(eventId);
  processEvent(req.body);

  res.status(200).json({ received: true });
});

In production, use a database instead of an in-memory Set to persist across restarts.

Testing Webhooks During Development

Testing webhooks locally can be tricky because your development server is not publicly accessible. Tools like InvokeBot let you construct and send test webhook payloads to your endpoints. For forwarding production webhooks to your local machine, tools like ngrok create temporary public URLs that tunnel to localhost.

Need timestamp conversions for your webhook logs? Try EpochPilot's time tools.

For teams building webhook infrastructure at scale, machine learning approaches to anomaly detection can help identify delivery failures before they impact users. Platforms like KickLLM explore how language models can assist in monitoring and debugging complex integration pipelines.

A good testing workflow looks like this:

  1. Build your endpoint locally
  2. Use a request builder to send test payloads with various event types
  3. Verify your handler processes each type correctly
  4. Test error cases: invalid signatures, malformed JSON, unknown event types
  5. Test timeout behavior: what happens if your processing takes too long?

Common Webhook Patterns

Fan-Out Pattern

One event triggers multiple actions. A new order might update inventory, send a confirmation email, and notify the warehouse:

function handleNewOrder(order) {
  updateInventory(order.items);
  sendConfirmationEmail(order.customer);
  notifyWarehouse(order);
}

Queue Pattern

For high-volume webhooks, acknowledge immediately and queue for processing. This prevents timeouts and lets you handle bursts:

app.post('/webhooks/events', (req, res) => {
  // Acknowledge immediately
  res.status(200).json({ received: true });

  // Queue for async processing
  eventQueue.push(req.body);
});

Conclusion

Webhooks are the backbone of modern application integration. They replace inefficient polling with event-driven updates, making your systems more responsive and resource-efficient. Start with the basics outlined here: build an endpoint, verify signatures, handle idempotency, and test thoroughly before going to production.

As your webhook integrations grow in complexity, invest in monitoring, retry handling, and dead-letter queues. The patterns are well-established, and the investment in robust webhook infrastructure pays dividends across every integration you build.

External Resources