Scope: what the Stripe twin implements — and deliberately doesn't

Worlds' rule is depth over breadth, one connector at a time (Stripe is the first twin, and the one this page covers): every behavior on the implemented surface has lifecycle-correct state and a test that fails if it drifts. Anything not listed as implemented returns Stripe's Unrecognized request URL 404 rather than a half-working imitation.

Implemented (/v1/*)

resource operations depth notes
Customers create, retrieve, update, delete, list (email filter) deleted customers stay retrievable as {deleted: true} stubs (HTTP 200, like real Stripe); deletes hide them from lists and block updates; deleting a customer does not cancel its subscriptions (Known gaps)
Products create, retrieve, list (active) minimal fields, enough for prices/invoices
Prices create, retrieve, list (product, active) one-time and recurring; recurring backs subscriptions; unit_amount ≥ 0
Invoice items create, retrieve, list (customer, pending) pending items sweep into the next invoice; quantity ≥ 0
Invoices create, retrieve, delete (drafts — the items it swept are stranded, Known gaps), list (customer, status, subscription), lines, finalize, pay, void, mark_uncollectible full draft → open → paid/void/uncollectible lifecycle with status-transition timestamps, deterministic numbers, guards with Stripe error codes
PaymentIntents create (confirm supported), retrieve, list (customer), confirm, cancel test tokens pm_card_visa and pm_card_chargeDeclined; declines record the failed charge + last_payment_error
Charges retrieve, list (customer, payment_intent, invoice) born only from flows (invoice pay, PI confirm, subscription billing); amount_refunded/refunded maintained
Refunds create (charge or payment_intent), retrieve, list (charge) partial refunds accumulate; over-refund rejected with Stripe's message; full re-refund → charge_already_refunded; a seeded amount_refunded lists one refund for that total, its id derived from the seed hash and the charge id
Subscriptions create, retrieve, update (cancel_at_period_end), cancel (DELETE), list (customer, status, price) creation bills the first period; clock advance bills every elapsed period at its boundary
Events retrieve, list (type) every mutation emits the correct evt_; pending_webhooks reflects the delivery queue

Plus, everywhere: Stripe's exact error envelope (type/code/message/param, correct statuses), cursor pagination (limit, starting_after, ending_before, has_more), idempotency keys (replay + idempotency_error; a request rejected by parameter validation reserves no key, an executed outcome — errors and 500s included — is replayed), Bearer/Basic auth with Stripe-shaped 401s, form-encoded bracket syntax bodies (the real SDKs' wire format), and the Stripe-Should-Retry header on injected errors where Stripe sends it (lock_timeout), which is what the official SDKs' retry logic reads. Official stripe-node 16 (the demo agent) and 22 (the cold agent, stock defaults: two retries, an idempotency key on every POST, API version 2026-08-26.dahlia) run against the twin in the test suite; the twin ignores Stripe-Version and answers in its own pinned shapes.

Deliberately not implemented

Where one of these is reachable by a parameter on an implemented route, the request is refused with worlds_unsupported_parameter (below, "Refused parameters") — never accepted and silently ignored.

Payment plumbing — PaymentMethods as objects (only the two test tokens), SetupIntents, 3DS/requires_action, captures/holds (capture_method: manual), multi-currency conversion, balance/payouts/transfers, Connect, disputes.

Billing breadth — coupons/discounts/promotion codes, tax (rates, automatic tax), credit notes, quotes, usage-based/metered billing, tiered prices, trials, proration, subscription schedules, multi-item subscriptions (one item per subscription), collection_method: send_invoice (auto-charge only), invoice auto_advance timers (finalize/pay are explicit or subscription-driven).

Platform surface — Checkout, Payment Links, Billing Portal, Sources/Tokens (legacy), files, search endpoints (use list filters or the admin state API), expand[] (objects embed what the twin considers primary; expansion params are accepted and ignored).

Field-level omissions on implemented objects (returned as null/absent rather than half-implemented): address/shipping on customers, default_payment_method, application_fee_amount, transfer_data, on_behalf_of, statement_descriptor overrides, receipt_url contents, balance_transaction objects, invoice PDFs/hosted_invoice_url, webhook endpoint objects via /v1/webhook_endpoints (the twin's endpoint is configured via the admin API instead — one per world).

Semantics simplificationslivemode is always false; the account is single-currency, USD (ACCOUNT_DEFAULT_CURRENCY), so where Stripe reads the account's default currency — pinning a customer's currency the moment a non-zero balance denominates it — the twin answers usd; one currency per invoice/subscription (whatever its lines/price use) and one per customer balance (the first balance transaction pins currency, as at Stripe; a transaction in another currency is refused with worlds_unsupported_parameter rather than summed into balance, and the balance settles only invoices in its own currency — a usd credit leaves a eur invoice charged in full, as Stripe's per-currency balances do (drift: credit-other-currency-not-applied); a negative-total invoice in a second currency, whose credit the twin cannot bank beside the pinned balance, is refused with the same code — and so are the requests that would build one, a plan change with prorations and a negative invoice item, each in a second currency on a pinned customer or beside pending credit lines in another currency, and the balance is not pinned — by a transaction, a balance update or a negative total flowing back — while a credit line in another currency is pending, and POST /v1/prices refuses a negative unit_amount as Stripe does (the one other way to a negative renewal line), so no request builds a renewal the twin cannot bill; a renewal that meets the refusal anyway (state written outside the API) is atomic — the clock movement rolls back, a twin request answers a logged api_error 500 and clock/advance a 409 — and in clock_mode: "auto" every twin request then answers that 500 until the world is reset, while in manual mode a positive line in the invoice's currency or cancel_at_period_end=true on the subscription frees the next advance; Stripe's own model, a per-currency invoice_credit_balance, is a follow-up plan); one currency per one-off invoice too: POST /v1/invoices collects the pending items of the currency named by currency, or of the only currency pending, and 400s on currency when more than one is pending and none is named, while a renewal sweeps only the pending items in its subscription's currency; charges cannot be created directly (POST /v1/charges is legacy); no rate limiting unless a scenario injects it.

One rule for credit in two currencies — a customer's pending credit lines are in one currency, the balance's once it is pinned; the twin refuses, at the request, whatever would break that (a second-currency credit line, a pin beside pending credits, a negative price), so a renewal never meets a credit it cannot bank. Stripe's per-currency invoice_credit_balance is the faithful model and a follow-up plan.

If your agent needs something in this list, open an issue — the bar for adding surface is "we will model its state correctly and test its lifecycle", not "we can return a plausible JSON blob".

Refused parameters

Real Stripe accepts every parameter below and changes how money moves; the twin does not model it. Rather than return a normal success for a request whose real outcome would differ — a manual capture that never captures, a coupon that never discounts, a trial that never defers — the twin refuses the request before the handler runs: 400 invalid_request_error with the twin's own code worlds_unsupported_parameter (not a Stripe code, so a test can tell "the twin cannot grade this" from "Stripe rejected this"), the parameter in param, no idempotency result (a corrected retry under the same key runs; a key already holding a stored result keeps replaying it, and a request under that key that carries a refused parameter is refused rather than answered idempotency_error), and a row in the request log whose unsupported column names the parameter — GET /admin/worlds/:id/requests?unmatched=true lists these beside the un-twinned routes, one row per route and parameter. The request has been authenticated and counted by then: it uses one of the key's requests_used and advances the scenario counters it matched, exactly as an injected or unmatched request does; what does not happen is the handler, the auto-clock tick and the idempotency store. The drift battery pins the gap: pi-capture-manual-refused and price-trial-period-days expect real Stripe to accept what the twin refuses.

Values marked allowed are the twin's own behavior and pass through; any other value is refused, including one Stripe would reject too — guessing which unknown values are harmless is how a new Stripe value would slip through. A boolean whose false is Stripe's do-nothing default passes as false. An absent, null or empty value is not "present": coupon="" clears a discount, and there is none to clear. Additive fields Stripe merely echoes (statement_descriptor, receipt_email, expand[], setup_future_usage, automatic_payment_methods) are accepted and ignored as before; values a handler already rejects with its own Stripe-shaped 400 (a second subscription item, an unknown proration_behavior) are not listed twice. The table is a denylist of known spellings, so its limit is inherent: it catches the money-changing parameters it names and nothing else — an unknown parameter, a case variant such as Capture_Method included, is ignored wholesale, as every unknown parameter always has been; and a listed parameter sent in a shape the table does not read passes too, so automatic_tax=false as a flat string (Stripe requires the automatic_tax[enabled] hash and 400s the string) is accepted here, a pre-existing leniency. The same code also marks the state-dependent refusals a handler makes itself — a balance transaction in a second currency on a customer whose balance is already denominated, a plan change whose prorations or an invoice item whose credit would land in a second currency on such a customer or beside pending credit lines in another currency, a balance pinned — by a transaction, a balance update or a negative total flowing back — while such a line is pending, and a negative-total invoice in a second currency finalized on one (above, under semantics simplifications) — and those, unlike the table's, are execution-phase errors: the twin had to read the customer to know, so a keyed request's outcome is stored under its idempotency key exactly as Stripe stores its own answer (the message says so when there is a key; an unkeyed request stores nothing and the message says nothing of it), and a corrected retry under the same key gets idempotency_error, not a second attempt. Their param is the parameter the request sent (currency, price, balance, proration_behavior), and absent on a finalize, which sends none.

The table is REFUSED_PARAMS in packages/server/src/twin/unsupported.ts; a test fails when this page and that table disagree in either direction.

route parameter allowed why
POST /v1/payment_intents capture_method automatic, automatic_async a manual capture holds funds without moving them; the twin captures on confirm
POST /v1/payment_intents payment_method_options[card][capture_method] the card-level capture_method overrides the top-level one: manual holds the funds instead of capturing them
POST /v1/payment_intents application_fee_amount a Connect fee splits the charge
POST /v1/payment_intents transfer_data a Connect transfer moves money to another account
POST /v1/payment_intents on_behalf_of Connect settlement on another account
POST /v1/payment_intents/:id/confirm capture_method automatic, automatic_async a manual capture holds funds without moving them; the twin captures on confirm
POST /v1/payment_intents/:id/confirm payment_method_options[card][capture_method] the card-level capture_method overrides the top-level one: manual holds the funds instead of capturing them
POST /v1/subscriptions trial_period_days a trial defers the first charge
POST /v1/subscriptions trial_end a trial defers the first charge
POST /v1/subscriptions trial_from_plan false takes the trial from the price's recurring[trial_period_days]; a trial defers the first charge
POST /v1/subscriptions coupon a discount changes every invoice total
POST /v1/subscriptions discounts a discount changes every invoice total
POST /v1/subscriptions promotion_code a discount changes every invoice total
POST /v1/subscriptions default_tax_rates tax changes invoice totals
POST /v1/subscriptions automatic_tax[enabled] false tax changes invoice totals
POST /v1/subscriptions billing_cycle_anchor moves the first period boundary and prorates the first invoice
POST /v1/subscriptions billing_cycle_anchor_config moves the first period boundary and prorates the first invoice
POST /v1/subscriptions backdate_start_date bills past periods at creation
POST /v1/subscriptions collection_method charge_automatically send_invoice leaves the invoice open instead of charging it
POST /v1/subscriptions days_until_due only meaningful with send_invoice
POST /v1/subscriptions payment_behavior allow_incomplete, error_if_incomplete default_incomplete and pending_if_incomplete leave the invoice unpaid; the twin charges at once
POST /v1/subscriptions cancel_at the twin models cancel_at_period_end only
POST /v1/subscriptions add_invoice_items one-time lines change the first invoice total
POST /v1/subscriptions items[0][price_data] an inline price the twin cannot read; create the price, then pass items[0][price]
POST /v1/subscriptions items[0][tax_rates] tax changes the line total
POST /v1/subscriptions items[0][discounts] a discount changes the line total
POST /v1/subscriptions application_fee_percent a Connect fee splits every invoice's charge
POST /v1/subscriptions transfer_data a Connect transfer moves every invoice's charge to another account
POST /v1/subscriptions on_behalf_of Connect settlement on another account
POST /v1/subscriptions billing_thresholds bills when the amount crosses a threshold, not at the period boundary
POST /v1/subscriptions/:id trial_end a trial defers the next charge
POST /v1/subscriptions/:id coupon a discount changes every invoice total
POST /v1/subscriptions/:id discounts a discount changes every invoice total
POST /v1/subscriptions/:id promotion_code a discount changes every invoice total
POST /v1/subscriptions/:id default_tax_rates tax changes invoice totals
POST /v1/subscriptions/:id automatic_tax[enabled] false tax changes invoice totals
POST /v1/subscriptions/:id billing_cycle_anchor unchanged resetting the anchor prorates and moves every renewal
POST /v1/subscriptions/:id proration_date changes the proration amounts
POST /v1/subscriptions/:id cancel_at the twin models cancel_at_period_end only
POST /v1/subscriptions/:id pause_collection pauses charging while the subscription stays active
POST /v1/subscriptions/:id collection_method charge_automatically send_invoice leaves the invoice open instead of charging it
POST /v1/subscriptions/:id days_until_due only meaningful with send_invoice
POST /v1/subscriptions/:id payment_behavior allow_incomplete, error_if_incomplete default_incomplete and pending_if_incomplete leave the invoice unpaid; the twin charges at once
POST /v1/subscriptions/:id add_invoice_items one-time lines change the next invoice total
POST /v1/subscriptions/:id items[0][price_data] an inline price the twin cannot read; create the price, then pass items[0][price]
POST /v1/subscriptions/:id items[0][tax_rates] tax changes the line total
POST /v1/subscriptions/:id items[0][discounts] a discount changes the line total
POST /v1/subscriptions/:id items[0][deleted] false deletes the one item — Stripe 400s unless another item replaces it, and the twin models exactly one
POST /v1/subscriptions/:id application_fee_percent a Connect fee splits every invoice's charge
POST /v1/subscriptions/:id transfer_data a Connect transfer moves every invoice's charge to another account
POST /v1/subscriptions/:id on_behalf_of Connect settlement on another account
POST /v1/subscriptions/:id billing_thresholds bills when the amount crosses a threshold, not at the period boundary
DELETE /v1/subscriptions/:id invoice_now false invoices pending prorations at once
DELETE /v1/subscriptions/:id prorate false credits the unused time on cancel
POST /v1/invoices collection_method charge_automatically send_invoice leaves the invoice open instead of charging it
POST /v1/invoices days_until_due only meaningful with send_invoice
POST /v1/invoices due_date only meaningful with send_invoice
POST /v1/invoices auto_advance false Stripe finalizes and collects on its own about an hour later; the twin finalizes and pays only when told to
POST /v1/invoices discounts a discount changes the invoice total
POST /v1/invoices default_tax_rates tax changes invoice totals
POST /v1/invoices automatic_tax[enabled] false tax changes invoice totals
POST /v1/invoices pending_invoice_items_behavior include exclude leaves the pending items off the invoice; the twin always sweeps them
POST /v1/invoices subscription includes only that subscription's pending items; the twin sweeps every pending item of the customer
POST /v1/invoices application_fee_amount a Connect fee splits the charge
POST /v1/invoices transfer_data a Connect transfer moves money to another account
POST /v1/invoices on_behalf_of Connect settlement on another account
POST /v1/invoiceitems price_data an inline price the twin cannot read; pass amount + currency, or price
POST /v1/invoiceitems unit_amount the twin reads amount (a total), or price × quantity
POST /v1/invoiceitems unit_amount_decimal the twin reads amount (a total), or price × quantity
POST /v1/invoiceitems discounts a discount changes the line total
POST /v1/invoiceitems tax_rates tax changes the line total
POST /v1/invoiceitems invoice adds the line to a specific draft invoice; the twin sweeps pending items only
POST /v1/invoices/:id/pay paid_out_of_band false marks the invoice paid with no charge
POST /v1/invoices/:id/pay forgive false writes off the remaining amount
POST /v1/refunds refund_application_fee false a Connect fee refund
POST /v1/refunds reverse_transfer false a Connect transfer reversal
POST /v1/customers coupon a discount changes every future invoice
POST /v1/customers promotion_code a discount changes every future invoice
POST /v1/customers cash_balance a second balance the twin does not model
POST /v1/customers/:id coupon a discount changes every future invoice
POST /v1/customers/:id promotion_code a discount changes every future invoice
POST /v1/customers/:id cash_balance a second balance the twin does not model
POST /v1/prices billing_scheme per_unit tiered pricing changes the amount per quantity
POST /v1/prices tiers tiered pricing changes the amount per quantity
POST /v1/prices tiers_mode tiered pricing changes the amount per quantity
POST /v1/prices transform_quantity changes the billed quantity
POST /v1/prices recurring[usage_type] licensed metered billing bills reported usage, not the quantity
POST /v1/prices recurring[aggregate_usage] metered billing; a Plans-era parameter Stripe's current API no longer accepts on a price, refused rather than echoed
POST /v1/prices recurring[meter] metered billing
POST /v1/prices recurring[trial_period_days] a trial every subscription created with trial_from_plan takes; a trial defers the first charge
POST /v1/prices unit_amount_decimal the twin reads unit_amount
POST /v1/prices custom_unit_amount the customer chooses the amount
POST /v1/prices currency_options multi-currency prices
POST /v1/prices product_data an inline product the twin cannot create; create the product, then pass product

Known gaps

Behavior on the implemented surface that is known to differ from Stripe and is written down here rather than fixed yet; each row names its follow-up.

gap today Stripe follow-up
DELETE /v1/customers/:id leaves the customer's subscriptions active the stub is deleted: true; the subscriptions keep renewing on every clock advance, billing a deleted customer "Also immediately cancels any active subscriptions on the customer" a drift probe (customer-delete-cancels-subscriptions) and the fix, under C2 (whatever live drift flags)
every recurring price echoes recurring.aggregate_usage: null priceShape in packages/server/src/twin/build.ts still emits the field, though POST /v1/prices refuses the parameter the current API neither accepts nor returns it dropped when the price shape is next re-pinned against the drift battery
deleting a draft invoice strands the items it swept the items keep invoice: <the deleted id> — not pending (?pending=true misses them), on no invoice; the next POST /v1/invoices answers Nothing to invoice unverified: returned to pending, or deleted with the draft a drift probe decides; until then, void instead of delete, or create the items after deleting the draft

The tickets surface (/api/v2/*) — demo-grade, by design

Worlds also expose a Zendesk-shaped support-desk surface so composite ticket+billing agent runs can be graded by one diff: GET/POST /api/v2/tickets.json, GET/PUT /api/v2/tickets/{id}.json, GET /api/v2/tickets/{id}/comments.json, with Zendesk's envelopes, ISO timestamps (from the logical clock), status/priority lifecycles (closed tickets are immutable), and Zendesk-shaped errors.

This surface is demo-grade: faithful enough for realistic agent traffic, deterministic like everything else, but — unlike the Stripe surface — not drift-verified against the real Zendesk API, and deliberately simplified (numeric-string ids, comments stored on the ticket, no users/orgs/macros/search, no page-based pagination). It graduates to verified status only when a design partner needs it to; until then, treat "the twin behaves exactly like Zendesk" as out of scope.