Webhooks Fundamentals: How Webhooks Work, How to Secure Them and How to Test Them

Last updated: September 26, 202615 min read

A webhook is an HTTP callback that delivers event data from one system to another as it happens. When an event occurs (a payment succeeds, a user signs up, a commit is pushed), the source system sends an HTTP POST to a URL you registered, so you never have to poll for changes.

This guide covers how delivery and retries work, how to build and secure an endpoint, how to survive duplicates and out-of-order events, and how to test all of it on localhost, with provider details current as of September 2026.

Key takeaways

  • A webhook is an HTTP POST a provider sends to your URL when an event happens; it replaces polling with push.
  • Delivery is at-least-once: providers retry on timeouts and non-2xx responses (Stripe for up to 3 days, Shopify up to 8 times in 4 hours), so every handler must be idempotent. GitHub is the exception that doesn't retry automatically.
  • Verify the signature over the raw body before parsing JSON, check the timestamp, and compare in constant time; re-serialized JSON is the most common cause of "invalid signature".
  • Acknowledge with a 2xx within a few seconds (Shopify allows 5, GitHub 10) and do the real work from a queue.
  • Newer providers converge on two signing styles: Standard Webhooks headers (webhook-id, webhook-timestamp, webhook-signature, used by OpenAI) and asymmetric signatures verified with public keys from a JWKS URL (used by fal.ai).
  • To see what a provider really sends, point it at a Hooklistener endpoint: it captures the headers and body, forwards them to localhost with hooklistener tunnel --port 3000, replays them on demand, and lets Claude Code or Codex wait for a delivery through the Hooklistener MCP server.

What is a webhook?

A webhook is an HTTP request triggered by an event in a source system and sent to a destination system, often with a payload of data.

Webhooks enable automated communication between independent systems, allowing applications to notify each other about events in real-time without constant polling. They're the foundation of event-driven architectures and modern integration patterns.

How do webhooks work?

You register a URL with the provider once; after that, every matching event triggers a signed HTTP POST to it, retried until you answer with a 2xx.

1

Event Occurs

Something significant happens in the source system - a user signs up, payment completes, or file is uploaded.

2

HTTP Request Created

The source system automatically generates an HTTP POST request containing event details and relevant data.

3

Webhook Delivered

The HTTP request is sent to the configured webhook URL endpoint in the destination system.

4

Event Processed

The destination system verifies the signature, returns a 2xx quickly, and processes the event. If it times out or returns an error, most providers retry the same event later.

Webhook vs API: what's the difference?

An API is pull (you ask when you want data); a webhook is push (the provider tells you when something changes). They work together: the webhook signals the event, and an API call fetches the current state when you need to be sure.

APITraditional APIs (Pull Model)

  • Client requests data when needed
  • Requires constant polling for updates
  • Higher resource usage and latency
  • Client controls timing of requests

WHWebhooks (Push Model)

  • Server pushes data when events occur
  • Real-time event notifications
  • Efficient resource usage
  • Server controls timing of notifications

How do I build a webhook endpoint?

Creating a Webhook Endpoint

A webhook endpoint is a public HTTPS route that accepts a POST, verifies it, stores it and answers 2xx within a few seconds. In practice it:

  • Accepts POST requests (typically)
  • Processes JSON or form-encoded payloads
  • Returns HTTP status codes to indicate processing result
  • Handles requests asynchronously for better performance

Before you deploy to production, it pays to test your webhooks against a disposable inspection URL so you can confirm the exact headers, body, and timing of incoming requests without deploying half-finished handler code to a real environment.

