Skip to main content
Faithful to the live Stripe API (test-mode capture 2026-06-09, account version pinned 2024-12-18.acacia). The object shapes, event names, error envelopes, IDs and formats below are exactly what real Stripe returns — this page is the vocabulary to assert against, not a tutorial on Stripe. If you know the API, the only new surface is the control layer at the bottom.

Objects

A decline does not park the PI in a failed state — it bounces back to requires_payment_method with last_payment_error set, and stays retryable.

Events

resource.action. data.object is a full snapshot sharing the REST serializer. Three origins drive them: your own API calls, the clock (renewals, auto-finalize, dunning), and the world (the issuing bank declines, demands 3DS, the cardholder disputes). 30 types captured live:
  • Era layering is real: a recurring price dual-writes a legacy plan.created; paying an invoice emits invoice.paid and deprecated-but-alive invoice.payment_succeeded and new-generation invoice_payment.paid. Handlers in the wild key on any of the three.
  • event.request.id is not a reliable did-I-cause-this marker — internally generated children of your own call (the subscription-create invoice) carry request: { id: null }.
  • invoice.upcoming has no persisted object (data.object.id is null) — it fires before the renewal invoice exists.
  • Delivery: Stripe-Signature: t=<ts>,v1=<hmac> — HMAC-SHA256 over "{t}.{raw_body}" keyed by the endpoint’s whsec_. Stripe returns that secret only on create; in the sandbox it’s stable and re-readable (test a webhook).

Errors

One envelope, but two surfaces — and error.code is per-class optional (whole families ship message-only: both 401s, bad Stripe-Version, “Invoice is already paid”). 1 · A call fails → HTTP error response.
Strictness boundary: names and enums are strict 400s; numeric bounds are silently clamped (limit=99999 → 200). 2 · A payment fails → the error is also durable state. The 402 body carries the issuer’s verdict and the full PaymentIntent, post-bounce:
…and the traces persist: a failed charge with failure_code + outcome, the PI’s last_payment_error, plus payment_intent.payment_failed and charge.failed events. Async failures (ACH returns, disputes) skip the synchronous envelope entirely — state + events are the only surface.

Magic values

Stripe is the one API whose documented test contract is magic inputs — so the pack honors them. Each documented test card is an alias for the matching world trigger: your existing suite keeps passing after the repoint, unmodified. Each recognized value desugars to the same apply-fn as the matching control-surface call — same state, same events, no separate code path. A documented Stripe test value the pack doesn’t recognize yet fails loudly (unrecognized Stripe test value) instead of silently succeeding — a repointed suite never passes for the wrong reason. For outcomes the real test mode can’t produce on demand (a decline at renewal, a dispute on a seeded charge, a webhook retry storm), use the control surface — that’s the point of the sandbox.

Test clocks

/v1/test_helpers/test_clocks is fully supported — create, attach customers (hidden from the plain customer list, exactly like real Stripe), advance, delete. Two deliberate upgrades over the real thing: A Stripe test clock here is a named handle on the world’s single clock — one timeline per world. Need two independent timelines? Run two worlds (worlds); that’s the CI model anyway. The only observable divergence: a suite can never catch the advancing status — if yours asserts it, it’s testing Stripe’s infrastructure, not your code.

Pinnable values

The fields that originate outside Stripe’s logic — the only ones you supply. Everything else (ids, status, totals, timestamps, number) is engine-derived and unfakeable.

Formats & conventions

Control (prod-break)

The only surface that isn’t real Stripe. Everything above is the vendor’s; this is how you make a given outcome happen on demand.
  • Event names and behavior are pack-declared — the engine is generic; the Stripe vocabulary above lives in this pack. sandbox.world.next() lists the world events whose preconditions hold.
  • Control calls hit /__admin__/*. Their errors go to your test, not the app under test, and are not Stripe’s error envelope.
  • Magic test inputs are honored here (unlike other packs) because they’re Stripe’s documented contract — each desugars to the same apply-fn as its world.trigger equivalent. They only cover call-time outcomes; renewal-time and webhook-time outcomes need the control surface.
  • Concepts: exogenous values · force a branch · world events · the clock · HTTP API.
Pinned against Stripe API 2024-12-18.acacia (test-mode capture 2026-06-09), guarded by contract tests that diff the pack against captured live responses.