Test Stripe webhooks without deploying
Capture checkout, invoice and subscription events, see the exact payload, and verify the Stripe-Signature header.
Get a URL that accepts the webhook#
A capture URL takes any method, any body, and records exactly what arrived: method, headers, query string, body and source IP. You do not need an account to make one, and an anonymous URL is deleted after 24 hours.
curl -X POST https://mocklane.com/api/v1/anon/endpoints \
-H "Content-Type: application/json" -d '{}'
# The response carries a capture URL of the form:
# https://mocklane.com/h/<endpoint-id>Paste that URL into Stripe at Workbench, Webhooks tab, Create an event destination, then trigger something. Events worth starting with:
checkout.session.completedpayment_intent.succeededinvoice.paidcustomer.subscription.deleted
Verify the Stripe-Signature header#
Stripe signs each request so you can tell a real delivery from anyone who has guessed your URL. The digest is HMAC-SHA256 over the timestamp, a full stop, then the raw body, hex-encoded, keyed with your signing secret (starts with whsec_).
- Header
Stripe-Signature- Value format
t=1492774577,v1=5257a869…- Algorithm
- HMAC-SHA256, hex
- Signed content
- the timestamp, a full stop, then the raw body
- Replay window
- 5 minutes, in Stripe's own libraries
const crypto = require("crypto");
const parts = Object.fromEntries(
req.headers["stripe-signature"].split(",").map((p) => p.split("=")),
);
// Stripe signs "{timestamp}.{rawBody}".
const expected = crypto
.createHmac("sha256", process.env.STRIPE_WEBHOOK_SECRET)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));crypto.timingSafeEqual. A plain === returns as soon as two bytes differ, and that timing difference is enough to recover a valid signature one byte at a time.What catches people out#
- Verification needs the raw body. Any framework that parses JSON and re-serialises it changes the bytes and the signature stops matching, which is the single most common Stripe webhook bug.
- Stripe sends an extra signature with a fake v0 scheme on test events. Only v1 is real, and accepting any scheme you find is a downgrade attack.
- While a signing secret is being rolled, the header carries one v1 per active secret. Code that reads only the last one will reject half the deliveries.
- A 3xx response counts as a failure. Stripe does not follow redirects on webhook deliveries.
Replay it until your handler is right#
A captured request can be re-sent to your own endpoint, with the body or headers edited first. That turns a bug that needed a real Stripe event into something you can reproduce on demand, which matters most for events that are awkward to trigger twice.
Assert on it in CI#
A scenario chains the steps into one run that passes or fails, so a change that breaks your Stripe handling fails the build rather than reaching production.
steps:
- type: send_webhook
body: { event: "checkout.session.completed" }
- type: wait_for_webhook
timeout_seconds: 30
assert:
- status_code equals 200Triggered from CI with an API token. See the scenarios documentation for the full step list.
Signature details checked against Stripe’s documentation on 22 October 2026. Schemes change: if this page and their docs disagree, believe theirs and tell us.