# What is a webhook? How it works, webhook vs API and how to secure it

> A webhook is an HTTP request a service sends to your URL when something happens. How webhooks work, webhook vs API polling, and how to receive them safely.

- URL: https://computese.com/what-is-a-webhook/
- Author: Duong Quan Nguyen, CEO, Computese
- Published: 2026-09-25
- Updated: 2026-10-09
- Topics: Integrations, Web development

## In short
- A webhook is an HTTP request a provider sends to a URL you register when an event happens, so you are told about it instead of polling an API.
- Verify every delivery: recompute the provider's HMAC-SHA256 signature from the raw body and your secret, compare in constant time, and check the signed timestamp where the provider sends one.
- Answer with a 2xx within seconds (as of October 2026, GitHub waits 10 seconds and Shopify 5) and do the work from a queue.
- Expect duplicates, out-of-order events and gaps: dedupe by event ID, fetch current state from the API, and reconcile on a schedule.
- If you send webhooks, sign them, retry with exponential backoff and jitter, let customers replay failures, and block internal addresses to prevent SSRF.

A webhook is an HTTP request that a service sends to a URL you registered when an event happens, such as a payment succeeding or code being pushed. The request is usually a POST with a JSON body. You are told about the event as it happens, instead of calling an API again and again to ask.

