Glossary

Webhook

Webhook is an HTTP request that a service sends to a URL you control when an event happens, so you get data pushed to you instead of polling an API. You meet webhooks in payment, Git hosting and messaging platforms, which call your endpoint with a JSON body such as an order paid or a push received. No RFC defines webhooks, so each provider sets its own header names, signing scheme and retry rules.

How it works

You register a public HTTPS URL with the provider and choose the events you want. When one occurs, the provider sends a POST request with the event as a JSON body and waits for a response. Any 2xx status tells the provider the delivery succeeded. Anything else, or a timeout, counts as a failed delivery. Stripe retries failures for up to three days in live mode, while GitHub does not redeliver automatically.

Providers differ on timing. GitHub.com expects a 2XX response within 10 seconds, or 30 on GitHub Enterprise Server, and otherwise closes the connection and marks the delivery as failed. Stripe tells you to return a 2xx before running any complex logic. The usual pattern is to verify, store the event, answer immediately and process it in a background job.

Anyone can POST to a public URL, so you must verify that the sender is the provider. Stripe and GitHub both sign the request with HMAC-SHA256 using a shared secret. Stripe sends a Stripe-Signature header with a timestamp t and a signature v1, and signs the string timestamp, a dot, then the raw body. GitHub sends X-Hub-Signature-256 as sha256= followed by the hex digest of the raw body.

The example below uses the Stripe style. It signs a body, then verifies the valid request, a tampered body and a replayed request outside a 300 second window.

const crypto = require("crypto");
const secret = "whsec_test_123";
const body = '{"id":"evt_001","type":"order.paid","amount":4200}';
const ts = "1759660800";
const sign = (b) => crypto.createHmac("sha256", secret).update(`${ts}.${b}`).digest("hex");
const header = `t=${ts},v1=${sign(body)}`;
console.log(header);
function verify(raw, hdr, now) {
  const [t, v1] = hdr.split(",").map((p) => p.split("=")[1]);
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${raw}`).digest();
  const given = Buffer.from(v1, "hex");
  const ok = given.length === expected.length && crypto.timingSafeEqual(given, expected);
  return ok && Math.abs(now - Number(t)) <= 300;
}
console.log("valid:", verify(body, header, 1759660830));
console.log("tampered:", verify(body.replace("4200", "9900"), header, 1759660830));
console.log("replayed:", verify(body, header, 1759660800 + 3600));
t=1759660800,v1=fa8900ff365e3f897824bfe8e9a49c3c006997e541928fa76002897c342472bd
valid: true
tampered: false
replayed: false

What is the difference between a webhook and an API?

A webhook is pushed to you by the provider when something happens, while an API call is a request you make when you want data. With polling you ask repeatedly and most answers are empty. With a webhook you answer one request per event. Some events carry only part of the data, so integrations often call the provider's API afterward to fetch the full object.

Common pitfalls

  • Parsing the body before verifying: the signature covers the exact bytes sent. Stripe states that any manipulation of the raw body makes verification fail. Re-serialized JSON changes spacing and key order, so capture the raw body first.
  • Comparing signatures with a plain equals check: a normal string comparison can leak timing information. GitHub recommends a constant-time function such as crypto.timingSafeEqual, or hmac.compare_digest in Python.
  • No replay protection: a valid signed request can be captured and sent again. Include a timestamp in the signed data and reject old ones. Stripe libraries default to a 5 minute tolerance, and Stripe warns against a tolerance of 0.
  • Processing duplicates: providers can deliver the same event more than once. Store the event IDs you have handled and skip repeats, so the handler is idempotent.
  • Assuming order: neither Stripe nor GitHub guarantees that events arrive in the order they happened. Do not use arrival order for state changes, and fetch the current object when order matters.
  • Slow handlers: doing the work inside the request risks the timeout, a failed delivery and, with Stripe, a retried event. Acknowledge first, then queue the work.

Related terms

  • HTTP — webhooks are plain HTTP POST requests with a response status.
  • HTTPS — endpoints should be served over TLS so payloads and secrets are not exposed.
  • HMAC — the keyed hash most providers use to sign deliveries.
  • JSON — the usual body format of a webhook event.
  • Idempotent — handlers must tolerate the same event arriving twice.
  • REST — the pull-style API that webhooks complement.

See also

  • Cheatsheet: HTTP Status Codes — what each 2xx, 4xx and 5xx code means when you answer a delivery.