What a webhook actually is
A webhook inverts the usual direction of an API call. Normally your code asks a provider for information; with
a webhook, the provider tells you the moment something happens. When a payment settles, a build finishes, a
subscription lapses or a message arrives, the provider sends an HTTP POST to a URL you have
registered with it. There is nothing exotic in the mechanism — your webhook endpoint is an ordinary HTTP route
that happens to be called by a machine rather than a browser.
The appeal is efficiency and latency. Polling an API every thirty seconds to ask "has anything changed?" wastes requests, burns rate limit, and still leaves you up to thirty seconds behind reality. A webhook arrives within a second or two of the event and costs nothing while nothing is happening. The price you pay is that you now operate a public HTTP endpoint whose availability, security and correctness are your responsibility, and whose failures are invisible to you unless you go looking. That is why webhook debugging deserves its own discipline.
The anatomy of a delivery
A typical delivery has four parts worth examining separately, because each one fails in a different way:
- The method and URL. Nearly always
POSTto the exact URL you registered. A trailing-slash mismatch that triggers a redirect is a classic silent killer, since many providers do not follow redirects and will record the301as a failed delivery. - The headers. Usually
Content-Type: application/json, plus provider-specific ones: a signature, a timestamp, an event type, a delivery id and a retry counter. These carry more diagnostic information than the body does. - The body. A JSON document describing the event. Some providers send the full changed object; others send only an identifier and expect you to fetch the current state yourself, which avoids acting on stale data when deliveries arrive out of order.
- Your response. A status code, and sometimes a body the provider logs. This is the only thing the provider learns about you, and it drives every retry decision it makes.
Inspecting a payload before you trust it
The first move when a webhook integration misbehaves is to look at the raw delivery rather than at your parsed representation of it. Almost every provider keeps a delivery log in its dashboard showing the exact headers, the exact body bytes and the response your server returned. Read that log before reading your own code. It tells you immediately whether the problem is that the event never arrived, arrived and was rejected, or arrived and was accepted but mishandled — three very different bugs that produce the identical symptom of "the thing didn't happen".
Log the raw body on your own side too, exactly as received, before any parsing. Store it alongside the headers and a received-at timestamp, and keep it for at least as long as your provider retries. When a payload turns out to be an unreadable single line, our JSON formatter will pretty-print and validate it in your browser. If any part of the delivery carries a token — some providers authenticate themselves with a bearer JWT rather than an HMAC — the JWT decoder will show you its claims and expiry.
Pay attention to the event type header and to schema versioning. Providers add fields to payloads over time, and a handler that assumes an exact object shape will break on an addition it should have ignored. Parse defensively: read the fields you need, tolerate the ones you do not recognise, and never assume the set of event types is closed.
Verifying HMAC signatures correctly
Your webhook URL is a public endpoint. Anyone who learns it can post whatever they like to it, so the payload is untrusted input until proven otherwise. The standard defence is an HMAC signature: you and the provider share a secret, the provider hashes the request body with that secret and puts the digest in a header, and you recompute the same digest and compare.
Three mistakes account for nearly every failed verification. The first and by far the most common is
signing the wrong bytes. The HMAC covers the raw body exactly as transmitted. If your web
framework parses the JSON and you re-serialise it before hashing, key order and whitespace will differ and
the digest will not match — even though the data is identical. Capture the raw body before any middleware
touches it. The second is comparing with ==, which returns as soon as two bytes
differ and leaks timing information an attacker can use to forge a signature one byte at a time; every
language ships a constant-time comparison function for precisely this reason. The third is ignoring
the timestamp. A signature stays valid forever, so a captured delivery can be replayed against you
indefinitely unless you also check that the signed timestamp header is within a few minutes of now and reject
anything older.
Worth knowing: many providers sign a constructed string rather than the body alone — something like the timestamp, a separator and the body concatenated together — and some publish a list of several valid signatures at once so that secrets can be rotated without downtime. Read the provider's specification rather than assuming, and accept a delivery if it matches any currently valid secret. If the header is Base64 rather than hex, our Base64 encoder and decoder is handy while you work out which encoding you are looking at.
Retries, idempotency and responding quickly
Webhook delivery is at-least-once. No provider can promise exactly-once, because the network can drop your response after you have already committed the work, leaving the provider unable to tell success from failure. It therefore retries, typically with exponential backoff over hours or days. Your endpoint will receive duplicates, and the only safe assumption is that it will receive each event an unknown number of times.
The remedy is idempotency, and it is straightforward. Every delivery carries a unique event or delivery identifier. Record each id you have fully processed in a table with a unique constraint, and make handling an already-recorded id a no-op that returns success. Now a duplicate delivery is harmless rather than a second charge, a second email or a duplicated row. Do the same for ordering: deliveries can arrive out of sequence, so if the payload carries a version or sequence number, discard anything older than the state you already hold.
Answer fast, work later
Most providers enforce a timeout in the range of a few seconds. If your handler synchronously calls three other services before replying, you will breach that timeout under load, the provider will record a failure, and you will get a retry storm on top of an already-struggling system. The pattern that avoids this is to validate the signature, persist the raw event to a queue or table, and return 200 or 204 immediately. Then process it in a background worker where you control retries yourself.
Choose your failure codes deliberately, because they mean different things to the sender. A
5xx says "try again" and will be retried. A
4xx generally says "this will never work" and many providers respond by
disabling the endpoint after repeated occurrences — so returning a 400 for a temporary database
outage can silently switch your integration off. The one deliberate exception is
401 for a signature that fails to verify, which is the correct and
honest answer to an unauthenticated request. Our
HTTP status code reference covers what each code signals, and the
HTTP headers guide explains the standard headers you will see alongside
the provider's custom ones.
Testing locally with a tunnel
A webhook provider on the public internet cannot reach a server running on your laptop, so local development needs a tunnel: a service that gives you a public HTTPS URL and forwards every request hitting it down to a port on your machine. Register the tunnel URL with the provider in a sandbox or test-mode account, trigger a real event, and you can set a breakpoint inside your handler and step through an actual delivery with real headers and a real signature. Most tunnels also show you an inspector of each forwarded request, which gives you the raw bytes you need for signature debugging.
Two cautions. Free tunnel URLs usually change every restart, so re-register after each one or your deliveries will vanish into a dead address. And a tunnel URL is genuinely public for as long as it is open — never point a production webhook at one, and never leave one running against a service holding real data.
Our request console sends from our servers, not from your browser, so it can only reach publicly routable addresses. Loopback and private network ranges are blocked, which means a tunnel is required before any external tool — ours included — can reach a service on your machine.
Replaying a captured webhook with our request console
This is where the API Tester request console earns its place in a webhook workflow. Once you have a real delivery captured — from the provider's dashboard, your tunnel inspector or your own raw-body log — you can replay it against your endpoint as many times as you need, without waiting for the provider to generate another event. Instead of creating test payments to debug one branch of a handler, you send the captured payload on demand and watch what your code does.
- Copy the raw body from the delivery log, byte for byte, without reformatting it. If you reformat, any signature you also copy will no longer match.
- Open the console at api-tester.com, set the method to
POSTand paste your endpoint's public URL. - Recreate the headers in the Headers tab:
Content-Type: application/jsonplus the provider's signature, timestamp, event-type and delivery-id headers. This is the step people skip, and it is the step that makes the replay realistic. - Paste the body into the Body tab and press Send.
- Read your own response — status code, timing and body — in the response pane. The timing figure is a direct check against the provider's delivery timeout.
From there, vary one thing at a time. Send the same delivery id twice to prove your idempotency guard works.
Corrupt one character of the signature to confirm you reject it with a 401 rather than
processing it anyway. Backdate the timestamp header to check your replay window. Strip a field the payload
normally includes to see whether your parser degrades gracefully or throws. Each of these is a real production
failure mode, and each takes about ten seconds to reproduce here.
One caveat to keep the replay honest: if you are verifying signatures — and you should be — a hand-edited body will not match the original signature, because the HMAC covers the exact bytes. Either replay the body completely unmodified, or point the replay at a development instance configured with a test secret so you can generate a matching signature yourself. The Copy as cURL button in the console turns a working replay into a shell command, which is the easiest way to script a regression test or attach a reproducible case to a bug report.
Related tools and references
API Tester home
Send the POST that replays a captured webhook against your endpoint.
REST API Tester
Exercise the REST endpoints your webhook handler calls downstream.
GraphQL Tester
Query a GraphQL API for the current state after an event arrives.
HTTP Status Codes
Choose the response code that tells the provider what you actually mean.
HTTP Headers
Understand the signature, timestamp and delivery headers on a webhook.
Base64 Encoder
Decode Base64-encoded signature headers while debugging HMAC checks.
Webhook testing FAQ
What is a webhook?
An HTTP POST that a provider sends to a URL you own when an event happens in their system, replacing the need for you to poll their API. Your endpoint is an ordinary HTTP route that receives a JSON body plus descriptive headers and answers with a status code.
Does this page give me a URL that collects webhooks?
No. We do not host webhook inboxes, and we would rather tell you than imply a feature we do not have. Use your provider's own delivery log or a tunnel inspector to capture deliveries, then replay them with our request console against an endpoint you control.
How do I verify a webhook signature?
Compute an HMAC over the exact raw body bytes — before any JSON parsing — using the shared secret and the algorithm the provider specifies, then compare it to the signature header with a constant-time comparison. Also check the signed timestamp so old deliveries cannot be replayed against you.
Why did I receive the same webhook twice?
Delivery is at-least-once. Slow responses, 5xx replies and dropped connections all trigger retries, and a retry can follow work you already completed. Record each delivery id you have processed and treat a repeat as a no-op.
What status code should my endpoint return?
200 or 204 as soon as the event is durably stored, with the real work done asynchronously. Return 5xx only when you genuinely want a retry, and avoid 4xx for transient failures — most providers treat 4xx as permanent and may disable the endpoint after repeated occurrences.
How do I test a handler running on my laptop?
Use a tunnelling service to give it a public HTTPS URL, register that URL with the provider's test mode, and debug real deliveries with breakpoints. Our console sends from our servers and cannot reach localhost or private network addresses, so a public URL is required either way.