Stripe Webhook: Example Payload, Headers and Signature

What Stripe actually sends to your endpoint — the event envelope, the Stripe-Signature header and the retry behaviour — with a webhook URL you can create here to catch a real one.

No webhook URL yet. Creating one needs no account.

Request details
Requests sent to your webhook URL appear here: method, headers and the body exactly as it arrived.

The button posts the example below to your webhook URL from your browser. It is not sent by Stripe, it carries your browser's User-Agent, and its signature is a placeholder that will not verify. To see a real delivery, follow the steps further down.

Last updated . How this page is checked.

How the webhook URL works. It is a public address that records whatever is sent to it and answers 200. An inbox keeps the 50 most recent requests, accepts bodies up to 256 KB, and is deleted 24 hours after it was created — or sooner if you press Delete URL or our server restarts, because requests are held in memory and never written to disk.

Anyone who has the URL can read what it received. Use sandbox and test-mode accounts, do not route production traffic or customer data through it, and delete it when you are done. Privacy details.

What is a Stripe webhook?

A Stripe webhook is an HTTP POST that Stripe sends to a URL you registered whenever something happens in your account: a payment succeeds, an invoice is finalised, a subscription is cancelled, a dispute is opened. The body is an Event object. Its type names what happened and data.object carries the API resource the event is about, in the shape of the API version recorded in api_version.

Card payments are asynchronous in ways a redirect cannot report — 3-D Secure, delayed payment methods, renewals that happen while nobody is on your site. The webhook is the only reliable signal that money moved, which is why fulfilment belongs in the webhook handler rather than on the success page.

What a Stripe webhook request looks like

The request line and headers as they reach your server, followed by the body. Ids, tokens and the signature are placeholders.

