What Is a Webhook?

A webhook is an HTTP request that a service sends to your server when something happens: a payment succeeds, a commit is pushed, a form is submitted. You give the service a URL once; from then on it calls that URL with the details of each event. It is an API call in the opposite direction, and it replaces asking “anything new?” over and over. You can watch one arrive in the webhook tester.

Last updated . How this page is checked.

Webhook vs API: who makes the call

With an ordinary API, your code asks and the service answers. To learn that an invoice has been paid you would have to ask repeatedly — polling — and almost every answer would be “not yet”. A webhook turns the conversation around: the service makes the request, at the moment the event happens, and your code is the one answering.

Polling an APIWebhook
Who sends the requestYouThe provider
WhenOn your timerWhen the event happens
Delay before you knowUp to one polling intervalUsually seconds
Wasted requestsMost of themNone
What you must runA scheduled jobA public HTTPS endpoint
If your side is downYou catch up on the next pollYou depend on the provider's retries

The two are complements rather than rivals. Most integrations use webhooks to hear about changes and the API to fetch details or act on them, and keep an occasional reconciliation poll as a safety net for deliveries that never arrived.

How a webhook works, step by step

  1. You register a URL. In the provider's dashboard or through its API you enter an address on your server, such as https://example.com/hooks/payments, and choose which event types you want.
  2. The provider gives you a signing secret. You store it; it is how you will prove later that a request really came from them.
  3. An event happens on the provider's side.
  4. The provider sends an HTTP POST to your URL, with the event as the body.
  5. Your endpoint answers with a 2xx status to acknowledge receipt.
  6. If it does not — a timeout, a connection error, a 4xx or 5xx — the provider tries again later, typically several times with growing gaps.

What actually arrives

A delivery is an ordinary request. This one follows the common pattern:

POST /hooks/payments HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: ExamplePay-Webhooks/1.0
X-Event-Id: evt_01HZX3M9K2
X-Timestamp: 1767225600
X-Signature: sha256=5f1d0c…a93b

{
  "id": "evt_01HZX3M9K2",
  "type": "invoice.paid",
  "created": 1767225600,
  "data": { "invoice_id": "inv_8842", "amount": 4900, "currency": "usd" }
}

The body is usually JSON, though some providers send form-encoded fields. The headers carry three things worth finding in any provider's documentation: a unique event ID, a timestamp, and a signature. Header names and body shapes differ for every service; the webhook examples show the real ones for 30 providers, including Stripe, GitHub and Shopify.

Verify the signature before trusting anything

Your webhook URL is public. Anyone who learns it can send a request that looks like a payment confirmation. The signature is what stops that from working. The provider computes an HMAC of the request body, often with the timestamp added, using the secret only the two of you hold, and sends the result in a header. Your endpoint repeats the calculation and compares.

// Node.js
const crypto = require('crypto');

function isAuthentic(rawBody, timestamp, signatureHeader, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + rawBody)
    .digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Three details decide whether this works:

  • Sign the raw bytes. If your framework parses the JSON and you re-serialise it, the spacing and key order change and the signature no longer matches. Capture the body before any parser touches it.
  • Compare in constant time. An ordinary string comparison returns sooner the earlier the first difference is, which leaks information. Use the constant-time function your language provides.
  • Reject old timestamps. A captured request can be sent again later with its valid signature intact. Refusing anything more than a few minutes old closes that gap.

The exact string that is signed, the hash and the encoding all vary by provider, so follow their documentation rather than this sketch. A failed check should be answered with 401 or 403 and no further processing.

Writing a receiver that survives production

  • Answer fast, work later. Providers wait only a few seconds before counting a delivery as failed. Verify the signature, put the event on a queue, return 200, and do the slow work in a background job.
  • Expect duplicates. Delivery is at-least-once: if your 200 is lost on the way back, the same event is sent again. Record each event ID and skip any you have already handled. The idempotency guide covers the pattern.
  • Do not rely on order. A retried created event can arrive after the updated event that followed it. Use the timestamps in the body, or fetch the current state from the API, rather than assuming arrival order.
  • Return the status you mean. A 2xx stops retries, so never send one for an event you failed to store. A 5xx asks for a retry. A 410 tells some providers to stop sending to that URL for good.
  • Use HTTPS. The body often contains personal or payment data, and most providers refuse to deliver to a plain HTTP address.

Testing a webhook

The awkward part of development is that the provider cannot reach localhost. There are three usual ways around it.

  • Capture a real delivery first. The webhook tester gives you a temporary public URL and shows every request sent to it, headers and body included, so you can see exactly what a provider sends before writing any code.
  • Replay it at your own endpoint. Once you have a sample, send it to your local server from the API tester or with a curl POST. A replayed request carries the original signature, so it will only verify while the timestamp is still inside your tolerance.
  • Use the provider's own tools. Many offer a “send test event” button or a command-line tool that forwards events to your machine.

References

  • Standard Webhooks — An open specification for webhook signatures, timestamps and headers, adopted by a number of providers.
  • GitHub Docs: About webhooks — A provider's own explanation, including when to prefer webhooks over polling.
  • Stripe Docs: Webhooks — Signature checking, retries and best practice from a payment provider.
  • RFC 2104: HMAC — The keyed hash construction that webhook signatures are built on.

These links leave api-tester.com, which is independent of the organisations they point to.

Frequently asked questions

What is a webhook in simple terms?

A notification sent from one system to another as an HTTP request. You give a service a URL, and it sends a POST to that URL whenever a chosen event happens, with the details in the request body.

What is the difference between a webhook and an API?

Direction. With an API your code sends the request and the service responds. With a webhook the service sends the request and your code responds. Webhooks tell you that something happened; you then use the API to fetch more or act on it.

Is a webhook a GET or a POST?

Almost always a POST, with the event in the body as JSON or form fields. A few providers make a one-off GET to verify that you own the endpoint when you first register it.

Do webhooks need authentication?

Yes. The URL is public, so the receiver must check that each request is genuine, normally by recomputing an HMAC signature of the body with a shared secret and comparing it with the one in a header.

What should a webhook endpoint return?

A 2xx status, as quickly as possible, once the event has been safely stored. The response body is usually ignored. Any other status, or no response within the provider's timeout, counts as a failure and triggers a retry.

Why did I receive the same webhook twice?

Because delivery is at-least-once. If your acknowledgement is slow or lost, the provider cannot tell whether you received the event and sends it again. Store the event ID and ignore repeats.