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.