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.
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
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.