What Is a Webhook? A Practical Guide
Understand webhook delivery, signature verification, retries, idempotency, and safe debugging with practical examples from Stripe, GitHub, and Slack.
A webhook is an HTTP callback sent by one service to another when an event occurs. Instead of repeatedly polling an API for changes, your application exposes an HTTPS endpoint and the provider sends an event—usually as a POST request with a JSON body.
The familiar analogy is a doorbell. Polling an API is like opening the front door every minute to check whether a parcel arrived. A webhook is the doorbell: you get on with other work, and the delivery only interrupts you when it actually happens.
Webhook versus API polling
Polling is pull-based: your application asks for the latest state on a schedule. Webhooks are push-based: the provider contacts your application only when something changes. Many integrations use both—webhooks for timely notifications and APIs to retrieve or reconcile authoritative state.
A representative event
{
"id": "evt_123",
"type": "payment.succeeded",
"created": "2026-02-01T10:30:00Z",
"data": { "amount": 2000, "currency": "usd" }
}Treat the payload as an untrusted notification. Verify its signature and, when correctness matters, fetch the referenced object from the provider API before changing critical state.
Build a reliable receiver
app.post('/webhooks/provider', rawBodyParser, async (req, res) => {
verifyProviderSignature(req.rawBody, req.headers)
await queueEvent(req.body)
res.sendStatus(202)
})The same shape in Python, using Flask:
@app.route('/webhooks/provider', methods=['POST'])
def provider_webhook():
verify_provider_signature(request.get_data(), request.headers)
queue_event(request.get_json())
return '', 202- Preserve the raw request body when the provider signs raw bytes.
- Return a successful response quickly and move slow work to a queue.
- Store the provider event ID and ignore duplicates.
- Log delivery metadata without recording secrets or unnecessary personal data.
- Apply body-size limits and accept only the HTTP methods and content types you expect.
Verify signatures correctly
Providers commonly use HMAC or asymmetric signatures. Follow the provider documentation exactly, compare signatures in constant time, validate timestamps when supplied, and rotate secrets safely. Header names and signed-message formats differ between providers, so one generic verifier is rarely sufficient.
The header carrying the signature differs by provider: Stripe sends Stripe-Signature, GitHub sends X-Hub-Signature-256, and Slack sends X-Slack-Signature alongside X-Slack-Request-Timestamp. What is signed also differs—some providers sign the raw body alone, others sign a timestamp concatenated with the body—so read the specification rather than assuming.
Retries and idempotency
Delivery is usually at least once, not exactly once. A timeout can cause the provider to retry even when your first attempt completed. Make processing idempotent by recording event IDs or using a business-level idempotency key before sending email, provisioning access, or recording a payment.
Debug without leaking production data
A request inspector is useful for viewing headers and payload structure, but its URL is a public receiver. Use synthetic or redacted events. SimpleTaskTools stores captured webhook requests for up to 24 hours when MongoDB is available and limits each inbox to the newest 100 requests. Clear the inbox when testing is complete.
Tunnelling tools that expose localhost work too, but most issue a new hostname on each restart, which means re-registering the endpoint with the provider every time. A stable inspection URL avoids that churn, and captured payloads can be replayed against a local server as often as you need.
Common mistakes
- Doing slow work inside the handler instead of enqueuing it.
- Skipping signature verification, which leaves the endpoint open to spoofed events.
- Ignoring duplicate deliveries, which can double-charge or double-notify.
- Returning a non-2xx status for errors you have already handled, causing needless retries.
- Logging nothing, which makes a failed delivery impossible to diagnose.
- Assuming the payload shape is frozen; providers add and deprecate fields over time.
Test before you rely on it
- Send test events from the provider dashboard where one is offered.
- Replay a captured event against staging.
- Return a 500 deliberately and confirm the provider retries as documented.
- Deliver the same event twice and confirm the outcome is unchanged.
- Check behaviour under concurrent deliveries, not just one at a time.
Release checklist
- HTTPS endpoint configured
- Signature and timestamp verified
- Duplicate events tested
- Retries simulated
- Secrets removed from logs
- Monitoring and dead-letter handling configured
Summary
Webhooks remove polling from an integration, but they move the reliability burden onto your receiver. Respond quickly, verify every signature, assume duplicates will arrive, and keep enough logs to reconstruct what happened. Those four habits cover most of what goes wrong in production.