# Stripe

URL: /code/platform/stripe · Id: `page:code/platform/stripe`

> How Offers become Stripe objects, which side owns each money record, and how Stripe's webhooks reach your code as Events.

Each [Startup](/business/nouns/startup) sells through its own Stripe connected account. Your code never calls Stripe: [Offers](/business/nouns/offer) compile to Stripe objects, [Verbs](/business/nouns/verb) move the money, and Stripe's webhooks arrive as [Events](/business/nouns/event).

## What an Offer compiles to
`leaseline.app` sells one plan at $29 a month:

```mdx title="offers/pro.mdx"
---
price: { amount: 29, currency: USD }
interval: month
---
```

On merge, the [SalesBuilder](/code/builders/sales) writes a Stripe product for the Offer and a Stripe price for its [Price](/business/nouns/price). The first checkout writes a Stripe customer for the [Customer](/business/nouns/customer). The first charge opens the connected account and hands onboarding to the owners as a [Task](/business/nouns/task).

A new Price mints a new Stripe price. Each [Subscription](/business/nouns/subscription) on the old one keeps it until it switches. [Merchant account](/systems/finance/merchant-account) covers onboarding and who carries the losses.

A refund is one Verb. The platform turns it into a Stripe refund on the original charge:

**Ask**

```text
Refund $29 on order_31. The renewal notice went out late.
```

**API**

```bash
curl -X POST https://api.sb/leaseline.app/orders/order_31/refund \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount":{"amount":29,"currency":"USD"},"reason":"The renewal notice went out late."}'
```

**SDK**

```ts
import { $ } from 'startup-builder'

await $.Order.refund({ order: 'order_31', amount: { amount: 29, currency: 'USD' }, reason: 'The renewal notice went out late.' })
```

**CLI**

```bash
npx startup-builder do Order.refund --json '{"order":"order_31","amount":{"amount":29,"currency":"USD"},"reason":"The renewal notice went out late."}'
```

**MCP**

```json
{
  "name": "do",
  "arguments": {
    "code": "return await $.Order.refund({ order: 'order_31', amount: { amount: 29, currency: 'USD' }, reason: 'The renewal notice went out late.' })"
  }
}
```

## Every mapped Noun
Each [Noun](/business/nouns/noun) Stripe holds, the Stripe object it maps to, and which side is the source of truth. Where [db.sb](/code/platform/db-sb) leads, the platform writes Stripe. Where Stripe leads, db.sb mirrors it, written only from Stripe's webhooks, and the record reconciles to the [Ledger](/systems/ledger).

| Noun | Stripe object | When | Source of truth |
| --- | --- | --- | --- |
| [Account](/business/nouns/account) | **Treasury FinancialAccount** (`treasury.financial_account`) | `heldAt` is `treasury` | Stripe |
| [Card](/business/nouns/card) | **Issuing Card** (`issuing.card`) | Always | db.sb |
| [Customer](/business/nouns/customer) | **Customer** (`customer`) | Always | db.sb |
| [Dispute](/business/nouns/dispute) | **Dispute** (`dispute`) | `kind` is `chargeback` | Stripe |
| [Invoice](/business/nouns/invoice) | **Invoice** (`invoice`) | Always | db.sb |
| [Offer](/business/nouns/offer) | **Product** (`product`) | Always | db.sb |
| [Price](/business/nouns/price) | **Price** (`price`) | Always | db.sb |
| [Subscription](/business/nouns/subscription) | **Subscription** (`subscription`) | Always | db.sb |
| [Transaction](/business/nouns/transaction) | **PaymentIntent** (`payment_intent`) | `kind` is `charge` | Stripe |
| [Transaction](/business/nouns/transaction) | **Payout** (`payout`) | `kind` is `payout` | Stripe |
| [Transaction](/business/nouns/transaction) | **Refund** (`refund`) | `kind` is `refund` | Stripe |

Each Noun page lists its field-by-field mapping under Stripe.

## Webhooks arrive as Events
The platform receives every Stripe webhook for every connected account, verifies its signature, updates the mirrored record and emits one Event: `Order.paid`, `Subscription.renewed`, `Order.refunded`. Your code subscribes to those Events, never to Stripe, so a retried or reordered webhook never reaches it twice.

To test a receiver locally, stream the Event to it, then fire one in test:

```bash
npx startup-builder listen Order.paid --forward-to http://localhost:3000/hooks
npx startup-builder trigger Order.paid
```

[Webhooks](/code/api/guides/webhooks) covers signing, retries and replay.

## Names and casing
The API, the SDK and the records are camelCase, and Stripe's snake_case names appear only in the mapping tables. Pass `billingEmail`, never `email`, and `taxIds`, never `tax_ids`. A Stripe field with no Noun property stays on Stripe's side, and no Verb accepts it.

## Test and live
The test environment runs on Stripe's test keys, so test cards, test clocks and declines behave as Stripe documents them. `4242 4242 4242 4242` succeeds, `4000 0000 0000 0002` declines, and [errors and testing](/code/sdk/guides/errors-and-testing) lists every magic value.

Live charges start once the owners finish onboarding. Stripe's own guides cover the rail underneath: [connected accounts](https://docs.stripe.com/connect), [billing](https://docs.stripe.com/billing) and [testing](https://docs.stripe.com/testing).