API Design & Best Practices

Implementing Webhooks: A Complete Guide

Everything you need to build reliable webhooks: payload design, HMAC signature verification, delivery guarantees, and retry handling.

What Are Webhooks?

A webhook is an HTTP callback — your server makes a POST request to a URL registered by the consumer whenever a specified event occurs. Instead of the consumer polling your API repeatedly, your system pushes notifications the moment something happens.

Common use cases: payment confirmations (Stripe), push events (GitHub), form submissions (Typeform), and deployment notifications (Vercel).

Webhook vs Polling

PollingWebhooks
**Latency**Up to poll intervalNear real-time
**Server load**High (many idle requests)Low (event-driven)
**Client complexity**SimpleRequires public endpoint
**Reliability**Client controls retriesProvider must retry

Use webhooks when low latency matters and the consumer can expose a public HTTPS endpoint. Fall back to polling for environments behind firewalls.

Designing Webhook Payloads

A good webhook payload is self-describing and stable:

{
  "id": "evt_01HX4K9Z2M",
  "type": "order.completed",
  "created_at": "2024-03-15T14:22:31Z",
  "api_version": "2024-01-01",
  "data": {
    "object": "order",
    "id": "ord_9182",
    "amount": 4999,
    "currency": "usd",
    "status": "completed"
  }
}

Key design decisions:

  • Include a unique event ID — enables deduplication on the consumer side
  • Include created_at — consumers can detect and discard out-of-order delivery
  • Include type — consumers route to the right handler without parsing the body
  • Embed a snapshot of the object — consumers do not need to call back to fetch current state (thin vs fat payloads debate; fat is more reliable)
  • Include api_version — lets consumers handle schema evolution

Delivery Guarantees

Most webhook systems guarantee at-least-once delivery: events will be delivered at least once, but may be delivered more than once due to retries. Exactly-once delivery is extremely hard to guarantee across distributed systems.

Your consumer must be idempotent — processing the same event twice must produce the same result. Use the event id as an idempotency key, store processed IDs in a database, and skip duplicates.

Signature Verification (HMAC)

Never trust a webhook payload without verifying it came from your provider. The standard approach is HMAC-SHA256:

Provider side — compute a signature and send it in a header:

import hashlib, hmac

def sign_payload(payload: bytes, secret: str) -> str:
    sig = hmac.new(secret.encode(), payload, hashlib.sha256)
    return 'sha256=' + sig.hexdigest()
POST /webhooks HTTP/1.1
X-Signature-256: sha256=abc123...

Consumer side — verify the signature before processing:

import hashlib, hmac

def verify_signature(payload: bytes, header: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(
        secret.encode(), payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, header)  # constant-time compare

Always use hmac.compare_digest (or equivalent), never ==, to prevent timing attacks.

Retry and Failure Handling

When the consumer returns a non-2xx status code (or times out), the provider should retry with exponential backoff:

AttemptDelay
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry6 hours

After exhausting retries, move the event to a dead-letter queue for manual inspection. Alert the consumer via email or dashboard.

Consumer best practice: respond with 200 immediately, then process asynchronously. A 30-second processing timeout that causes a retry is a common source of duplicate processing.

Testing Webhooks

During development, use a tunnel to expose your local server:

# ngrok
ngrok http 8000
# → https://abc123.ngrok.io → http://localhost:8000

# Stripe CLI (for Stripe webhooks)
stripe listen --forward-to localhost:8000/webhooks/stripe

For testing retry logic, return 500 deliberately and observe the retry schedule. Log all incoming webhook payloads to a persistent store during development — it is much easier to replay stored payloads than to re-trigger real events.

Giao thức liên quan

Thuật ngữ liên quan

Thêm trong API Design & Best Practices