What
This extends @emulators/stripe from a payments emulator into a Stripe Billing emulator, so you can test a SaaS subscription integration end to end against emulate with no Stripe account and no network:
- Webhooks as real Event objects:
evt_ids,/v1/events,/v1/webhook_endpoints(we_), per-endpoint delivery tracking, signed deliveriesconstructEventaccepts, and opt-in seeded delivery chaos (drop, duplicate, delay until flush, reorder), including across a whole test clock advance. - Payment methods and test cards: saved
pm_cards, attach and detach, customer default payment methods, the documented test-card table (declines, 3D Secure, 3D Secure 2, optional 3DS, already set up), PaymentIntent confirmation against card behaviour, and SetupIntents (seti_). - Invoices (
in_), invoice items (ii_), invoice payments (inpay_): draft, finalize, pay, void, mark uncollectible, attempt counting, and the hosted invoice page. - Prices: recurring, tiered (graduated and volume) and metered prices.
- Subscriptions (
sub_) and subscription items (si_): trials (including trial end without a payment method), plan and interval changes, scheduled and immediate cancellation withcancellation_details, pausing and resuming,send_invoicecollection, and Checkout insubscriptionmode. Billing periods live on the items (items.data[].current_period_start/end), as in current API versions. - Test clocks (
clock_):POST /v1/test_helpers/test_clocks/:id/advanceruns renewals, trial ends,invoice.upcoming, dunning retries andincomplete_expiredin simulated time. - Dunning: failed renewal payments retried on a configurable schedule, then
past_due,unpaid,canceledorpausedafter the last retry. - Usage-based billing: billing meters (
mtr_), meter events with identifier deduplication, event summaries, and metered lines priced from aggregated usage, including final usage on cancellation. - Customer portal (
bps_sessions): an emulator HTML portal for cancel, undo cancel and card update, pluscustomer.subscriptionsand the other includable customer fields throughexpand[]. - Errors carry Stripe's
type,code(resource_missingand so on),paramanddoc_url. - Seed config:
api_version, webhooks, webhook chaos, billing settings (retry schedule, after-retries action), customers with cards, default payment methods and test clocks, products, meters and recurring prices. Theemulate inittemplate now seeds a monthly price. - Emulator-only controls under
/_stripe/:webhooks/chaos,webhooks/flush,webhooks/deliveries,events/:id/redeliver,billing/settings, andsimulate/(3D Secure for payment and setup intents, saved-card behaviour). - Docs: the package README, the CLI help, the registry entry, the skill and the docs site.
Closes #324 Closes #325
Why
Billing is the part of a Stripe integration that is hardest to test. Renewals, trial ends, retries and metered invoices happen days or weeks later, and they reach your app only as webhooks. Stripe's test mode handles this with test clocks, but that needs an account, the network and real waiting for async webhooks. With this PR the same flows run in-process and deterministically: advance a clock, and the matching events have reached your webhook handler before the call returns.
How
- Follows the package's existing layout: entities and store collections, one route module per resource, shared helpers in
@emulators/core. - Object shapes, id prefixes, field names,
nullfields, enum values and status transitions follow the Stripe API reference for2026-10-28.endive(the defaultapi_version, which seed config can override). Wirenulls staynull. - Test clock advances run synchronously: Billing processes every due action up to the new
frozen_time, then delivers that advance's events in order before the response returns. - A Differences from real Stripe section in the package README lists every deliberate simplification.
Simplifications vs real Stripe (full list in the package README)
- Webhooks are delivered inline and in order before the response. There are no automatic delivery retries (only manual redelivery), one
api_versionfor every event, no thin or v2 events, andlivemodeis alwaysfalse. - Only cards exist, and only the documented test cards. There is no Radar, no exemptions, and 3DS is completed through an emulator route (
next_actionis alwaysuse_stripe_sdk). No SetupAttempts or mandates. - No taxes, discounts, coupons, credit notes, customer balance, shipping or Connect on invoices or subscriptions.
- No prorations. An interval change bills the full new period, and updates always behave as
payment_behavior=allow_incomplete. No subscription schedules, quotes,pause_collection,billing_cycle_anchor_configor thresholds.billing_modeis alwaysflexible. All items of a subscription share one currency and interval. - Billing time passes only for customers on a test clock. Without one there are no background timers, so trials don't end, periods don't renew and retries don't run. Advances are synchronous (the stored clock is already
ready), and clocks are never auto-deleted atdeletes_after. - Retries follow a deterministic custom schedule (default 3/5/7 days). No Smart Retries, automations or overdue-invoice settings beyond
send_invoiceturningpast_due. - Meter events are validated synchronously (400 instead of
v1.billing.meter.error_report_triggered). Usage is summed per customer and meter at finalization. There are no v2 meter event streams. - The customer portal and hosted invoice page are emulator HTML with a test-card select. There is one default portal configuration, and the portal cannot change plans or quantities.
- Prices and products have no update, search or delete API yet.
Idempotency-Keyis echoed but not replayed. Some error and validation messages are the emulator's own wording, and the README marks these.
How tested
- 413 Vitest tests in
packages/@emulators/stripe(22 files) cover each resource and SaaS scenarios end to end over HTTP: trial → paid → renewal → failed payment → retries → recovery or cancellation, metered billing, plan changes, pause and resume, portal flows, webhook chaos and test-clock edge cases. The 23 existing tests are unchanged and still pass. - The whole monorepo passes
pnpm build,pnpm type-check,pnpm lintandpnpm format:check. pnpm testpasses for every package except two tests inpackages/emulate(custom-config.test.tsandstart-generated-secrets.test.ts, the "listener cannot bind" cases). They fail the same way without this branch on a macOS host where a wildcard::listener does not block a127.0.0.1bind, soEADDRINUSEnever comes. This PR doesn't touch them.
Credits
This builds on earlier work: PR #194 by Ansonhkg ("Add Creem and expanded Stripe billing emulation") and the Stripe billing work in the josh-vincent/emulate fork. Thank you both. The package README credits them too.
🤖 Generated with Claude Code
Activity
46Review the following changes in direct dependencies. Learn more about Socket for GitHub.
| Diff | Package | Supply Chain Security | Vulnerability | Quality | Maintenance | License |
|---|---|---|---|---|---|---|
| npm/stripe@23.1.0 |
Additional findings
-
packages/@emulators/stripe/src/subscriptions.ts:726-729— Canceling a subscription while its renewal invoice is still a draft leaves automatic collection enabled, so advancing the clock charges the canceled subscription. -
packages/@emulators/stripe/src/routes/checkout-sessions.ts:384-392— Payment-mode Checkout displays a different total from the amount charged when a price uses unit_amount_decimal, transform_quantity or tiered pricing. -
packages/@emulators/stripe/src/billing.ts:357-362— Manually finalizing an auto-advancing invoice on a test clock leaves it open indefinitely because the scheduler excludes invoices without a previous payment attempt. -
packages/@emulators/stripe/src/invoicing.ts:398— After the latest invoice is voided, settling the preceding unpaid invoice leaves the subscription past_due because it no longer matches latest_invoice.
@enginer is attempting to deploy a commit to the Vercel Labs Team on Vercel.
A member of the Team first needs to authorize it.