A webhook is an HTTP request that one service sends to a URL you own, the moment
something happens on its side. A customer pays, a pull request is merged, an
order ships — and your server gets a POST about it within a second or two.
The name is unhelpful. There is no hook, and it is not really a “web” anything. It is just a callback: you hand a provider a URL, and the provider calls it.
The shape of one
Section titled “The shape of one”Here is a Stripe webhook, trimmed down. Nothing about it is exotic:
POST /hooks/stripe HTTP/1.1Host: api.acme.devContent-Type: application/jsonStripe-Signature: t=1758275400,v1=5257a869e7ecebeda32affa62cdca3fa…
{ "id": "evt_1PqR2sK8f", "type": "invoice.paid", "created": 1758275400, "data": { "object": { "id": "in_1PqR2s", "amount_paid": 4200 } }}Three parts matter. The body describes what happened. The signature
header proves the request came from Stripe and not from someone who found your
URL. And the status code you return tells Stripe whether to try again: a
2xx means you have it, anything else means you do not.
That last part catches people out. Your response is not a courtesy — it is the only signal the sender has, and it decides whether the event is retried or dropped.
Webhook vs API polling
Section titled “Webhook vs API polling”Both move the same data. The difference is who starts the conversation.
| Polling an API | Webhook | |
|---|---|---|
| Who calls whom | You call the provider | The provider calls you |
| Latency | As long as your interval | A second or two |
| Cost when idle | Every empty poll still costs a request | Nothing |
| What you must run | A scheduler | A public HTTPS endpoint |
| Failure mode | You fall behind quietly | You miss the event entirely |
Polling every minute means a thousand requests a day to learn about three payments. It also means the news is up to a minute old. A webhook costs nothing while nothing is happening and arrives almost immediately.
The catch is the last row. A poller that breaks catches up as soon as it comes back. A webhook endpoint that is down during the one minute an event fires may never hear about it again, depending on how forgiving the sender is.
In practice most integrations use both: the webhook tells you something changed, and then you call the API to read the authoritative current state. That also sidesteps out-of-order delivery, because the API always answers with the latest version.
Webhook vs WebSocket
Section titled “Webhook vs WebSocket”Different tools for different traffic.
A webhook is one request per event, to a public URL, with nothing held open in between. A WebSocket is one connection that stays open, carrying messages both ways until someone closes it.
Webhooks win for server-to-server events that happen occasionally and have to survive a restart, like a payment or a build finishing. WebSockets win when messages are constant and losing one barely matters — a price ticker, a cursor moving.
If you find yourself sending a webhook every few hundred milliseconds, you probably want a stream. If you find yourself keeping a WebSocket open all night to catch two events, you probably want a webhook.
What you actually have to build
Section titled “What you actually have to build”The first version of a webhook handler is four lines and works fine in development. The production version is a different animal, and shipping the first one is how most webhook outages start.
A public HTTPS endpoint. No localhost. Senders will not reach your laptop,
and most refuse plain http. During development you need a tunnel or a relay —
see testing webhooks locally.
Signature verification. Your URL is public and guessable. Without a
signature check, anyone can post a fake invoice.paid and see what your code
does with it. Verifying means recomputing an HMAC over the raw bytes you
received, before any JSON parser touches them, and comparing in constant time.
Every provider does this slightly differently — Stripe signs {timestamp}.{body}
and puts the result in Stripe-Signature, GitHub signs the body alone and sends
X-Hub-Signature-256, Shopify does the same in base64.
A fast response. Senders time out quickly. Stripe gives you 20 seconds,
GitHub 10. If your handler charges a card, writes to three tables and sends an
email before answering, you will eventually cross the line and the sender will
record a failure for an event you actually processed. Store the payload, answer
2xx, do the work afterwards.
Idempotency. You will receive the same event twice. Sometimes the sender retries after a response that got lost on the way back; sometimes it just sends duplicates. Store the event id and skip anything you have already handled.
Somewhere to put failures. Your endpoint will be down at some point — a deploy, a bad migration, an upstream that stopped answering. What happens to the events that arrive during those four minutes? If the answer is “nothing, they are gone”, you have a reconciliation job in your future.
A record of what arrived. When a customer says the webhook never came, you need to know whether it arrived and your handler broke, or it never arrived at all. Those have opposite fixes, and you cannot tell them apart from your application logs alone.
Retries are not guaranteed
Section titled “Retries are not guaranteed”There is no standard here, and the variation is wider than most people expect:
- Stripe retries with exponential backoff for about three days.
- GitHub does not retry. One attempt, and you can re-send it by hand from the repository settings.
- Shopify retries 19 times over 48 hours, then removes the subscription.
- Plenty of smaller providers try once and never mention it again.
So “the sender will retry” is not a plan. It is a property of one specific provider that you have to look up, and it changes when you add the next integration.
Where a webhook gateway fits
Section titled “Where a webhook gateway fits”A gateway sits between the sender and your app. It accepts the request, checks the signature, stores the whole thing, and only then forwards it to your handler on its own schedule.
The point is that accepting and processing become separate problems. The sender
always gets a fast 200, so it never starts its own retry storm and never
disables your endpoint for being unreliable. Your handler can be down for an
hour, and the events wait in a queue instead of evaporating. When something
fails, you have the raw request — headers, body, response, latency per attempt —
instead of a guess.
That is what Webhooker does: one ingest URL per sender, verification before anything is stored, and delivery to your endpoints with retries, filters and a dead-letter queue. The quick start goes from an empty account to a delivered event in four steps.
Common questions
Section titled “Common questions”Can a webhook return data to the sender? Only a status code and a short body, and almost no provider reads the body. Treat the response as an acknowledgement, not a channel.
What if I need the events in order? Assume you will not get them in order. Retries alone will shuffle them. Sort on a sequence number or timestamp inside the payload, or re-read the current state from the API and ignore arrival order.
Is a webhook secure over plain HTTP? No. The payload is readable in transit and the signature does not fix that. Use HTTPS.
How big can a payload be? It depends on the sender, and it is rarely documented. A GitHub push with hundreds of commits can run past a megabyte, which is enough to hit a size limit you did not know you had.
- Quick start — an ingest URL and a delivered event in four steps.
- Verify our signature — working verification code in Node, Python and Go.
- Inbound verification — the signature schemes Webhooker checks for you.