Stripe signs each webhook with Stripe-Signature: t=<timestamp>,v1=<HMAC-SHA256>, computed over the timestamp and the raw body. Stripe's libraries reject signatures more than five minutes old. That makes captured Stripe events awkward to replay by hand: the body must be byte-exact and the timestamp must be fresh.
Receive Stripe events on localhost
hwcli listen payments --forward :3000/webhooks/stripe --include-sensitive
Add the endpoint URL in Stripe's Dashboard → Developers → Webhooks. --include-sensitive delivers the Stripe-Signature header, so your handler can run stripe.webhooks.constructEvent as it would in production. Live deliveries arrive within seconds, so their signatures are still fresh.
Replay with a fresh signature
Save the endpoint's signing secret (whsec_…) in Endpoint settings → Signature. HookWatcher stores it encrypted and never shows it again. Then:
hwcli replay evt_93df18ab --resign hwcli replay evt_93df18ab --set data.object.amount_received=0 --resign
Each replay gets a new timestamp and a valid v1 signature over the body that is actually sent. That includes modified bodies, so you can test edge cases without disabling verification in your code.
Things to test for Stripe
- Duplicates: Stripe can deliver an event more than once.
--duplicate 3, and key onevent.id. - Ordering:
payment_intent.succeededcan arrive beforepayment_intent.created. Replay them in reverse. - Signature rejection:
--invalid-signatureand--remove-signaturemust get a 400. - Raw bodies: if your framework parses JSON before your handler, verification fails on every event. Exact replays show this immediately.