Skip to content
TryWebhook
New URL

Payments · Free · No signup

Test and debug Stripe webhooks

Point a Stripe endpoint at a throwaway URL, trigger an event, and see the request byte for byte — including the Stripe-Signature header you can then verify against your own signing secret, in your browser.

No signup · No email · Ready in about a second

At a glance

Stripe webhooks in brief

Category
Payments
Where to configure
Stripe Dashboard → Developers → Webhooks → Add endpoint
Signature header
Stripe-Signature
Secret
Signing secret (starts with `whsec_`)
Retries
In live mode, retried with exponential backoff for up to 3 days — roughly 16 attempts. In test mode, 3 attempts over a few hours. Any event can also be resent by hand from the Dashboard.
Response deadline
Respond well inside 20 seconds. Stripe's guidance is to acknowledge first and do the work afterwards.
Treated as success
Any 2xx. A 3xx redirect counts as a failure — Stripe does not follow them.

Cross-checked against Stripe's own webhook documentation. Providers change these; if something here is stale, tell us.

The payload

What Stripe actually sends

Shape-accurate, with obviously fake identifiers. These are the same samples the inspector's Send test webhook panel fires, so you can read one here and then send the identical bytes to your own URL.

POST/w/{your-bin-id}712 B

The event almost every Stripe integration starts with.

