Stripe
How Offers become Stripe objects, which side owns each money record, and how Stripe's webhooks reach your code as Events.
Each Startup sells through its own Stripe connected account. Your code never calls Stripe: Offers compile to Stripe objects, Verbs move the money, and Stripe's webhooks arrive as Events.
What an Offer compiles to
leaseline.app sells one plan at $29 a month:
---
price: { amount: 29, currency: USD }
interval: month
---On merge, the SalesBuilder writes a Stripe product for the Offer and a Stripe price for its Price. The first checkout writes a Stripe customer for the Customer. The first charge opens the connected account and hands onboarding to the owners as a Task.
A new Price mints a new Stripe price. Each Subscription on the old one keeps it until it switches. 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:
Refund $29 on order_31. The renewal notice went out late.Every mapped Noun
Each Noun Stripe holds, the Stripe object it maps to, and which side is the source of truth. Where 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.
| Noun | Stripe object | When | Source of truth |
|---|---|---|---|
| Account | Treasury FinancialAccount (treasury.financial_account) | heldAt is treasury | Stripe |
| Card | Issuing Card (issuing.card) | Always | db.sb |
| Customer | Customer (customer) | Always | db.sb |
| Dispute | Dispute (dispute) | kind is chargeback | Stripe |
| Invoice | Invoice (invoice) | Always | db.sb |
| Offer | Product (product) | Always | db.sb |
| Price | Price (price) | Always | db.sb |
| Subscription | Subscription (subscription) | Always | db.sb |
| Transaction | PaymentIntent (payment_intent) | kind is charge | Stripe |
| Transaction | Payout (payout) | kind is payout | Stripe |
| 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:
npx startup-builder listen Order.paid --forward-to http: //localhost: 3000/ hooks
npx startup-builder trigger Order.paidWebhooks 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 lists every magic value.
Live charges start once the owners finish onboarding. Stripe's own guides cover the rail underneath: connected accounts, billing and testing.