Stripe webhook
Stripe's subscription lifecycle deliveries, and what authorises them.
packages/api/src/billing/stripe-webhook.controller.ts by packages/docs/tools/generate-reference.mjs. Every example is a real exchange recorded against the control plane by packages/api/src/testing/reference-examples.spec.ts, with ids from that run. Credentials in the formats this product issues (kr_, krsk_, krses_, JWTs) and values under credential-named fields (password, *_secret, *_token, *_key, lookup_hash) are elided at record time; the recorded file and every page of this site are swept for credential shapes (kr_, krsk_, krses_, JWTs, URLs carrying a password, password fields) before either is accepted. Do not edit by hand: src/reference.spec.ts regenerates it and fails on a difference.POST /v1/webhooks/stripe -- the second unauthenticated route in this API
that changes state without holding a credential, and the same shape as the
first.
What authorises it is the signature over the raw body, and that check runs
before anything else: before the JSON is parsed, before a customer is
looked up, before a connection is borrowed. The body reaches this handler as
a Buffer precisely so that is true rather than merely intended --
app.factory.ts leaves this one route's payload unparsed, for the reason
Stripe's own documentation gives (the signature covers the exact bytes, and
re-serialising the JSON reorders keys and changes whitespace).
The answer is 200 for every delivery the signature admits, including the ones
this handler deliberately does nothing about. A 4xx tells Stripe the endpoint
is broken and, past its retry window, disables it; an event type we do not
handle is not a broken endpoint. A 5xx is reserved for a delivery that failed
for a reason a retry could fix, which is what an uncaught throw produces
through exception.filter.ts.
The response body names the outcome. It is not a protocol requirement -- Stripe reads the status and nothing else -- it is what makes a delivery legible in Stripe's own dashboard, where the response is shown beside the event.
POST /v1/webhooks/stripe
Answers 200 on success.
Authentication.
- No credential.
Example.
curl https://api.keyringdev.com/v1/webhooks/stripe \
-X POST \
-H "Stripe-Signature: $STRIPE_SIGNATURE" \
-H "Content-Type: application/json" \
-d '{
"id": "evt_reference_muqfmpe2",
"object": "event",
"api_version": "2026-08-26.dahlia",
"created": 1790913455,
"type": "checkout.session.completed",
"livemode": false,
"data": {
"object": {
"id": "cs_reference_muqfmpe2",
"object": "checkout.session",
"mode": "subscription",
"customer": "cus_reference_muqfmpe2",
"subscription": "sub_reference_muqfmpe2",
"client_reference_id": "01a0fac2-a367-76e5-9af4-ead368f3c163"
}
}
}'HTTP/1.1 200 OK
Content-Type: application/json
{
"received": true,
"id": "evt_reference_muqfmpe2",
"outcome": "applied"
}