The loop
Create an endpoint and it prints a permanent ingest URL. Everything sent there is captured whole — headers, body, the exact bytes — before anything else happens. Stream those events to a port on your machine, and replay any one of them as often as you like while you fix the handler. Forwarding runs from your own machine, so it isn't a billable delivery.
What's different about Stripe
Stripe's event names are dotted strings that look easy to guess until they bite. A sub-resource gets its own segment, and it does not cascade: customer.subscription.updated fires nothing on customer.updated, so a destination subscribed to the parent sits silent while subscriptions churn. The second surprise is that retry behavior differs across the two halves of your test loop. An event created in a sandbox is retried three times over the course of a few hours; live mode backs off across three days. Tune a handler's failure path against sandbox timings and you have tuned it against the wrong curve.
Where the URL goes
Stripe keeps event destinations in Workbench, under the Webhooks tab. The button that starts a new one reads "Create an event destination" on Stripe's webhooks page and "Create new destination" inside Workbench itself. Two screens come before the interesting ones: which account the destination listens to, and which API version its event objects are rendered against. Then pick your event types, choose Webhook endpoint as the destination type, and drop the ingest URL into the Endpoint URL field. Registered endpoints have to be publicly accessible HTTPS URLs, and Stripe's webhook traffic speaks only TLS 1.2 and 1.3. You can register up to 16 webhook endpoints with Stripe, so prune dead ones before adding another. The same registration is available through the API.
Stripe's own CLI does the firing. Running stripe trigger payment_intent.succeeded fires a real test event rather than a canned sample. The other documented route is re-delivery rather than creation: Resend in the Dashboard, or stripe events resend from the CLI, both acting on an event that already exists. A Dashboard button for sending a fresh test event is not something Stripe's pages document.
What Stripe sends
Stripe names events resource.event: dot-separated, lowercase, with underscores living inside a segment rather than between them. A sub-resource earns an extra segment. The shape is regular enough to invite guessing, and the sub-resource rule is where guessing fails — the parent resource stays quiet when a sub-resource changes.
- payment_intent.succeeded
- payment_intent.payment_failed
- checkout.session.completed
- charge.succeeded
- invoice.paid
- customer.subscription.deleted
Worth knowing first
Sandbox retries give up early
Live mode retries a failing destination with exponential backoff for up to three days. An event created in a sandbox gets three attempts across a few hours and then stops, so the retry pressure you observe while testing is far shorter than what a broken production handler will actually sit through.
A redirect is recorded as an error
Stripe doesn't follow a 3xx on a webhook request — it logs the delivery as a failure, and tells you to register the URL the redirect resolves to. The rule that sits next to it on the same page: return the 2xx before any complex logic that could cause a timeout. An event showing a status code of 200 is what marks the delivery as successful.
Ordering isn't promised, repeats happen
Stripe states plainly that events aren't delivered in the order they were generated, and that an endpoint might occasionally receive the same event more than once. The documented defense is to make the destination independent of order, log the event IDs you have processed, and skip anything already logged.
Five minutes of clock tolerance
Stripe's signature libraries default to a five-minute tolerance between the Stripe-Signature timestamp and the current time, so a machine with a drifted clock will reject perfectly good payloads. Worth ruling out before you go hunting for a wrong signing secret.
Signature checking is handled for you once the secret is registered — you can also check a captured signature by hand in the signature verifier.