Webhooks
The Clinical API exposes one inbound webhook: Stripe billing events at
POST /api/billing/webhook. It is server-to-server and is not part of the client SDK.
Endpoint
POST /api/billing/webhook
- No JWT. Authentication is via Stripe's signature, not Auth0.
- The route reads the raw request body (a dedicated content-type parser keeps the body
unparsed) and verifies it with
stripe.webhooks.constructEvent(body, signature, secret). - The signature is taken from the
stripe-signatureheader; the secret isSTRIPE_WEBHOOK_SECRET.
If Stripe is not configured, the endpoint returns 503. A missing signature returns 400;
a failed signature verification returns 400 with the verifier's message. On success it
returns { "received": true }.
Handled events
| Event | Action |
|---|---|
checkout.session.completed | Retrieves the subscription and upserts the local subscription row (tier, status, period, cancelAtPeriodEnd). |
customer.subscription.updated | Updates the existing subscription's status/tier/period. Warns if not found locally. |
customer.subscription.deleted | Marks the subscription CANCELED. |
invoice.payment_failed | Marks the subscription PAST_DUE. |
Any other event type is logged at debug and acknowledged without action.
Status mapping
Stripe statuses are normalized to the local enum:
| Stripe | Local |
|---|---|
active | ACTIVE |
past_due | PAST_DUE |
canceled | CANCELED |
unpaid | UNPAID |
trialing | TRIALING |
incomplete | INCOMPLETE |
incomplete_expired, paused | CANCELED |
| (anything else) | INCOMPLETE |
Tier mapping
The tier is derived from the subscription's Stripe price id: it matches
STRIPE_PLUS_PRICE_ID → PLUS, otherwise → CORE.
Period extraction
In Stripe SDK v21+, current_period_start / current_period_end live on the
subscription item, not the subscription. The handler reads them from the first item,
falling back to the subscription's created timestamp.
Local testing
Use the Stripe CLI to forward events to your local API:
stripe listen --forward-to localhost:3001/api/billing/webhook
# copy the printed whsec_... into STRIPE_WEBHOOK_SECRET, then:
stripe trigger checkout.session.completed
Required environment
| Variable | Purpose |
|---|---|
STRIPE_SECRET_KEY | Stripe API access. |
STRIPE_WEBHOOK_SECRET | Verifies webhook signatures. |
STRIPE_CORE_PRICE_ID / STRIPE_PLUS_PRICE_ID | Map prices ⇄ tiers. |
FRONTEND_URL | Success/cancel/return redirect targets. |
The client-facing billing flow (
checkout,verifyCheckout,portal,subscription) is documented in the API reference and wrapped by the SDK. The webhook is the asynchronous counterpart that keeps the local subscription state in sync.