// Basic webhook endpoint example
app.post('/webhook', async (req, res) => { try { const event = req.body; // Validate the webhook (signature, headers, etc.) if (!isValidWebhook(event, req.headers)) { return res.status(401).json({ error: 'Unauthorized' }); } // Process the event asynchronously await processWebhookEvent(event); // Return success immediately res.status(200).json({ received: true }); } catch (error) { console.error('Webhook processing failed:', error); res.status(500).json({ error: 'Internal server error' }); } }); async function processWebhookEvent(event) { // Handle different event types switch (event.type) { case 'user.created': await handleUserCreated(event.data); break; case 'payment.completed': await handlePaymentCompleted(event.data); break; default: console.log(`Unhandled event type: ${event.type}`); } }

Webhook Payload Structure

Typical webhook payloads include:

{ "id": "evt_1234567890", "type": "payment.succeeded", "created": 1672531200, "data": { "object": { "id": "pay_1234567890", "amount": 2000, "currency": "usd", "status": "succeeded", "customer": "cus_1234567890" } }, "api_version": "2022-11-15" }

How do I secure a webhook endpoint?

Signature Verification

Verify the provider's signature over the raw request body before you parse or trust it. Most providers use HMAC-SHA256 with a shared secret. Two details break most hand-written checks: verifying a re-serialized body instead of the raw bytes, and comparing buffers of different lengths (Node's timingSafeEqual throws instead of returning false):

const crypto = require('crypto'); // rawBody: the exact bytes received (e.g. express.raw()), never JSON.stringify(req.body) function verifyWebhookSignature(rawBody, signatureHex, secret) { const expected = crypto.createHmac('sha256', secret).update(rawBody).digest(); const received = Buffer.from(signatureHex || '', 'hex'); return received.length === expected.length && crypto.timingSafeEqual(received, expected); }

Additional Security Measures

HTTPS Requirements

  • • Always use HTTPS endpoints
  • • Validate SSL certificates
  • • Encrypt data in transit
  • • Use TLS 1.2 or higher

Access Control

  • • IP whitelist restrictions
  • • Authentication headers
  • • Rate limiting protection
  • • Request origin validation

What are the most common webhook problems?

At-Least-Once Delivery

Webhooks are typically delivered "at-least-once," meaning you might receive duplicates.

// Implement idempotent processing const processedEvents = new Set(); function processWebhook(event) { if (processedEvents.has(event.id)) { console.log('Duplicate event ignored:', event.id); return { status: 'already_processed' }; } // Process the event const result = handleEvent(event); // Mark as processed processedEvents.add(event.id); return result; }

Timeout Handling

Senders give you a few seconds before they count a delivery as failed (Shopify 5 s, GitHub 10 s) and retry it. Acknowledge first, then process asynchronously, ideally from a durable queue rather than in-process:

app.post('/webhook', (req, res) => { // Return 200 immediately res.status(200).json({ received: true }); // Process asynchronously setImmediate(async () => { try { await processWebhookEvent(req.body); } catch (error) { console.error('Async processing failed:', error); // Handle retry logic or dead letter queue } }); });

Out-of-Order Delivery

Webhooks may arrive out of chronological order. Design your system to handle events regardless of order, or implement ordering mechanisms using timestamps.

The best way to internalize these edge cases is to see them in action against a live endpoint. Try our free webhook tester to practice these concepts — send duplicates, replay payloads, and watch how retries actually behave before you ship a handler.

What changed in webhooks in 2025–2026?

Webhook tooling has matured quickly over the last two years. Event-driven architectures are no longer a niche pattern — they are the default for SaaS integrations, internal microservices, and AI-powered workflows. Several concrete shifts are worth calling out.

Standard envelopes and headers. Two specs are reducing per-provider guesswork. The Standard Webhooks spec fixes the signing headers (webhook-id, webhook-timestamp, webhook-signature); OpenAI uses it, so one verifier library covers every provider that does. The CNCF CloudEvents spec standardizes the payload envelope, giving consumers predictable fields (id, source, type, time) regardless of producer; Knative and Azure Event Grid speak it natively.

Retry policies differ a lot. Stripe retries live-mode events with exponential backoff for up to 3 days, Shopify retries up to 8 times over 4 hours, OpenAI keeps trying for up to 72 hours, and GitHub does not automatically redeliver failed deliveries at all. Know your provider's window, and implement idempotency either way.

Public-key signatures & key rotation. Shared HMAC secrets are still the default, but more providers sign with asymmetric keys and publish the public half at a JWKS URL. fal.ai, for example, signs with ED25519 and serves its keys at rest.fal.ai/.well-known/jwks.json, so there is no secret to leak and keys rotate without coordination. HMAC providers handle rotation by sending several signatures during a grace window. See webhook security fundamentals for verifying rotating keys safely.

AI-triggered event workflows. Agents now emit and consume webhooks directly — a model finishes a task, fires an event, and another system reacts. This raises the bar for schema discipline, auth, and audit trails, since autonomous producers amplify any weakness in your pipeline. Read more on what's next in webhook trends for 2025–2026.

Webhook Best Practices

Implementation Patterns

  • • Return HTTP 200 quickly (< 5 seconds)
  • • Process events asynchronously
  • • Implement idempotent operations
  • • Validate signatures and payloads
  • • Log all webhook events
  • • Use message queues for reliability

Monitoring & Observability

  • • Track success/failure rates
  • • Monitor processing latency
  • • Alert on signature failures
  • • Dashboard for webhook health
  • • Retry failed processing
  • • Dead letter queue setup

Common Webhook Use Cases

Payment Processing

Notify about successful payments, refunds, chargebacks, and subscription changes

User Management

Sync user registrations, profile updates, and account status changes

Content Publishing

Trigger builds, deployments, and content synchronization across platforms

Communication

Send notifications, messages, and alerts across multiple channels

Data Synchronization

Keep databases, analytics, and external systems synchronized

DevOps Automation

Trigger CI/CD pipelines, deployments, and infrastructure changes

How do I test webhooks locally? (with Hooklistener)

Give the provider a public HTTPS URL that records each delivery and forwards it to your machine, then replay captured requests while you fix the handler. Providers can't reach localhost, and most webhook bugs (wrong body bytes, missing headers, unexpected payload shape) are only visible in the real request.

  1. Capture: create an endpoint in the Hooklistener dashboard (the Free plan includes one), paste its URL into the provider's webhook settings, and trigger an event. You see every header and the exact body.
  2. Forward to localhost: install the CLI (npm i -g hooklistener or brew tap hooklistener/tap && brew install hooklistener), run hooklistener login, then hooklistener tunnel --port 3000.
  3. Replay: resend any captured request to your handler after each change, including edited bodies, instead of re-triggering the event in the provider.
  4. From your coding agent: connect the Hooklistener MCP server (claude mcp add --transport http hooklistener https://app.hooklistener.com/api/mcp) and Claude Code, Codex or Cursor can call create_endpoint, call wait_for_request, which waits for the matching webhook (as a task the agent follows, or blocking up to 60 s with blocking: true), and replay_request it against the code it just wrote.

FAQ

What is a webhook?

A webhook is an HTTP POST that a source system sends to a URL you register when an event happens, such as a payment succeeding or a job finishing. The body describes the event, usually as JSON, so your application reacts immediately instead of polling an API for changes.

What is the difference between a webhook and an API?

With an API your code asks for data (pull); with a webhook the provider sends data to you when something changes (push). Most integrations use both: the webhook says an event happened, and an API call fetches the current, authoritative state.

What are the best practices for signing and verifying webhook payloads?

Verify a signature over the raw request body before parsing it (HMAC-SHA256 with a shared secret, or an asymmetric signature such as ED25519 checked against the provider's published keys), include a timestamp in the signed content and reject anything older than about 5 minutes, compare signatures in constant time, and support two active secrets during rotation. IP allowlists are defense in depth, not a replacement.

How do I handle duplicate webhook deliveries?

Assume at-least-once delivery. Store each event ID (or a header such as webhook-id) with a unique constraint, skip events you have already processed, and make side effects idempotent. Providers retry on timeouts and non-2xx responses, so duplicates are normal, not an error.

How do I test webhooks on localhost?

Providers can't reach localhost, so give them a public HTTPS URL. Capture deliveries on a Hooklistener endpoint to inspect the exact headers and body, forward them to your machine with hooklistener tunnel --port 3000, and replay captured requests while you fix the handler. AI coding agents can do the same through Hooklistener's MCP tools create_endpoint, wait_for_request and replay_request.

Related Webhook Resources