Stripe Webhooks

Stripe webhook receiver for subscription lifecycle management. It is the single writer of the local subscription mirror: the customer.subscription.* events own subscriptionStatus and copy Stripe's value verbatim. Stripe owns the vocabulary; there is no local translation.

Security

Every request is verified using the Stripe-Signature header and the endpoint's signing secret. Requests with a missing or invalid signature are rejected before any state change. This endpoint does not use SpeyBooks authentication; Stripe authenticates itself through the signature.

Handled Events

Subscription events pass an admission guard before any handler runs: the subscription's status must belong to the supported vocabulary below, or the event is refused with evidence (see Admission Control).

Stripe Event Effect on the mirror
checkout.session.completed Establishes the customer and subscription pointers (surfaced as hasStripeCustomer and hasSubscription); writes no status.
customer.subscription.created Writes subscriptionStatus (Stripe's value verbatim), currentPeriodEnd, and the derived cancelAtPeriodEnd; sets the plan from the price when the status is active or trialing; records the first-subscription timestamp once, immutable thereafter.
customer.subscription.updated Same effects as created.
customer.subscription.deleted Writes subscriptionStatus verbatim (canceled) and clears the derived cancelAtPeriodEnd. Everything else survives: the subscription pointer, the plan, currentPeriodEnd, trialEndsAt, and the first-subscription timestamp, so the cancelled mirror stays matchable to Stripe through the retention window.
invoice.paid Logs the payment; no status write.
invoice.payment_failed Logs the failure; no status write.
customer.subscription.trial_will_end Logged only; the pre-charge reminder is Stripe's own trial-ending email.

Unrecognised event types are logged and ignored.

Status Vocabulary

subscriptionStatus holds Stripe's status verbatim or not at all: the three customer.subscription.* events are its only writers, there is no local mapping, and a status is either copied character for character or refused, never translated. The supported vocabulary is trialing, active, past_due, unpaid, canceled. An unset status means the organisation has never subscribed (the cardless grace window after registration).

Admission Control

A subscription status outside the supported vocabulary (Stripe's incomplete, incomplete_expired, paused, or any status Stripe mints in future) never reaches the mirror. The guard records an unsupported_status receipt in the transition ledger, logs the refusal, and the delivery is acknowledged so Stripe never retries: a refusal is a permanent condition, not a transient one.

Two ledgers cooperate here, by design: an idempotency gate ledger (has this event been seen, and did processing succeed) and a transition ledger (what the event did to subscription state, and in what order). Both are written inside the one processing transaction. Neither replaces the other.

Envelope of Record

Two fields are read from their current Stripe locations rather than their historical ones. The billing period end is read from the subscription item, with the legacy top-level field retained as a fallback for replays of older-shaped events. The pending-cancellation state is derived: true when either the scheduled-cancellation timestamp or the legacy boolean says a cancellation is scheduled, false again when the schedule is cleared on resume. The cancelAtPeriodEnd field therefore carries the derived state, not a raw Stripe value. Every cancellation surface this product exposes schedules the cancellation at the period end.

Receipt Outcomes

Every subscription event leaves exactly one receipt in the transition ledger. applied means the event mutated the local record (any field; a pending-cancellation flag flip with the status unchanged is an application). no_op means the event matched the stored state and no write occurred. stale_discarded means the event was created strictly before the last applied event for its subscription and was not applied; Stripe's event creation time is the ordering authority. unsupported_status is the admission refusal above. Receipts are permanent records of what each event did at the time it was processed.

Reliability

Every event is processed inside a single transaction, and two outcomes are deliberately distinct. A refusal (unsupported status) commits normally: the receipt is recorded, the mirror untouched, the delivery acknowledged, no retry. A failure (a handler threw) rolls back, records the failure in the gate ledger, and signals Stripe to redeliver; the redelivery reprocesses through the idempotency gate. Stripe's backoff schedule bounds repeated failures.

Idempotency

The gate ledger is consulted, not merely written. A first sight of an event is processed and committed; a duplicate delivery of an already-processed event is skipped and acknowledged; a redelivery of a previously failed event reprocesses. Concurrent duplicates of a first delivery serialise on the gate; concurrent retries of a failed event are not serialised, but every handler writes absolute values (verbatim copies and id links, never increments), so parallel reprocessing is safe by construction.

Organisation Resolution

checkout.session.completed resolves the organisation from the checkout session's metadata reference (set during checkout creation). Subscription created and updated events consult the subscription's metadata reference first, then fall back to the stored customer link. A metadata reference that names an organisation owned by a different customer is never trusted: the reference is logged and resolution falls through to the stored customer link. Subscription deleted events and invoice events resolve through the stored customer link only.

Endpoints