Keyring

Stripe webhook

Stripe's subscription lifecycle deliveries, and what authorises them.

Generated from 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.

Request
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"
      }
    }
  }'
Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "received": true,
  "id": "evt_reference_muqfmpe2",
  "outcome": "applied"
}

On this page