Skip to main content

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-signature header; the secret is STRIPE_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

EventAction
checkout.session.completedRetrieves the subscription and upserts the local subscription row (tier, status, period, cancelAtPeriodEnd).
customer.subscription.updatedUpdates the existing subscription's status/tier/period. Warns if not found locally.
customer.subscription.deletedMarks the subscription CANCELED.
invoice.payment_failedMarks 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:

StripeLocal
activeACTIVE
past_duePAST_DUE
canceledCANCELED
unpaidUNPAID
trialingTRIALING
incompleteINCOMPLETE
incomplete_expired, pausedCANCELED
(anything else)INCOMPLETE

Tier mapping

The tier is derived from the subscription's Stripe price id: it matches STRIPE_PLUS_PRICE_IDPLUS, 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

VariablePurpose
STRIPE_SECRET_KEYStripe API access.
STRIPE_WEBHOOK_SECRETVerifies webhook signatures.
STRIPE_CORE_PRICE_ID / STRIPE_PLUS_PRICE_IDMap prices ⇄ tiers.
FRONTEND_URLSuccess/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.