POST /your/webhook/path HTTP/1.1
Host: your-server.example
Stripe-Signature: t=1767225600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Content-Type: application/json; charset=utf-8
User-Agent: Stripe/1.0 (+https://stripe.com/docs/webhooks)
{
  "id": "evt_1PEXAMPLE0000000000000001",
  "object": "event",
  "api_version": "2024-06-20",
  "created": 1767225600,
  "type": "payment_intent.succeeded",
  "livemode": false,
  "pending_webhooks": 1,
  "request": {
    "id": "req_EXAMPLE00000001",
    "idempotency_key": null
  },
  "data": {
    "object": {
      "id": "pi_3PEXAMPLE0000000000000001",
      "object": "payment_intent",
      "amount": 2000,
      "amount_received": 2000,
      "currency": "usd",
      "status": "succeeded",
      "customer": "cus_EXAMPLE0000001",
      "payment_method": "pm_1PEXAMPLE0000000000000001",
      "metadata": {
        "order_id": "6735"
      }
    }
  }
}

Amounts are integers in the currency's smallest unit, so 2000 is 20.00 USD. livemode tells you whether the event came from live or test data — check it before you ship anything. The object in data.object is a snapshot from the moment the event was created; for anything money-related, many handlers re-fetch the object by id rather than trusting the snapshot.

Headers Stripe sends

HeaderExampleWhat it is
Stripe-Signaturet=1767225600,v1=5257a869e7ecebeda32affa62cd…Timestamp and one or more HMAC-SHA256 signatures. More than one v1 appears while a signing secret is being rolled.
Content-Typeapplication/json; charset=utf-8The body is always JSON.
User-AgentStripe/1.0 (+https://stripe.com/docs/webhooks)Identifies the sender. Not proof of origin — only the signature is.

Header names are case-insensitive. Your framework may show them lower-cased, and HTTP/2 always does.

How Stripe signs a webhook

Each endpoint has its own signing secret beginning whsec_. Stripe builds the string {timestamp}.{raw body}, computes an HMAC-SHA256 of it with that secret, and sends the hex digest as v1 in the Stripe-Signature header next to the timestamp t.

To verify, rebuild the same string from the header's t and the bytes you received, compute the HMAC, and compare it to every v1 value. Then reject the event if t is too old — Stripe's libraries default to a five-minute tolerance — so a captured request cannot be replayed against you later. Compute it over the raw request body exactly as received — before any JSON parsing — and compare with a constant-time function.

const crypto = require('crypto');

// rawBody: the request body exactly as received (string), not re-serialised JSON
function verifyStripe(rawBody, header, secret, toleranceSec = 300) {
  const t = (header.match(/(?:^|,)t=(\d+)/) || [])[1];
  const sigs = Array.from(header.matchAll(/(?:^|,)v1=([a-f0-9]+)/g), (m) => m[1]);
  if (!t || !sigs.length) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;

  const expected = crypto.createHmac('sha256', secret)
    .update(t + '.' + rawBody).digest('hex');
  return sigs.some((s) => s.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}

Retries, ordering and what to return

Return any 2xx quickly, before doing slow work. Anything else — a non-2xx status, a timeout, a TLS failure — counts as a failed delivery. In live mode Stripe retries with exponential backoff for up to three days, per its documentation at the time of writing; test-mode retries run on a much shorter schedule.

Two consequences shape a correct handler. Delivery is at-least-once, so the same evt_ id can arrive more than once: record processed event ids and make a repeat a no-op. And events are not guaranteed to arrive in order — invoice.paid can land before invoice.created — so do not build a state machine that assumes sequence.

Catch a real Stripe webhook

  1. Press Create webhook URL at the top of this page and copy the address.
  2. In the Stripe Dashboard, open Developers → Webhooks and add an endpoint (Stripe now also calls this an event destination). Use a sandbox or test-mode account.
  3. Paste the URL, choose the events to send — payment_intent.succeeded is a good first one — and save.
  4. Trigger the event: complete a test payment, or run stripe trigger payment_intent.succeeded with the Stripe CLI.
  5. The delivery appears in the inbox above within a few seconds, with the real headers and a real signature.

To check a real signature you also need the endpoint's whsec_ secret, shown on the endpoint's page in the Dashboard. That secret is yours to keep; never paste a live-mode one into a web page.

To run a caught request against your own code, select it and press Open in API Tester: the method, headers and body are loaded into the request console, where you enter your server's public URL and send it. A body you edit will no longer match its signature — that is the signature doing its job.

Things that catch people out

  • Body parsers break verification. If your framework parses JSON before your handler runs, the bytes are gone. Read the raw body for the webhook route (express.raw({ type: 'application/json' }) in Express).
  • One secret per endpoint, and per mode. The secret printed by stripe listen is different from the one in the Dashboard, and test and live endpoints do not share one.
  • Thin events exist. Newer event destinations can send a small notification that carries only ids; the handler then fetches the full object. Check which kind your endpoint is subscribed to.

api-tester.com is independent and is not affiliated with Stripe. Providers change header names, limits and retry schedules; treat the official Stripe documentation as the source of truth.

Related examples and tools

Other webhooks with a timestamp in the signature

References

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

Stripe webhook FAQ

What does a Stripe webhook payload look like?

A JSON Event object with id, object, api_version, created, type, livemode, pending_webhooks, request and data. The resource the event is about sits in data.object, shaped by the API version in api_version.

Which header carries the Stripe signature?

Stripe-Signature. It holds a timestamp t and one or more v1 values, each an HMAC-SHA256 of "timestamp.rawbody" keyed with the endpoint's whsec_ signing secret.

Why does my Stripe signature check fail?

Almost always because the body was parsed and re-serialised before verification, or because the wrong secret is in use — the CLI, test mode and live mode each have their own. Verify against the raw bytes with the secret of the exact endpoint that received the event.

Does Stripe retry failed webhooks?

Yes. A non-2xx response or a timeout is retried with exponential backoff — for up to three days in live mode according to Stripe's documentation at the time of writing. Handlers must therefore tolerate duplicates.

Can I receive Stripe webhooks on localhost?

Not directly; Stripe needs a public HTTPS URL. Use the Stripe CLI to forward events to a local port, or point the endpoint at a webhook URL created on this page to inspect deliveries without running anything.