Worlds / Payments and billing
Stripe API
A local Stripe with customers, charges, refunds, subscriptions, invoices and credit notes. Test the billing agent that cleans up after an outage without refunding the wrong account.
- Base path
/v1- Auth
- Bearer secret key, the world key
- Endpoints
- 33
- Status
- Active today
Connect
The run gives your agent these bindings. Keep your SDK; change only where it sends requests.
| binding | meaning |
|---|---|
WORLDS_BASE_URL | This world's origin |
WORLDS_API_KEY | The world key, accepted wherever Stripe expects a credential |
WORLDS_WORLD_ID | The isolated world's identity |
import Stripe from "stripe";
const url = new URL(process.env.WORLDS_BASE_URL);
const stripe = new Stripe(process.env.WORLDS_API_KEY, {
host: url.hostname,
port: url.port,
protocol: url.protocol.slice(0, -1),
});
const refund = await stripe.refunds.create({
payment_intent: "pi_123",
amount: 1500,
});API reference
Customers
People or companies you bill, with email and balance.
- POST
/v1/customersCreate a customer - GET
/v1/customers/{id}Retrieve a customer - POST
/v1/customers/{id}Update a customer - DELETE
/v1/customers/{id}Delete a customer - GET
/v1/customersList customers, filter by email
Fields · 5
id | string | Customer id, cus_... |
email | string | Billing email |
name | string | Display name |
balance | integer | Credit or debit balance in minor units |
metadata | object | Free-form key value pairs |
Products
Goods or plans you sell.
- POST
/v1/productsCreate a product - GET
/v1/products/{id}Retrieve a product - GET
/v1/productsList products, filter by active
Fields · 4
id | string | Product id, prod_... |
name | string | Product name |
active | boolean | Whether it can be purchased |
description | string | Longer description |
Prices
One-time or recurring amounts attached to a product.
- POST
/v1/pricesCreate a price - GET
/v1/prices/{id}Retrieve a price - GET
/v1/pricesList prices, filter by product
Fields · 5
id | string | Price id, price_... |
product | string | Product id |
unit_amount | integer | Amount in minor units |
currency | string | Three-letter currency code |
recurring | object | Interval settings for subscriptions |
Invoices
Bills with line items that move from draft to paid.
- POST
/v1/invoicesCreate a draft invoice - GET
/v1/invoices/{id}Retrieve an invoice - POST
/v1/invoices/{id}/finalizeFinalize a draft invoice - POST
/v1/invoices/{id}/payPay an open invoice - POST
/v1/invoices/{id}/voidVoid an open invoice - GET
/v1/invoicesList invoices
Fields · 6
id | string | Invoice id, in_... |
customer | string | Customer id |
status | string | draft, open, paid, void or uncollectible |
amount_due | integer | Amount owed in minor units |
number | string | Invoice number |
subscription | string | Subscription id, if any |
PaymentIntents
A single attempt to collect a payment.
- POST
/v1/payment_intentsCreate a PaymentIntent - GET
/v1/payment_intents/{id}Retrieve a PaymentIntent - POST
/v1/payment_intents/{id}/confirmConfirm a PaymentIntent - POST
/v1/payment_intents/{id}/cancelCancel a PaymentIntent - GET
/v1/payment_intentsList PaymentIntents
Fields · 6
id | string | PaymentIntent id, pi_... |
amount | integer | Amount in minor units |
currency | string | Three-letter currency code |
status | string | requires_confirmation, succeeded, canceled and more |
customer | string | Customer id |
last_payment_error | object | Details of the last decline |
Charges
Card charges created by payment and invoice flows.
- GET
/v1/charges/{id}Retrieve a charge - GET
/v1/chargesList charges, filter by customer or invoice - GET
/v1/charges?payment_intent={id}List charges for a PaymentIntent
Fields · 5
id | string | Charge id, ch_... |
amount | integer | Amount in minor units |
amount_refunded | integer | Total refunded so far |
refunded | boolean | True when fully refunded |
payment_intent | string | Originating PaymentIntent id |
Refunds
Full or partial returns of a charge.
- POST
/v1/refundsCreate a refund for a charge or PaymentIntent - GET
/v1/refunds/{id}Retrieve a refund - GET
/v1/refundsList refunds, filter by charge
Fields · 5
id | string | Refund id, re_... |
charge | string | Charge id |
payment_intent | string | PaymentIntent id |
amount | integer | Refunded amount in minor units |
status | string | pending, succeeded or failed |
Subscriptions
Recurring billing for a customer.
- POST
/v1/subscriptionsCreate a subscription - GET
/v1/subscriptions/{id}Retrieve a subscription - POST
/v1/subscriptions/{id}Update price, quantity or cancellation - DELETE
/v1/subscriptions/{id}Cancel a subscription - GET
/v1/subscriptionsList subscriptions
Fields · 5
id | string | Subscription id, sub_... |
customer | string | Customer id |
status | string | active, past_due, canceled and more |
items | object | Prices and quantities |
cancel_at_period_end | boolean | Cancel at the end of the period |
Test workflow
- Prepare. Select the Stripe setup for your workflow and review its starting records, including customers, products, prices.
- Connect. Configure your agent with this world's URL and key using the SDK example. Check that it reaches the intended API before running a test.
- Define. Describe the task, expected final changes and records that must stay untouched. Keep expected outcomes separate from the agent's instructions.
- Run and inspect. Use the dashboard or your workflow's generated CLI setup. Review final records and changes, then rerun from the same starting setup with the relevant failure conditions. Use Deployment and CI to automate the same workflow.
What to verify
- Duplicate charge ch_1b refunded exactly once
- No refund on ch_1 or on same-name account cus_A2
- No more than 2 credit notes across the batch
- sub_dup canceled, sub_orig still active
- No new invoice opened on cus_A by the next billing date
Test file
A task file names the jobs, the policy your agent is given and what the grader reads back from the world's final state. The agent never sees the expected outcomes.
# worlds/tasks.yaml · Stripe
version: 2
connector: stripe
seed: stripe-starter
mode: isolated
policy: >
Refund duplicate charges once and never refund the same charge twice. Issue
at most two goodwill credits per batch. Cancel duplicate plans and keep the
original.
tasks:
- id: refund-a-duplicate-charge-exactly-once
prompt: Refund a duplicate charge exactly once
- id: issue-a-goodwill-credit-note-within-policy
prompt: Issue a goodwill credit note within policy
- id: cancel-a-duplicate-subscription-and-keep-the-original
prompt: Cancel a duplicate subscription and keep the original
expected:
- Duplicate charge ch_1b refunded exactly once
- No refund on ch_1 or on same-name account cus_A2
- No more than 2 credit notes across the batch
- "sub_dup canceled, sub_orig still active"
- No new invoice opened on cus_A by the next billing date
scenarios:
- rate-limited
- card-declined
- webhook-twiceSee Tasks and outcomes for every field, and Deployment and CI to run the file on every pull request.
Failure conditions
Each one fires before the handler, at the same step on every run.
| condition | what the agent sees |
|---|---|
rate-limited | POST /v1/refunds returns 429 on the first try |
card-declined | The retry charge fails with card_declined |
webhook-twice | invoice.paid is delivered two times |
key-expires | The restricted key stops working partway through the run |
idempotency-conflict | A reused key with new params returns 400 |
Starting data
Start each run from the same records and relationships. The overview below describes the Stripe starter; use the setup supplied for your workflow when preparing your own snapshot.
| Records | Starting setup |
|---|---|
| customers | 60, including two accounts named Dana Lee |
| charges | 180 succeeded, 6 duplicated by an outage retry |
| subscriptions | 45 active, 3 duplicates created the same minute |
| invoices | 120 paid, 9 open, 2 past due |
| credit notes | 4 issued this month |
The record shapes below reuse this world's API field definitions. They show illustrative values and placeholders, not an importable seed. API responses and seed input can differ; include all required records and references in the accepted import format for your workflow.
Customers records
Customers
People or companies you bill, with email and balance.
| Field | Type | Meaning / relationship |
|---|---|---|
id | string | Customer id, cus_... |
email | string | Billing email |
name | string | Display name |
balance | integer | Credit or debit balance in minor units |
metadata | object | Free-form key value pairs |
{
"id": "<Customer id, cus_...>",
"email": "person@example.test",
"name": "<Display name>",
"balance": 1,
"metadata": {}
}Products records
Products
Goods or plans you sell.
| Field | Type | Meaning / relationship |
|---|---|---|
id | string | Product id, prod_... |
name | string | Product name |
active | boolean | Whether it can be purchased |
description | string | Longer description |
{
"id": "<Product id, prod_...>",
"name": "<Product name>",
"active": false,
"description": "<Longer description>"
}Prices records
Prices
One-time or recurring amounts attached to a product.
| Field | Type | Meaning / relationship |
|---|---|---|
id | string | Price id, price_... |
product | string | Product id |
unit_amount | integer | Amount in minor units |
currency | string | Three-letter currency code |
recurring | object | Interval settings for subscriptions |
{
"id": "<Price id, price_...>",
"product": "<Product id>",
"unit_amount": 1,
"currency": "<Three-letter currency code>",
"recurring": {}
}Invoices records
Invoices
Bills with line items that move from draft to paid.
| Field | Type | Meaning / relationship |
|---|---|---|
id | string | Invoice id, in_... |
customer | string | Customer id |
status | string | draft, open, paid, void or uncollectible |
amount_due | integer | Amount owed in minor units |
number | string | Invoice number |
subscription | string | Subscription id, if any |
{
"id": "<Invoice id, in_...>",
"customer": "<Customer id>",
"status": "<draft, open, paid, void or uncollectible>",
"amount_due": 1,
"number": "<Invoice number>",
"subscription": "<Subscription id, if any>"
}PaymentIntents records
PaymentIntents
A single attempt to collect a payment.
| Field | Type | Meaning / relationship |
|---|---|---|
id | string | PaymentIntent id, pi_... |
amount | integer | Amount in minor units |
currency | string | Three-letter currency code |
status | string | requires_confirmation, succeeded, canceled and more |
customer | string | Customer id |
last_payment_error | object | Details of the last decline |
{
"id": "<PaymentIntent id, pi_...>",
"amount": 1,
"currency": "<Three-letter currency code>",
"status": "<requires_confirmation, succeeded, canceled and more>",
"customer": "<Customer id>",
"last_payment_error": {}
}Charges records
Charges
Card charges created by payment and invoice flows.
| Field | Type | Meaning / relationship |
|---|---|---|
id | string | Charge id, ch_... |
amount | integer | Amount in minor units |
amount_refunded | integer | Total refunded so far |
refunded | boolean | True when fully refunded |
payment_intent | string | Originating PaymentIntent id |
{
"id": "<Charge id, ch_...>",
"amount": 1,
"amount_refunded": 1,
"refunded": false,
"payment_intent": "<Originating PaymentIntent id>"
}Refunds records
Refunds
Full or partial returns of a charge.
| Field | Type | Meaning / relationship |
|---|---|---|
id | string | Refund id, re_... |
charge | string | Charge id |
payment_intent | string | PaymentIntent id |
amount | integer | Refunded amount in minor units |
status | string | pending, succeeded or failed |
{
"id": "<Refund id, re_...>",
"charge": "<Charge id>",
"payment_intent": "<PaymentIntent id>",
"amount": 1,
"status": "<pending, succeeded or failed>"
}Subscriptions records
Subscriptions
Recurring billing for a customer.
| Field | Type | Meaning / relationship |
|---|---|---|
id | string | Subscription id, sub_... |
customer | string | Customer id |
status | string | active, past_due, canceled and more |
items | object | Prices and quantities |
cancel_at_period_end | boolean | Cancel at the end of the period |
{
"id": "<Subscription id, sub_...>",
"customer": "<Customer id>",
"status": "<active, past_due, canceled and more>",
"items": {},
"cancel_at_period_end": false
}Stripe records
The examples below are seed record fragments. Put them inside a complete seed with matching referenced records before validating or importing it.
Stripe seed records use explicit prefixed IDs: cus_, prod_, price_, in_, ch_ and sub_. References must point to records in the same seed.
Amounts are integer cents. Where supported, created_offset is a nonnegative number of seconds before the epoch; a larger value means an older record.
Stripe record fields and examples
data.customers[]
| Field | Type or default |
|---|---|
id |
Required, unique cus_... |
name |
Required string |
email |
Valid email or null; defaults to null |
description, phone |
String or null; default null |
balance |
Integer cents; default 0 |
metadata |
String-valued object; default {} |
created_offset |
Seconds before the epoch; default 0 |
data.products[] and data.prices[]
| Record | Required fields | Optional fields |
|---|---|---|
| Product | id (prod_...), name |
description, active (default true), metadata |
| Price | id (price_...), product, unit_amount |
currency (default usd), nickname, recurring, active, metadata |
The price's product must exist. unit_amount is nonnegative. recurring defaults to null for a one-time price; subscriptions require a recurring price:
"recurring": { "interval": "month", "interval_count": 1 }
Intervals are day, week, month or year. interval_count is a positive integer, defaulting to 1.
data.invoices[]
{
"id": "in_example",
"customer": "cus_example",
"status": "paid",
"currency": "usd",
"lines": [
{ "description": "Monthly plan", "amount": 4000, "quantity": 1 }
],
"charges": [
{ "id": "ch_example", "amount": 4000 }
]
}
| Field | Rule |
|---|---|
customer |
Required reference to an existing customer |
status |
draft, open, paid, void or uncollectible |
lines |
At least one; amount is the line total, not the unit price |
lines[].quantity |
Positive integer; defaults to 1 |
lines[].price |
Optional reference to a price |
charges |
Allowed only on paid invoices; defaults to empty |
charges[].amount |
Positive cents; defaults to the invoice total |
charges[].amount_refunded |
Prior refunded cents, from 0 up to the charge amount |
currency, metadata, created_offset |
Defaults: usd, {}, 0 |
Non-draft invoices receive a deterministic invoice number and matching status-transition timestamps.
Multiple charges on one invoice can represent a duplicate charge. Prior refunds are represented by amount_refunded; individual historical refund objects are not seeded. A fully refunded charge rejects another refund.
data.subscriptions[]
{
"id": "sub_example",
"customer": "cus_example",
"price": "price_monthly",
"quantity": 1,
"current_period_start": 1766361600,
"current_period_end": 1769040000,
"cancel_at_period_end": false
}
The customer and recurring price must exist. Seeded subscriptions are active; quantity defaults to 1, cancel_at_period_end to false, created_offset to 0 and metadata to {}.
Period timestamps are absolute Unix seconds. By default, the period starts at the created time and ends one price interval later.
Advancing the clock across a period boundary renews and bills the subscription, or applies its scheduled cancellation. Set a period near the seed epoch to test this without a large clock advance.
Seed file structure
This is a valid empty starting world:
{
"name": "my-seed",
"version": 1,
"epoch": 1767225600,
"clock_mode": "manual",
"data": {}
}
| Field | Required | Meaning |
|---|---|---|
name |
Yes | Lowercase letters, digits and hyphens; starts with a letter or digit |
version |
Yes | Integer of at least 1; independent of the tasks file's version |
epoch |
Yes | Nonnegative Unix seconds; the world's starting logical time |
data |
Yes | Stripe and legacy record arrays; omitted arrays default to empty |
zendesk |
No | Native Zendesk profile |
clock_mode |
No | manual (default) or auto |
auto_tick_seconds |
No | Positive integer; defaults to 1 |
With a manual clock, time moves through worlds clock advance or the admin API. Auto mode also advances by auto_tick_seconds on eligible API requests. Authentication, injected failures and preflight refusals do not tick; see logical clock behavior.
Loading a seed emits no events or webhooks. The world records the parsed seed's canonical-JSON SHA-256 as seed_hash; generated record IDs derive from that hash and world counters. See determinism.
Import a Stripe account
Provide a restricted read-only key through STRIPE_API_KEY, then run:
worlds seed import --from-stripe --name my-account --manifest worlds/my-account.manifest.json
The importer reads supported account records and writes a validated seed. By default it samples up to 100 objects per resource and writes seeds/my-account.json, or the directory set by WORLDS_SEEDS_DIR.
| Treatment | Data |
|---|---|
| Preserved | Amounts, currencies, invoice states, charge refund totals, subscription timing, product and price IDs |
| Replaced | Customer names and emails; product names, price nicknames and invoice-line descriptions |
| Remapped | Customer, invoice, charge and subscription IDs |
| Dropped | Customer descriptions and phones; metadata by default |
This is pseudonymization, not anonymity. Amounts and timing can still identify an account. Product and price IDs remain verbatim, including merchant-chosen text. Review the manifest before sharing an imported seed.
The same source state and options produce identical files. The default epoch comes from the newest source creation timestamp, rounded up to UTC midnight.
Stripe import options
| Option | Purpose |
|---|---|
--key <key> |
Alternative to STRIPE_API_KEY |
--out <file> |
Choose the seed destination |
--manifest <file> |
Record fields preserved, replaced or dropped and import losses |
--identity-map <file> |
Write a private original-to-pseudonym map; keep it local |
--limit <n> |
Maximum objects per resource; default 100 |
--tickets <export> |
Also fetch customers named by the export and their billing records |
--base-url <url> |
Stripe-compatible origin; defaults to https://api.stripe.com |
--epoch <unix> |
Override the starting time |
--keep-names |
Preserve product names and price nicknames |
--keep-descriptions |
Preserve invoice-line descriptions |
--keep-metadata |
Preserve customer metadata, for test-mode accounts only |
Customer identities are always replaced. Customers sharing an email retain that relationship through a shared pseudonym. By default, invoice-line descriptions use the price's placeholder nickname, or a numbered line label when no price exists.
Metadata on products, prices, invoices and subscriptions is not copied. The manifest lists preserved custom product/price IDs and samples of free text retained through --keep-* options.
Unsupported records, such as non-active subscriptions or invoices whose customer was not imported, are skipped and reported. Refund objects become charge-level amount_refunded totals. Disputed charges import as ordinary charges and are listed in the manifest because disputes are outside the twin's scope.
Referential integrity
Validation rejects unknown fields, duplicate IDs, missing references and inconsistent history. Errors name the JSON path to fix.
For Stripe, it also rejects charges on unpaid invoices, over-refunded charges, subscriptions on one-time prices and inverted periods.
Validate the complete seed before use:
worlds seed validate my-seed.json
Save a world's current state as a seed
In the dashboard, choose Save as starting setup from an eligible world. From the CLI:
worlds world export w_abc123def456 --name my-fork
The admin equivalent is GET /admin/worlds/:id/export-seed?name=my-fork. Use the result to start another test from the state your agent produced.
The export uses the world's current logical clock as its epoch and validates as a seed. Its lossy report lists state the seed format cannot retain:
| Not retained | What survives |
|---|---|
| Stripe refund objects | Each charge's refunded total |
| PaymentIntents and charges without an invoice | No seed representation |
| Pending invoice items | Items already swept into invoice lines |
| Deleted customers and canceled subscriptions | No seed representation |
| Event log | Current supported records |
Stripe and legacy record offsets remain relative to the new epoch.
Exporting and reloading represents settled state, not a replay of the run. Re-exporting that loaded world preserves the same data block.
Library seeds
| Seed | Use |
|---|---|
saas-billing-small |
Stripe examples, including a duplicate charge |
saas-billing-smoke |
Minimal Stripe fixture for fast tests |
acme-prod |
Scripted support-and-billing demonstration |
acme-prod-shift |
Shared-world workday with follow-ups and a shift budget |
acme-prod-library |
Billing starter cases and the source of billing packs |
Bundled seeds are synthetic fixtures. See the generated seed catalog for their builders and starter packs for ready-made tasks.