2 headers
user-agent
Stripe/1.0 (+https://stripe.com/docs/webhooks)
stripe-version
2025-06-30.basil
Body · application/json · 1 line
{"id":"evt_1PfakeEventIdExample","object":"event","api_version":"2025-06-30.basil","created":1750000000,"livemode":false,"pending_webhooks":1,"request":{"id":"req_fakeRequestId","idempotency_key":null},"type":"checkout.session.completed","data":{"object":{"id":"cs_test_a1FakeCheckoutSession","object":"checkout.session","amount_subtotal":2900,"amount_total":2900,"currency":"usd","customer":"cus_FakeCustomer","customer_details":{"email":"buyer@example.com","name":"Ada Lovelace","address":{"country":"GB","postal_code":"EC1A 1BB"}},"client_reference_id":"user_1234","metadata":{"plan":"pro","seats":"3"},"mode":"subscription","payment_status":"paid","status":"complete","subscription":"sub_FakeSubscription"}}}
POST/w/{your-bin-id}420 B

Dunning path — the one teams forget to handle.

2 headers
user-agent
Stripe/1.0 (+https://stripe.com/docs/webhooks)
stripe-version
2025-06-30.basil
Body · application/json · 1 line
{"id":"evt_1PfakeInvoiceFailed","object":"event","created":1750000600,"livemode":false,"type":"invoice.payment_failed","data":{"object":{"id":"in_1FakeInvoice","object":"invoice","attempt_count":2,"amount_due":2900,"currency":"usd","customer":"cus_FakeCustomer","customer_email":"buyer@example.com","next_payment_attempt":1750259200,"status":"open","subscription":"sub_FakeSubscription","last_finalization_error":null}}}

No sample carries a signature header. A valid one can only be produced with your own signing secret, and a fabricated one would make the Signature tab report a mismatch that isn't real. Send a genuine delivery when you want to test verification.

Verification

Verifying a Stripe signature

Signed payload

The header carries a timestamp and one or more signatures, as `t=1750000000,v1=<hex>`. Each `v1` is an HMAC-SHA256 of the string `{t}.{raw body}`, keyed with the endpoint's signing secret.

Implemented as: HMAC-SHA256 over `{timestamp}.{raw body}`, hex, compared against each v1 in the header.

Built in. Open any captured request, switch to the Signature tab, pick Stripe, and paste your signing secret (starts with `whsec_`). The HMAC is computed in your browser with the Web Crypto API over the raw bytes that arrived — the secret is never sent to us, and never written to disk.

Events

Events worth subscribing to first

Not the full catalogue — the handful that carry most integrations. Subscribing to everything is the fastest way to a handler that times out.

EventFires when
checkout.session.completedA Checkout Session finished paying. The usual place to provision access, but read `payment_status` — it can complete without being paid yet.
invoice.payment_failedA subscription renewal was declined. Check `attempt_count` and `next_payment_attempt` before you cancel anything.
customer.subscription.updatedPlan, quantity, status, or cancellation date changed. The single most useful event for keeping entitlements in sync.
payment_intent.succeededFunds were captured. Fires alongside Checkout events, which is why handlers double-provision without an idempotency key.
charge.refundedA refund was issued, in full or in part. `amount_refunded` is cumulative, not the amount of this refund.

Wiring a Stripe endpoint to a test URL

Create a URL above, then in the Stripe Dashboard open Developers → Webhooks → Add endpoint, paste it, and pick the events you care about. Keep test mode on. Stripe sends nothing at creation time — there is no ping event — so trigger something:

stripe trigger checkout.session.completed

Or make a real test payment. Either way the delivery lands here within a second, and the request appears in the list without a refresh.

Read the signature header before you write any code

Open the captured request and look at Stripe-Signature:

t=1750000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Two fields, comma-separated. The t is a Unix timestamp; v1 is the HMAC. Both matter. The signed string is not the body — it is the timestamp, a literal dot, then the body:

1750000000.{"id":"evt_1PfakeEventIdExample","object":"event",...}

That dot is where most first attempts go wrong. The second most common mistake is signing JSON.stringify(parsedBody) instead of the original bytes; the two differ in whitespace, in key order, and sometimes in Unicode escaping, and HMAC does not forgive any of it.

The header can also carry more than one v1 — Stripe includes both the old and the new signature during a secret rotation. Compare against every v1 present and accept if any matches.

Verify the timestamp too

A signature proves the payload came from Stripe. It does not prove when. Reject deliveries whose t is more than five minutes old, or an attacker who captured one valid request can replay it indefinitely. Stripe’s official libraries do this for you with a default tolerance of 300 seconds; hand-rolled verification usually forgets it.

Reading the payload

Every Stripe event has the same envelope — id, type, created, and data.object holding the resource that changed. Switch on type and nothing else:

switch (event.type) {
  case 'checkout.session.completed':
    return provision(event.data.object);
  case 'invoice.payment_failed':
    return startDunning(event.data.object);
  default:
    return; // 200, quietly
}

The default: return is not laziness. Stripe sends every event type you subscribed to, plus new ones it introduces later, and an endpoint that throws on an unrecognised type will fail deliveries you never wanted in the first place.

Expand data.object in the tree above and note how little of it you need. metadata and client_reference_id are the two fields worth planning around: they are the only way to connect a Stripe object back to a row in your own database without a lookup table.

Debugging a failing endpoint

When the Dashboard shows failed attempts, the question is which of three things happened, and each has a different fingerprint:

  • Nothing arrived here at all. The URL is wrong, or a firewall or WAF blocked Stripe. Compare the URL in the Dashboard against the one on this page, character for character, including the scheme.
  • It arrived but your server returned 4xx. Signature verification is failing. Verify the captured request here against the same secret your server uses — if it passes here and fails there, the difference is body handling, not the secret.
  • It arrived but timed out. Your handler is doing the work inline. Acknowledge with a 200 immediately, put the event on a queue, and process it after. This is also the fix for the “worked in dev, times out in production” report: dev had one event, production has a backlog.

Copy the request as cURL and replay it against your own endpoint to test the last two without waiting for Stripe. The bytes are identical, so a handler that rejects the cURL will reject Stripe.

Failure modes

Where Stripe integrations usually break

The raw body, not the parsed one

Stripe signs the exact bytes it sent. If your framework parses JSON before you verify — Express with `express.json()`, Next.js route handlers reading `req.json()` — the re-serialised body will have different whitespace and key order, and every signature check will fail. Read the raw body first, verify, then parse.

Test mode and live mode have different secrets

Each endpoint has its own signing secret, and the test-mode endpoint is a separate endpoint. A working integration that fails the moment you go live is almost always one secret in the environment where the other belongs.

Events are not ordered

Stripe makes no ordering promise. `customer.subscription.updated` can arrive before the `checkout.session.completed` that created it. Treat every event as a statement about the current state and re-fetch the object if the order matters.

Each endpoint is pinned to an API version

The payload shape follows the API version recorded on the endpoint when it was created, not your library version. Read `api_version` on the event — it explains most fields that are missing when the docs say they exist.

Duplicates are expected

A retry after your 200 got lost looks identical to a first delivery. Store `id` and drop events you have already processed; that one table is the difference between a refund and a double charge.

Answers

Stripe webhook questions

Do I need a Stripe account to try this?+

Not to look at the samples — the Send test webhook panel fires a shape-accurate `checkout.session.completed` with no account at all. You do need an account, in test mode, to see a real delivery with a real Stripe-Signature header.

Can I verify the Stripe-Signature here?+

Yes, for real deliveries. Open the request, go to the Signature tab, choose Stripe, and paste your `whsec_` secret. The HMAC is computed in your browser over the bytes that arrived; the secret never leaves the page and is never stored.

Why does my signature verification fail locally but pass here?+

Almost always because your framework re-serialised the body before verification. TryWebhook keeps the original bytes, so it can verify what your parsed-then-restringified body no longer matches.

Is this the same as the Stripe CLI?+

No, and they answer different questions. `stripe listen` forwards events into localhost, which is what you want while writing a handler. TryWebhook shows you the request itself — useful when the handler exists and you need to know what it is actually receiving, or when the code is deployed somewhere you cannot attach a CLI to.

How long does the URL last?+

A few hours from the last request, then the bin and everything in it is deleted. Nothing to clean up, and no way to leave a live payment webhook pointed at a URL you have forgotten about.

Other providers

Testing something else?

One click

Point Stripe at a URL and watch the payload land.

Stripe Dashboard → Developers → Webhooks → Add endpoint — paste the URL, trigger an event, and read exactly what arrived.

No signup · No email · Ready in about a second