This guide covers how a delivery works, how webhooks differ from polling an API, and how to receive one safely. For retries in depth, see our guide to [dead letter queues](https://computese.com/dead-letter-queue/), and for where webhooks fit among other approaches, [system integration patterns](https://computese.com/system-integration/).

## What is a webhook, and how does it work?

A webhook reverses the direction of an API call. [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md), an open specification, calls webhooks HTTP callbacks and compares them to a "reverse API": a client makes an API call when it wants something from a service, and the service triggers a webhook when it wants to tell the client about an event.

A delivery follows these steps:

1. **You register a URL and choose events.** On [GitHub](https://docs.github.com/en/webhooks/about-webhooks) you specify a URL and subscribe to events, on [Stripe](https://docs.stripe.com/webhooks) you register an HTTPS endpoint, and on [Shopify](https://shopify.dev/docs/apps/build/webhooks) your app subscribes to webhook topics.
2. **Something happens.** An order is created or an issue is opened.
3. **The provider sends an HTTP request to your URL.** The body carries the event data, and headers carry metadata such as the event type, a delivery ID and a signature.
4. **Your endpoint answers with a 2xx status.** A late answer or an error counts as a failure.

A common use is keeping a website in step with its content. The Next.js documentation says to [configure the content source to trigger a webhook](https://nextjs.org/docs/app/getting-started/revalidating) that calls `revalidateTag` when the content changes, so a [headless CMS](https://computese.com/headless-cms/) can tell your site that its content changed.

Your endpoint is "a public, unauthenticated POST handler unless you add the authentication yourself", [as OWASP puts it](https://cheatsheetseries.owasp.org/cheatsheets/Webhook_Security_Cheat_Sheet.html). Without verification, [Stripe warns](https://docs.stripe.com/webhooks), an attacker could send fake events to trigger actions such as fulfilling orders, granting account access or modifying records.

## Webhook vs API: what is the difference?

Polling and webhooks are two ways to learn that data changed. When you poll, you call the provider's API on a schedule. With a webhook, the provider calls you. GitHub says webhooks are used [to receive data as it happens](https://docs.github.com/en/webhooks/about-webhooks), as opposed to polling an API to see if data is available.

![In the top row a laptop sends four request arrows to a server and three come back empty. In the bottom row a server sends one orange envelope to a laptop when an event happens.](https://computese.com/images/blog/what-is-a-webhook/polling-vs-push.e41fa15e53-1536.webp)

*A webhook sends one request when something happens; polling sends requests whether or not anything changed.*

|                               | Polling an API                                                                           | Webhook                                            |
| ----------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------- |
| Who starts the request        | You                                                                                      | The provider                                       |
| Delay before you see a change | Up to one polling interval                                                               | Near real time                                     |
| Load and API rate limit       | A request on every poll, even when nothing changed; many resources can use up your quota | A request per event                                |
| Reliability                   | Each request reads the current state                                                     | A delivery can be missed, repeated or out of order |
| GitHub suggests it for        | Information needed once or now and then, or a small set of resources                     | Monitoring many resources                          |

The two work together: a webhook is the signal that something changed, and the API is the source of truth for its current state. Stripe's snapshot event carries a copy of the object that [can be stale by the time your application processes it](https://docs.stripe.com/events/how-events-work), so Stripe says to fetch the latest version from the API before acting. Shopify's documentation says your app [shouldn't rely on receiving data from webhooks](https://shopify.dev/docs/apps/build/webhooks) and recommends a reconciliation job that fetches objects updated since the last run. For the API half of this pair, see our guide to [enterprise API development](https://computese.com/the-future-of-enterprise-api-development/).

## What does a webhook delivery look like?

A delivery is an HTTP POST with the event in the body and metadata in headers, as [Shopify](https://shopify.dev/docs/apps/build/webhooks/delivery-structure) describes its HTTPS deliveries. This is [GitHub's documented example](https://docs.github.com/en/webhooks/webhook-events-and-payloads) of an issue being opened, trimmed (`...` marks omitted fields):

```http
POST /payload HTTP/1.1
X-GitHub-Delivery: 72d3162e-cc78-11e3-81ab-4c9367dc0958
X-Hub-Signature-256: sha256=d57c68ca6f92289e6987922ff26938930f6e66a2d161ef06abdf1859230aa23c
Content-Type: application/json
X-GitHub-Event: issues

{
  "action": "opened",
  "issue": { "number": 1347, ... },
  "repository": { "full_name": "octocat/Hello-World", ... },
  "sender": { "login": "octocat", ... }
}
```

Read it from the top:

- **`X-GitHub-Event` and `action`.** The event is `issues` and the action is `opened`. GitHub says to [check both before processing](https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks), because it keeps adding event types and new actions to existing ones.
- **`X-GitHub-Delivery`.** A globally unique identifier (GUID) for the event. It stays the same if you request a redelivery, so it works as a deduplication key.
- **`X-Hub-Signature-256`.** The HMAC hex digest of the request body, sent only if the webhook has a secret.
- **The body.** JSON, or URL-encoded form data if you choose it. GitHub caps payloads at 25 MB and does not deliver a larger one.

## How do you receive a webhook safely?

A receiver that holds up in production does five things, in this order:

1. **Accept POST over HTTPS on a route used for nothing else.** If your framework checks CSRF tokens, exempt this route only: the provider cannot send a token, and OWASP names the signature check as the replacement control.
2. **Read the raw body.** The signature covers the exact bytes the provider sent.
3. **Verify the signature**, and the timestamp if the provider signs one. On failure, answer 401 with a generic body.
4. **Store the event with its ID** in a durable queue or table, and skip an ID you already have.
5. **Return a 2xx status.** A worker does the real work afterwards.

> [!WARNING]
> Verify the bytes you received, not a re-serialized copy: parsing the JSON and writing it out again can change whitespace and key order, and the signature stops matching. Stripe requires the raw body, and with Express [`express.json()` must come after your webhook route](https://docs.stripe.com/events/manage-webhook-endpoints).

As of October 2026, [GitHub](https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks) expects a 2XX response within 10 seconds, then terminates the connection and counts a failure. [Shopify](https://shopify.dev/docs/apps/build/webhooks/verify-deliveries) allows one second to connect and five seconds for the whole request, and treats any response outside the 200 range, 3XX included, as an error. Stripe gives no number and says to return a 2xx before any complex logic that might cause a timeout. Treat the shortest figure as your budget.

The way to meet that budget is a queue: your endpoint writes the verified event to durable storage and returns, and a worker processes it later. Stripe warns that a spike of deliveries, such as all subscriptions renewing at the start of the month, might overwhelm endpoint hosts that process events synchronously, and OWASP says to [return 200 only after the event is durably queued or processed](https://cheatsheetseries.owasp.org/cheatsheets/Webhook_Security_Cheat_Sheet.html).

![A burst of envelopes from a cloud reaches a small server, which places them in an orange row of boxes. A gear-shaped worker takes them out one at a time and passes them to a database.](https://computese.com/images/blog/what-is-a-webhook/queue-buffer.c74bb3290c-1536.webp)

*The endpoint only stores the event, and the worker drains the queue at its own pace, so a burst of deliveries does not become a burst of work.*

The handler below covers steps 2, 3 and 5 for GitHub's scheme and leaves step 4 as a comment. It uses the standard `Request` and `Response` types, so adapt the entry point to your framework.

```ts
import { createHmac, timingSafeEqual } from "node:crypto";

function signatureMatches(
  secret: string,
  rawBody: Buffer,
  header: string | null,
): boolean {
  const expected =
    "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const given = Buffer.from(header ?? "");
  const wanted = Buffer.from(expected);
  // timingSafeEqual throws if the lengths differ, so check them first.
  return given.length === wanted.length && timingSafeEqual(given, wanted);
}

export async function POST(request: Request): Promise<Response> {
  const secret = process.env.GITHUB_WEBHOOK_SECRET;
  if (!secret) return new Response(null, { status: 500 }); // fail closed

  // Raw bytes, read before any JSON.parse: the signature covers exactly these.
  const rawBody = Buffer.from(await request.arrayBuffer());
  const header = request.headers.get("x-hub-signature-256");
  if (!signatureMatches(secret, rawBody, header)) {
    return new Response(null, { status: 401 });
  }

  const deliveryId = request.headers.get("x-github-delivery");
  const eventType = request.headers.get("x-github-event");
  // Save { deliveryId, eventType, rawBody } to a durable queue or table here,
  // skipping a deliveryId you have already stored.
  console.log("accepted", deliveryId, eventType, rawBody.length);
  return new Response(null, { status: 200 });
}
```

A missing secret returns 500 instead of skipping verification, so a missing variable cannot open the endpoint. The comparison is constant-time: GitHub says never to use a plain `==` operator, and [`crypto.timingSafeEqual`](https://nodejs.org/api/crypto.html), which Node.js documents as suitable for comparing HMAC digests, throws when the buffers differ in length. In Python, use [`hmac.compare_digest`](https://docs.python.org/3/library/hmac.html) on bytes: for strings it accepts only ASCII characters.

## How does webhook signature verification work?

A signature lets you check that the request came from someone who holds the shared secret and that the signed bytes were not changed on the way. The mechanism is HMAC, which [RFC 2104](https://www.rfc-editor.org/rfc/rfc2104.html) describes as a mechanism for message authentication using cryptographic hash functions, in combination with a secret shared key. The provider computes HMAC-SHA256 over a message with your secret and sends the result in a header; you compute the same value from what you received and compare the two.

![A server sends two envelopes toward a gate. The envelope with an orange wax seal that matches the receiver's stamp passes the raised barrier. The unsealed envelope stops at the lowered barrier.](https://computese.com/images/blog/what-is-a-webhook/signature-check.e0856b14ca-1536.webp)

*The receiver checks the signature against the shared secret before it trusts anything else: a delivery that does not match is stopped at the gate.*

Providers differ in what they sign and how they write it, so follow the provider's scheme or use its library, as Stripe recommends.

|                | GitHub                     | Stripe                                            | Shopify                 |
| -------------- | -------------------------- | ------------------------------------------------- | ----------------------- |
| Header         | `X-Hub-Signature-256`      | `Stripe-Signature`                                | `X-Shopify-Hmac-Sha256` |
| What is signed | The body                   | The timestamp, a `.` and the body                 | The body                |
| Header value   | `sha256=` and a hex digest | `t=` timestamp, then one or more `v1=` signatures | A base64 digest         |

GitHub [publishes a test pair](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries), so you can check an implementation without any provider: the secret `It's a Secret to Everybody` and the payload `Hello, World!` must produce `sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17`. OpenSSL reproduces it:

```bash
printf '%s' 'Hello, World!' | openssl dgst -sha256 -hmac "It's a Secret to Everybody"
# the hex digest at the end of the output is the value after "sha256=" above
```

A valid signature does not stop a replay: an attacker who captures a delivery can send the same bytes again. Two defences work together:

- **A signed timestamp with a tolerance window.** Stripe signs the timestamp together with the body, so changing it breaks the signature, and its libraries default to a tolerance of five minutes between the timestamp and the current time. Never set the tolerance to 0, which turns the recency check off.
- **Event ID tracking.** OWASP says to keep authenticated event IDs for at least twice the tolerance window, ten minutes for a five-minute window.

GitHub signs the body only, so, as OWASP points out, neither a delivery timestamp nor the `X-GitHub-Delivery` header is authenticated. The header helps identify ordinary redeliveries but gives no authenticated replay protection.

[OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Webhook_Security_Cheat_Sheet.html) says to treat webhook secrets like database credentials: a distinct secret for each webhook, kept in a secrets manager and never hard-coded in source. To rotate without dropping deliveries, Stripe lets you keep the old secret active for up to 24 hours and sends one signature per active secret, so accept a delivery if any one matches.

## What else protects a webhook endpoint?

The signature is the main control. These cover what it leaves open:

- **HTTPS only.** A signature does not encrypt the data, so, as Standard Webhooks notes, it may be possible to eavesdrop and view the payloads. Stripe requires HTTPS in live mode. Our guide to [man-in-the-middle attacks](https://computese.com/understanding-man-in-the-middle-mitm-attacks/) shows how interception works.
- **An IP allowlist as an extra layer.** GitHub publishes its delivery addresses at `GET /meta`, and Stripe recommends an allowlist together with signature verification. OWASP calls allowlisting "extra layer only": ranges change, and a shared egress IP is not proof of identity.
- **Validation after verification.** A valid signature proves who sent the payload, not that its contents are safe, says OWASP. Enforce a maximum body size, validate against a schema and use parameterized queries downstream.
- **Quiet logs.** Log the event ID, type, status and latency, but keep full request bodies (they often contain personal data), signing secrets and signature headers out.

Our [secure coding checklist](https://computese.com/best-practices-for-secure-coding/) covers the same habits for any code that takes outside input.

## What happens when a webhook delivery fails?

Plan for three things: events that arrive more than once, events that arrive out of order, and events that never arrive.

### How retries differ by provider

As of October 2026, the [GitHub](https://docs.github.com/en/webhooks/using-webhooks/handling-failed-webhook-deliveries), [Stripe](https://docs.stripe.com/events/how-events-work) and [Shopify](https://shopify.dev/docs/apps/build/webhooks/verify-deliveries) documentation says:

|                   | GitHub                                                                                                                                                                                                                                                    | Stripe                                                                                                                                | Shopify                                                                                                                          |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Automatic retries | None                                                                                                                                                                                                                                                      | Live mode: up to three days with exponential backoff. Sandbox: three times over a few hours                                           | 8 times over the next 4 hours; after 8 consecutive failures the subscription is deleted if it was configured using the Admin API |
| Recovery          | [Redeliver](https://docs.github.com/en/webhooks/testing-and-troubleshooting-webhooks/redelivering-webhooks) deliveries from the past 3 days, in the web interface or with the REST API; a script on a schedule can redeliver any whose status is not `OK` | Resend from the Dashboard (up to 15 days) or [with the CLI](https://docs.stripe.com/webhooks) (`stripe events resend`, up to 30 days) | A reconciliation job that fetches objects updated since its last run                                                             |

### Duplicates

Retries are not the only source of duplicates: Shopify says they can follow a network timeout. Stripe says that in some cases two separate `Event` objects are generated and sent, and that you can identify these duplicates by the ID of the object in `data.object` together with the event `type`. Standard Webhooks tells receivers to use `webhook-id` as an idempotency key, and CloudEvents says consumers may assume that events with the same `source` and `id` are duplicates.

The defence is an idempotent receiver: handling an event twice has the same effect as handling it once. Key it on the provider's ID: `X-GitHub-Delivery` at GitHub, the event `id` at Stripe and `X-Shopify-Webhook-Id` at Shopify. Two Shopify subscriptions to the same topic get different `X-Shopify-Webhook-Id` values but share one `X-Shopify-Event-Id`.

A unique key makes the database do the check. In PostgreSQL, [`ON CONFLICT DO NOTHING`](https://www.postgresql.org/docs/current/sql-insert.html) "simply avoids inserting a row", and the command reports how many rows it inserted:

```sql
CREATE TABLE webhook_events (
  provider TEXT NOT NULL,
  event_id TEXT NOT NULL,
  raw_body TEXT NOT NULL,
  PRIMARY KEY (provider, event_id)
);

-- psql prints INSERT 0 1 for a new event and INSERT 0 0 for a duplicate: on 0, answer 2xx and stop.
INSERT INTO webhook_events (provider, event_id, raw_body)
VALUES ('github', '72d3162e-cc78-11e3-81ab-4c9367dc0958', '{"action":"opened"}')
ON CONFLICT (provider, event_id) DO NOTHING;
```

### Order and gaps

Do not rely on order. Stripe does not guarantee that events arrive in the order they were generated, and Shopify does not guarantee ordering within a topic: a `products/update` can arrive before `products/create`. GitHub [may deliver webhooks in a different order](https://docs.github.com/en/webhooks/testing-and-troubleshooting-webhooks/troubleshooting-webhooks) than the events took place. When order matters or the event may be stale, fetch the object's current state from the API and act on that.

Gaps need a different tool: Shopify says webhook delivery "isn't always guaranteed", and your own handler can fail or be down, so run a scheduled reconciliation job that compares what the provider says changed with what you processed. Events your worker keeps failing on belong in a dead letter queue with an alert, as OWASP recommends. Our guide to [dead letter queues](https://computese.com/dead-letter-queue/) covers retries with backoff and idempotency.

## Is there a standard for webhooks?

GitHub, Stripe and Shopify each chose their own header names and signing format, so code written for one does not verify another. Two open efforts address parts of the problem.

[Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md) (version 1.0.0) standardizes the delivery: three headers (`webhook-id`, `webhook-timestamp` and `webhook-signature`), a signed string of `id.timestamp.body`, and guidance on retries and status codes. Signatures are HMAC-SHA256 (`v1`) or ed25519 (`v1a`), and the header is a space-delimited list so that a sender can sign with an old and a new secret during rotation.

[CloudEvents](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md) standardizes how event metadata is described, and the Cloud Native Computing Foundation [announced its graduation on January 25, 2024](https://www.cncf.io/announcements/2024/01/25/cloud-native-computing-foundation-announces-the-graduation-of-cloudevents/). Version 1.0.2 requires `id`, `source`, `specversion` and `type` on every event. Its [HTTP webhook specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/http-webhook.md) requires HTTPS and POST, forbids following redirects, and authenticates with a bearer-style token rather than an HMAC signature header.

## What should you do if you send webhooks?

If your product sends webhooks, the receiver's problems become yours. The Standard Webhooks specification and OWASP agree on the core:

- **Sign every delivery** with a secret unique to the endpoint, and put the message ID and timestamp inside the signed content.
- **Retry with exponential backoff and jitter** over several days. The specification's example schedule spans just over 75 hours: the first attempt at once, then waits of 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 14 hours, 20 hours and 24 hours.
- **Read status codes as HTTP intends.** A 3xx is a failure, so update the URL instead of following it. A 410 means disable the endpoint. Throttle on 429, 502 and 504.
- **Show receivers their failures** and let them replay one webhook or a range.

SSRF is the sender-side risk. Customers give you URLs and your servers call them, so an attacker can register an internal address or a cloud metadata endpoint. OWASP's [SSRF prevention cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html) gives webhooks as a case where allow lists cannot be used and a block-list, "not an impenetrable wall", is the best solution. OWASP's [webhook sheet](https://cheatsheetseries.owasp.org/cheatsheets/Webhook_Security_Cheat_Sheet.html) adds the steps: accept `https://` URLs only, reject any address that is not globally reachable, connect to the address you validated, disable redirects (or apply the same checks to every redirect target), and run delivery workers in a network segment that cannot reach internal services.

## How do you test and debug webhooks?

GitHub, Stripe and Shopify each offer a way to test a receiver without waiting for real events. GitHub [does not accept localhost](https://docs.github.com/en/webhooks/testing-and-troubleshooting-webhooks/troubleshooting-webhooks) as a webhook URL, so its [testing guide](https://docs.github.com/en/webhooks/testing-and-troubleshooting-webhooks/testing-webhooks) uses a smee.io forwarder, and Shopify's CLI can [trigger a test delivery](https://shopify.dev/docs/apps/build/webhooks/subscribe):

```bash
# Stripe: forward events to a local port (it prints a signing secret), then trigger one
stripe listen --forward-to localhost:4242/webhook
stripe trigger payment_intent.succeeded

# GitHub: forward through a smee.io channel
npm install --global smee-client
smee --url WEBHOOK_PROXY_URL --path /webhooks/github --port 3000

# Shopify: test delivery for one topic
shopify app webhook trigger --api-version=<version> --address=<destination> --topic=<topic-name>
```

To test verification without a provider, sign a body with `openssl` as above, send it with `curl`, then change one character of the body and confirm that the endpoint answers 401.

When a real delivery fails, start with the provider's delivery log: GitHub keeps a recent deliveries log, and [Stripe's Event deliveries tab](https://docs.stripe.com/webhooks) shows the HTTP status code of each attempt. Log the event ID on every request so you can match your entries to theirs.

## How do you fix the most common webhook failures?

| Symptom                                                 | Likely cause                                                                                                                                                                      | Fix                                                                                                    |
| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| Every signature check fails                             | The body was parsed or re-serialized first, for example by `express.json()` running ahead of the webhook route                                                                    | Verify the raw bytes, and register the webhook route before any global JSON middleware                 |
| Checks pass locally but fail in production              | The secret belongs to another endpoint or mode (Stripe issues a different secret for test and live use of one endpoint), or no secret is set, so GitHub sends no signature header | Copy the secret for this endpoint and mode, and confirm the header arrives                             |
| Deliveries are marked failed although the handler works | The endpoint does its work before it answers, so the provider's timeout passes (as of October 2026, GitHub 10 seconds and Shopify 5)                                              | Store the event, return 2xx, and do the work from a queue                                              |
| Duplicate orders, emails or charges                     | A retry after a network timeout, a manual redelivery, or two events for one change                                                                                                | Record the event ID under a unique key before any side effect, and make the side effect idempotent     |
| Valid deliveries are rejected as too old                | Your server clock has drifted, or the timestamp is checked in a worker after the event waited in a queue                                                                          | Synchronize the clock with NTP, and check the timestamp in the handler before queuing                  |
| Events never arrive                                     | The endpoint was down and the provider stopped retrying (GitHub does not retry), or a Shopify subscription was deleted after repeated failures                                    | Reconcile against the API on a schedule, redeliver within the provider's window, and alert on failures |

Computese builds these pieces as part of its [integrations service](https://computese.com/services/integrations/): webhooks with signature verification, durable queues, idempotent consumers, exponential backoff with jitter, dead-letter queues with safe replay, and daily reconciliation.

## Key terms
- **Webhook**: An HTTP request that a provider sends to a URL you registered when an event happens, usually a POST with a JSON body. Also called an HTTP callback or a reverse API.
- **Polling**: Calling an API on a schedule to see whether data has changed. It sends a request on every poll, even when nothing changed, and notices a change only at the next poll.
- **HMAC**: A hash-based message authentication code (RFC 2104): a hash function such as SHA-256 combined with a shared secret key. Webhook signatures use it to show the sender knows the secret and the signed bytes are unchanged.
- **Raw body**: The exact bytes of the HTTP request body before any parsing. Signatures are computed over these bytes, so parsing and re-serializing the JSON before you verify breaks the check.
- **Replay attack**: Re-sending a captured request whose signature is still valid. A signed timestamp with a tolerance window, plus tracking of event IDs, limits it.
- **Idempotent handler**: Code that has the same effect when it processes an event twice as when it processes it once, usually because it records the provider's event ID before it acts.
- **Exponential backoff with jitter**: A retry schedule in which the wait grows after each failure (exponential backoff) and is randomized (jitter), so that many failed deliveries do not all retry at the same moment.
- **Dead letter queue**: A holding queue for events that keep failing after retries, so they can be inspected, raise an alert and be replayed after a fix instead of being lost or retried forever.
- **SSRF**: Server-side request forgery: an attacker makes a server send a request to a destination the attacker chose, such as an internal service. Webhook senders are exposed because customers supply the URLs.
- **Standard Webhooks**: An open specification (version 1.0.0) for webhook headers, signatures and delivery behaviour, so that receivers can handle different providers in a consistent way.

## Common questions

### What is a webhook in simple terms?

A webhook is a message one system sends to another over HTTP when something happens, such as a payment succeeding or code being pushed. You give the sender a URL and it calls that URL with the details. You hear about events as they happen instead of asking an API whether anything has changed.

### What is the difference between a webhook and an API?

An API answers requests that you make. A webhook is a request the provider makes to you when an event happens, which is why the Standard Webhooks specification compares it to a reverse API. Use both: the webhook says something changed, and an API call fetches the current state, as Stripe advises for snapshot events.

### Are webhooks secure?

Not by default. A webhook endpoint is a public URL that accepts POST requests, so anyone can send it one. Verify an HMAC signature on the raw body with a constant-time comparison, check the timestamp where the provider signs one, require HTTPS, and treat an IP allowlist as an extra layer only.

### Do webhooks retry?

It depends on the provider. As of October 2026, Stripe retries live-mode events for up to three days with exponential backoff, Shopify retries 8 times over 4 hours, and GitHub does not automatically redeliver failed deliveries. Build the handler to expect duplicates, and run a reconciliation job to catch gaps.

### How do you verify a webhook signature?

Read the raw request body, compute HMAC-SHA256 with your secret over whatever the provider's documentation says is signed, and compare the result with the signature header using a constant-time function such as crypto.timingSafeEqual in Node.js or hmac.compare_digest in Python. GitHub signs the body; Stripe signs the timestamp, a dot and the body. Answer a mismatch with 401.

### How quickly must a webhook endpoint respond?

As of October 2026, GitHub terminates the connection if it has not received a 2XX response within 10 seconds, and Shopify allows one second to connect and five seconds for the whole request. Stripe asks for a 2xx before any complex logic. Acknowledge first and do the work from a queue.

### Can I test webhooks on localhost?

GitHub cannot deliver to localhost or 127.0.0.1, so use a forwarding service such as smee.io. The Stripe CLI forwards events to a local port with stripe listen --forward-to, and the Shopify CLI can send a test delivery with shopify app webhook trigger.

## Sources
1. [Standard Webhooks specification, version 1.0.0](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md), Standard Webhooks
2. [About webhooks](https://docs.github.com/en/webhooks/about-webhooks), GitHub Docs
3. [Receive Stripe events in your webhook endpoint](https://docs.stripe.com/webhooks), Stripe Docs
4. [About webhooks](https://shopify.dev/docs/apps/build/webhooks), Shopify Developer Docs
5. [Getting Started: Revalidating](https://nextjs.org/docs/app/getting-started/revalidating), Next.js
6. [Webhook Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Webhook_Security_Cheat_Sheet.html), OWASP Cheat Sheet Series
7. [How events work](https://docs.stripe.com/events/how-events-work), Stripe Docs
8. [Webhooks delivery structure](https://shopify.dev/docs/apps/build/webhooks/delivery-structure), Shopify Developer Docs
9. [Webhook events and payloads](https://docs.github.com/en/webhooks/webhook-events-and-payloads), GitHub Docs
10. [Best practices for using webhooks](https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks), GitHub Docs
11. [Manage webhook endpoints](https://docs.stripe.com/events/manage-webhook-endpoints), Stripe Docs
12. [Verify webhook deliveries](https://shopify.dev/docs/apps/build/webhooks/verify-deliveries), Shopify Developer Docs
13. [crypto](https://nodejs.org/api/crypto.html), Node.js Documentation
14. [hmac: Keyed-Hashing for Message Authentication](https://docs.python.org/3/library/hmac.html), Python Documentation
15. [Validating webhook deliveries](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries), GitHub Docs
16. [RFC 2104: HMAC: Keyed-Hashing for Message Authentication](https://www.rfc-editor.org/rfc/rfc2104.html), IETF
17. [Handling failed webhook deliveries](https://docs.github.com/en/webhooks/using-webhooks/handling-failed-webhook-deliveries), GitHub Docs
18. [Redelivering webhooks](https://docs.github.com/en/webhooks/testing-and-troubleshooting-webhooks/redelivering-webhooks), GitHub Docs
19. [Troubleshooting webhooks](https://docs.github.com/en/webhooks/testing-and-troubleshooting-webhooks/troubleshooting-webhooks), GitHub Docs
20. [INSERT](https://www.postgresql.org/docs/current/sql-insert.html), PostgreSQL Documentation
21. [CloudEvents, version 1.0.2](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md), CloudEvents (CNCF)
22. [HTTP 1.1 Web Hooks for Event Delivery, version 1.0.2](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/http-webhook.md), CloudEvents (CNCF)
23. [Cloud Native Computing Foundation announces the graduation of CloudEvents](https://www.cncf.io/announcements/2024/01/25/cloud-native-computing-foundation-announces-the-graduation-of-cloudevents/), Cloud Native Computing Foundation
24. [Server-Side Request Forgery Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html), OWASP Cheat Sheet Series
25. [Testing webhooks](https://docs.github.com/en/webhooks/testing-and-troubleshooting-webhooks/testing-webhooks), GitHub Docs
26. [Manage webhook subscriptions](https://shopify.dev/docs/apps/build/webhooks/subscribe), Shopify Developer Docs
