# Checkout and orders

Let people in your app buy what your sellers list: one button to Stripe's checkout, stock held while they pay, and every order for you and the buyer.

A buyer pays for one of your sellers' [items](/billing/items) on Stripe's
hosted checkout. Each purchase is an **order**: what was bought, at what
price, from whom, and how the money splits between your fee and the seller.
You see every order in the console; your server reads them and gets a
webhook for each.

> **Note:**

**Arrives with selling.** Checkout arrives with [selling](/billing/selling)
and runs on the same Stripe keys: test keys in Staging for test sales, live keys
in Production once [live selling](/billing/selling#test-in-staging-then-go-live) is open for your app. Fixed, free and pay-what-you-want prices
can be bought once, and recurring prices as [subscriptions](#subscriptions)
with tiers, an introductory price or a set number of payments. A one-time
price can also be [paid in parts](#paying-in-parts): a deposit, milestones or
installments. A buyer gets messages, links, access to something in your app, a
seller's manual delivery, files to download and licence keys; bundles come
later.

**Requires `@buildbase/sdk` 1.2.0 or later.** `<BuyButton>`, `useCheckout`, `useMyOrders`, `useMyOrder`, `useHoldings`, `useHasAccess`, `useGuestCheckout`, `useOrderByToken`, `<SellerSales>`, `useSales`, `useRefundSale`, `useFulfilSale`, `<OrderParts>`, `useOrderPartActions`, `useChargeSalePart`, `useSalesExport`, `<SellerFiles>`, `<HoldingFiles>`, `useSellerFiles`, `useReleases`, `useDownloadFile`, the server's `commerce` actions, the [commerce tools for an app's agent](#agents) and the `order.*` and `grant.*` events in its catalog arrive with it; earlier versions have none of them.

## Walkthrough

New to selling? [Zero to first sale: marketplace](/billing/first-sale-marketplace) and [Zero to first sale: storefront](/billing/first-sale-storefront) walk the whole flow once, from Stripe keys to a refund and going live.

### When to use it

This page covers everything after a buyer presses buy: one-time
checkout, [subscriptions](#subscriptions), [paying in parts](#paying-in-parts),
[carts across sellers](#carts-across-sellers), [refunds](#refunds),
[fulfilment](#fulfilment), [files](#files), [licence keys](#licence-keys)
and [payouts](#payouts). Exports are in
[selling](/billing/selling#exporting-sales).

### Before you start

- Selling on, a seller who can sell, and a published item.
- Your **Order page** under **Selling**, **Settings**, "Your terms and
  policies", so buyers get a receipt.
- To sell to people without an account, "Who can buy" under **Selling**,
  **Settings** ([guests](#guests)).
- For subscriptions in the storefront shape, the Connect endpoint set up
  with **Set it up for me**, so renewals reach your app.

### Set it up in the console

Nothing to switch on: checkout works once an item is published. You work
orders under **Billing**, **Selling**: **Orders** (paid, partly paid,
awaiting shipment, and **Export CSV**) and **Subscriptions**. Each order's
page shows the split, what the buyer got, refunds, disputes, parts and
shipments ([in the console](#in-the-console)). Cart rules (subscriptions
per cart, how one leaves a shared subscription) are under **Settings**,
**Carts**.

### Marketplace and storefront

|                       | Marketplace                                  | Storefront                                                          |
| --------------------- | -------------------------------------------- | ------------------------------------------------------------------- |
| Stripe's page         | On your account                              | On the seller's account                                             |
| The money             | The seller's share is transferred to them    | Your fee is sent to you                                             |
| Refunds               | You only (`REFUND_NOT_ALLOWED` for a seller) | The seller, or you                                                  |
| Disputes              | Answered in your Stripe Dashboard            | Answered in the seller's                                            |
| A subscription        | Lives on your account                        | Lives on the seller's; its events come through the Connect endpoint |
| A cart across sellers | One page for every seller                    | One page per seller, [in steps](#storefront-carts-in-steps)         |
| Stripe Tax, when on   | Worked out on your account                   | Worked out on each seller's account                                 |

![Stripe's hosted checkout in test mode for Coaching session at $45.00, paid to the app's Stripe account](/images/billing/selling/stripe-checkout-marketplace.webp)

![Stripe's hosted checkout in test mode for Coaching session at $45.00, on the seller's own Stripe account](/images/billing/selling/stripe-checkout-storefront.webp)

### What the seller sees

`<SellerSales>`: each sale, what they receive, refunds so far, **Mark as
sent** on a line they deliver, the payments of a subscription and the
parts of an order paid in parts, and (storefront) **Refund**.
`<SellerSubscriptions>` lists the subscriptions their items started.

![SellerSales on one sale in the storefront shape: what the seller receives, Mark as sent on the line, and Refund](/images/billing/selling/sdk-seller-sales.webp)

### What the buyer sees

Your buy button, Stripe's page, then your order page: the order, what they
got (`useHoldings`), their subscription (`<HoldingSubscription>`), their
parts (`<OrderParts>`) and their files and keys. A guest gets the same on
the page their receipt links to.

![The buyer's order page at 360 pixels wide: the order paid, from the seller, and the seller's message marked available](/images/billing/selling/sdk-order-page.webp)

### Emails and webhooks

The buyer gets **Order Receipt** when an order is paid or claimed free (with
your order page set); shipping adds **Order Shipped** and **Order
Delivered** ([shipping](/billing/shipping)), and an order refused because
the item is not sold in the buyer's country sends **Order Refused For
Country** ([item types](/billing/item-types#where-buyers-can-be)). Your server gets the `order.*`,
`grant.*`, `item_subscription.*` and `payout.*` events in
[Webhooks](#webhooks) and [Subscriptions](#subscriptions).

![The Order Receipt email: the app's name, Coaching session at $45.00, the seller, and a button to the order page](/images/billing/selling/email-order-receipt.webp)

### Limits

| What                     | Limit                                                                            |
| ------------------------ | -------------------------------------------------------------------------------- |
| Stock held while paying  | 30 minutes                                                                       |
| Open checkouts per buyer | 5 (`TOO_MANY_OPEN_CHECKOUTS`), exact: of checkouts started at once, five succeed |
| Lines in a cart          | 10, in one currency                                                              |
| Subscriptions in a cart  | 3 unless you change it, 1 to 10                                                  |
| Milestones on a price    | 2 to 6                                                                           |
| A refused part's grace   | 7 days                                                                           |
| Files in a release       | 20                                                                               |
| A sales export           | 366 days and 10,000 orders                                                       |

### Staging and Production

Make the first sale in Staging with a Stripe test card and refund it; the
go-live list there counts both. Test orders do not carry over to live mode
([go live](/billing/selling#test-in-staging-then-go-live)).

## A buy button

Each app designs its own catalog and item pages; put the button on yours.
The buyer must be signed in.

```tsx
import { BuyButton, usePublishedItems } from '@buildbase/sdk/react';

export function Shop() {
  const { items } = usePublishedItems({ typeKey: 'class' });
  return items.map((item) => {
    const variant = item.variants[0];
    const price = variant.prices[0];
    return (
      <div key={item._id}>
        {item.title}
        <BuyButton
          itemId={item._id}
          variantId={variant.id}
          priceId={price.id}
        />
      </div>
    );
  });
}
```

The button starts checkout and sends the buyer to Stripe's page. When they
have paid, Stripe sends them back to the same page with `?checkout=success`,
or to `successUrl` if you pass one. Pass `quantity` for more than one, and
`amount` (in minor units) for a pay-what-you-want price. With `free`, the
button says "Get it free" and the order completes at once, with no payment
page.

Show the order where the buyer lands. `useMyOrder` reads an open order again
every two seconds, for up to a minute, until Stripe's payment reaches your
app:

```tsx
import { useMyOrder, useMyOrders } from '@buildbase/sdk/react';

const { orders } = useMyOrders();
const { order } = useMyOrder(orders[0]?._id, { waitForPayment: true });
// order.paymentStatus: 'unpaid' until the payment lands, then 'paid'
```

To build the button yourself, `useCheckout()` returns `checkout(itemId,
input)`, which answers Stripe's page (`url`) and the order. Send the buyer
to `url`; a free claim answers `url: null`.

## What the buyer gets

Each benefit of what they bought becomes a **grant**: one per benefit of the
order line, holding as many units as they bought (two seats of a class
whose option grants two each hold four). Grants are written when the order
is paid, before `order.paid` reaches your server.

| Benefit        | What the buyer sees                                          |
| -------------- | ------------------------------------------------------------ |
| `message`      | The seller's text                                            |
| `link`         | The URL, only after paying                                   |
| `access`       | Nothing to show: your app opens the object the benefit names |
| `manual`       | The seller's instructions, while the seller delivers by hand |
| `webhook_only` | Nothing from BuildBase: your server acts on `grant.created`  |

A grant opens at payment, or later by the benefit's drip (`afterDays`,
`availableAt`) or the option's pre-order date (`releasesAt`). A `link` or
`access` with `durationDays` ends that many days after it opens. A message
or link is read from the item as it is now, so a seller who fixes a link
fixes it for every buyer.

```tsx
import { useHasAccess, useHoldings } from '@buildbase/sdk/react';

const { holdings } = useHoldings({ orderId });
// holdings[0]: { kind: 'link', title, state: 'available', content: { url } }

const { hasAccess } = useHasAccess('course', course.id);
```

`state` is `available`, `not_yet` (before it opens), `expired` or `revoked`,
and `content` is there only while it is `available`. `useHasAccess` is for
showing or hiding something in the page; before serving the object itself,
your server asks the same with its API key:

```ts
const res = await fetch(
  `${serverUrl}/api/v1/server/commerce/grants/access?userId=${userId}&resourceType=course&resourceId=${courseId}`,
  { headers: { Authorization: `Bearer ${orgId}:${apiKey}` } }
);
const { hasAccess, until } = await res.json();
```

Licence keys, bundles and transfers are not delivered yet: checkout
refuses an option that includes one (`CHECKOUT_REFUSED`, problem
`benefit_not_delivered`).

## Guests

People can buy without an account when you let them: **Billing > Selling >
Settings**, "Who can buy", signed-in people and guests by email. Until you
do, a guest's checkout is refused (`GUESTS_NOT_ALLOWED`).

A guest gives a name and an email. Pass them to the button; the page that
asks for them is yours.

```tsx
<BuyButton
  itemId={item._id}
  variantId={variant.id}
  priceId={price.id}
  guest={{ name, email }}
/>
```

Stripe returns the guest to your page with `order` and `token` added to
the URL. The token opens that one order, without signing in:

```tsx
import { orderLinkFromUrl, useOrderByToken } from '@buildbase/sdk/react';

const { order, holdings } = useOrderByToken(orderLinkFromUrl(), {
  waitForPayment: true,
});
```

Signed out, `usePublishedItems` reads your catalog without a session, so the
same shop page works for both.

When a guest later signs in with the same email, verified, their orders and
what they got become theirs: they show in `useMyOrders` and `useHoldings`
from then on. The receipt's link keeps working. When the email already
belongs to a verified person (not an agent account) at the moment the
purchase completes, it becomes theirs then, with no read of theirs needed;
a purchase made before they verified is picked up by their next read.

Guest checkouts are rate limited per address and per app, like every
request made without signing in (`GUEST_CHECKOUT_LIMITED`).

### The receipt

Set your app's order page under **Billing > Selling > Settings**, "Your terms and policies",
"Order page". Every buyer, guests included, then gets a receipt from your
app's email sender when their order is paid or claimed free, linking to
that page with `?order=` and `?token=`. Without an order page, no receipt is
sent. Turn it off under **Notifications**, "Order Receipt", to send your own
on `order.paid` instead.

## Deleting a buyer

Orders, payments, subscriptions, bookings, returns and payouts are financial
records, so deleting a person keeps them and removes what identifies them.
Deleting a user (from **Users**, or by the person deleting their own account
in your app) clears their name and email on every order, subscription,
booking and return, the street, city and postcode of their delivery address
(the country and state stay, for tax), their return reasons, the seller's
fulfilment notes to them, the links to a Checkout page or a subscription's
hosted invoice (each shows their details), the machine names their licence
keys were activated on, and the label files of shipments already delivered
or past the longest return window. Their guest purchases go with them when
their email was verified. A label provider's tracking event keeps its body
only until it is applied.

A guest who never had an account is erased by email from the console:
`POST /api/users/guest-erasure` with `{ "email": "..." }`. Their records stay
under one random id in place of the email.

A deleted person who sold stops selling: their seller is closed and their
live items archived, while what they are owed is still paid out. A deleted
affiliate earns nothing new; commissions already earned are still paid to
the payout account on file, which is then cleared. Your server hears both as
`seller.status_changed` and `affiliate.status_changed`, with the reason
`account_deleted`.

A label kept because its shipment was still on its way when the buyer was
deleted goes by itself once the shipment is delivered or past the longest
return window, with the daily shipping check.

Keep personal data out of an order's `metadata`: it is your app's own
reference, and deleting a person does not change it.

## Stock while they pay

Starting checkout holds the units the buyer asked for, for 30 minutes,
Stripe's shortest checkout. If they pay, the units are sold. If they leave,
the hold goes back once its time runs out. A one-of-a-kind item is held for
one buyer at a time; the next sees "Someone else is buying this right now."
One buyer can hold at most five checkouts open.

## What stops a checkout

| Code                      | Status | When                                                                                    | What to do                                                                         |
| ------------------------- | ------ | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `CHECKOUT_REFUSED`        | 422    | Not for sale, a retired price, a quantity or amount out of range; `problems` says which | Fix what `problems` names; a retired price needs the seller to add a live one      |
| `OUT_OF_STOCK`            | 409    | Not enough left                                                                         | Buy fewer, or wait for the seller to restock                                       |
| `BUY_OWN_ITEM`            | 409    | The buyer is the seller                                                                 | Buy as another person; a seller cannot buy their own item                          |
| `SELLER_CANNOT_SELL`      | 409    | The seller cannot take sales now (paused, or Stripe needs more)                         | The seller finishes what `<SellerHome>` asks, or you resume them under **Sellers** |
| `SELLING_UNAVAILABLE`     | 503    | Selling is turned off for the app, or not open yet                                      | Turn selling on under **Selling**; orders already paid are not affected            |
| `TOO_MANY_OPEN_CHECKOUTS` | 429    | The buyer has five checkouts open                                                       | Finish or leave an open checkout; a hold ends after 30 minutes                     |
| `REFUND_REFUSED`          | 422    | More than is left, an unknown line, or an order not paid; `problems` says which         | Refund at most what is left, on a paid order, naming a real line                   |
| `REFUND_NOT_ALLOWED`      | 403    | A seller refunding in the marketplace shape                                             | You refund from the console or your server                                         |
| `REFUND_FAILED`           | 502    | Stripe made no refund; nothing was returned                                             | Read the reason in Stripe and try again; nothing was counted                       |
| `FULFILMENT_REFUSED`      | 409    | Nothing on the line waits on the seller, or it is already marked sent                   | Nothing to do: the line needs no sending, or was already marked                    |

`<BuyButton>` shows each of these under the button, in the reader's
language.

## Where the money goes

Your fee is your selling fee rule, taken on each order. In the
storefront shape, Stripe charges on the seller's account and sends your fee
to you. In the marketplace shape, Stripe charges on your account and sends
the seller's share on. Stripe's own processing fee comes out after the
charge.

## Refunds

Who may refund follows who took the money:

![The refund dialog on an order: the Coaching session line, $45.00 of $45.00 left, a reason, and Refund $45.00](/images/billing/selling/console-order-refund.webp)

| Shape       | Who refunds                                                 |
| ----------- | ----------------------------------------------------------- |
| Storefront  | The seller in your app, or you (the console or your server) |
| Marketplace | You only: the seller is refused with `REFUND_NOT_ALLOWED`   |

Refund any part of what is left, line by line, with a reason. The money
goes back on how the buyer paid. Your fee on the refunded part goes back
too, unless you chose to keep it under **Billing > Selling > Settings**, "When a sale
is refunded":

- **Storefront:** the seller's Stripe balance pays the refund, and your fee
  on it is returned to them.
- **Marketplace:** your balance pays it, and the seller's share of it is
  taken back from them. Keeping your fee, the seller gives back all of it.

A line's money comes back goods first, then its shipping. A part refund,
or a [return](/billing/shipping), is of the goods: your fee and the
seller's share come back as they were on the goods, and the seller keeps
the shipping until the rest of the line is refunded. A line refunded in
full, in one refund or several, returns exactly each part.

A line refunded in full loses what it granted when its item type says
"After a refund: take it back"; a partial refund keeps it. A refund Stripe
refuses returns nothing and is not counted (`REFUND_FAILED`). A refund made
in Stripe's own dashboard is recorded on the order once, by Stripe.

A subscription's renewal, or a move up a tier, is a payment of its own. A
read of one order lists them all in `payments` (the first, then each
`renewal` and `change`, with what was refunded of each), and a refund names
one with `paymentId`: `{ paymentId, amount }`, or all that is left of it
without `amount`. It returns that payment's charge, your fee or the
seller's share on it in proportion, and leaves the order `paid` and the
subscription going; end it separately if you mean to. The refund carries
the same `paymentId` on the order and in `order.refunded`.

```tsx
import { SellerSales } from '@buildbase/sdk/react';

<SellerSales subject={{ user: true }} />;
```

`<SellerSales>` is a seller's sales inside your app: each order, what they
receive, refunds so far, and on a sale's page a refund (storefront) and
Mark as sent. A subscription's sale also lists its payments, each renewal
with its own refund (storefront). `useSales`, `useRefundSale` and `useFulfilSale` build the same
on your own screens.

## Disputes

A buyer who disputes the charge with their bank shows on the order as
disputed, with the reason, until the bank decides. Answer it in Stripe's
dashboard: yours in the marketplace shape, the seller's in a storefront. A
lost dispute takes access back as a full refund does.

Who bears a lost dispute is yours to set under **Billing > Selling >
Settings > Buyers, payouts and records**:

- **A lost dispute on a marketplace sale** (`policies.disputeLoss`). Stripe
  takes it, and its dispute fee, from your balance, while the seller kept
  their transfer. **Charge the seller** (`charge_seller`, the default)
  reverses the seller's transfer for the sale by the disputed amount, up to
  what they were sent, once. Stripe's dispute fee stays yours: a reversal
  cannot take back more than the transfer, and a full dispute already takes
  all of it. If Stripe refuses the reversal (the seller's
  balance is short), the order reads `disputeRecovery.status: "owed"`, a
  `marketplace_order.dispute_owed` row is written to your audit log, and
  nothing is tried again by itself: settle it with the seller. **You bear
  it** (`owner_bears`) leaves the seller's transfer alone. A storefront sale
  is unchanged: Stripe takes the dispute from the seller's own account.
- **Access while a dispute is open** (`revokeOnOpenDispute`, off by
  default): on, access is taken back as soon as the dispute opens, and
  given back if it is won; off, it is taken back only when it is lost.

The order shows what happened as `disputeRecovery` (`status`, `amount`,
and `fee`, which is always 0).

**Money moved in the Stripe Dashboard.** Transfers to sellers and your fees
on storefront sales live on your own Stripe account, so your account's
webhook endpoint (not the Connect one) must also send `transfer.created`,
`transfer.updated`, `transfer.reversed`, `application_fee.created`,
`application_fee.refunded` and `charge.dispute.updated`. **Check Stripe
setup** lists them with the rest. With them, a reversal or a fee refund you
make in the Stripe Dashboard reaches the order, for you and the seller, as
`transferReversed` and `appFeeRefunded` (Stripe's totals, in minor units),
and a marketplace sale's dispute shows its progress before it closes.

## Fulfilment

A line with a benefit the seller delivers by hand (`manual`) waits on them:
its `fulfilment` is `due`. The seller, you or your server mark it sent, with
a note the buyer reads on their order, such as a tracking number. Each line
is marked once.

A line that ships (an item with shipping rules) waits too, and follows its
[shipments](/billing/shipping): in progress once one is on its way, and
fulfilled once every unit is delivered.

## Files

A `file` benefit names a release series of its seller (`seriesId`, such as
`my-novel`). The seller uploads files and puts them in a release of that
series: a version, notes for buyers and up to 20 files. A release is a draft
until the seller publishes it, and fixed after: a fix is a new release.

```tsx
import { SellerFiles } from '@buildbase/sdk/react';

<SellerFiles subject={{ user: true }} seriesId="my-novel" />;
```

- **What can be sold.** PDF, ZIP, EPUB, MP3, M4A, WAV, OGG, FLAC, MP4, MOV and
  the raster images, each checked by its bytes: a file that is not what its
  name says is refused. Large files go up in chunks, up to your plan's file
  size.
- **Malware scan.** Every file is scanned before it can be sold, and a
  release publishes only when its files are clean. Files read `not_scanned`
  where no scanner runs yet, and such a file sells only in a non-production
  environment such as Staging, where nothing real is charged.
- **Which releases a buyer gets** is the benefit's `updatePolicy`: `latest`
  (every release, for ever), `purchased` (the release they bought) or
  `until` (releases for `updateMonths` after buying).
- **Downloads.** A buyer's holding lists those releases. Each download asks
  for a link that works for a few minutes and counts toward the benefit's
  `downloadLimit`; a refund or a revoke stops it, even for a link already
  given. A guest downloads from their order page with its token.

```tsx
import { HoldingFiles, useHoldings } from '@buildbase/sdk/react';

const { holdings } = useHoldings({ kind: 'file' });
holdings.map((h) => <HoldingFiles key={h._id} holding={h} />);
```

Your server does the same for a seller with `/api/v1/server/commerce/files`
and `/api/v1/server/commerce/releases` (`?sellerId=`). Codes:
`FILES_NOT_SET_UP` (this platform keeps no files for buyers yet),
`RELEASE_REFUSED`, `RELEASE_VERSION_TAKEN`, `FILE_IN_USE` (a release holds
it), `DOWNLOAD_LIMIT_REACHED`.

## Licence keys

A `license_key` benefit gives the buyer one key per unit: a five-seat
licence is five keys. A key works while its grant does. `activationLimit`
caps the instances one key runs on at once (a machine, a site), and
`expiresInDays` ends it that many days after buying.

Your software checks a key from the customer's machine with no secret, so
`LicensesApi` is safe to ship in a desktop app, a CLI or a plugin:

```ts
import { LicensesApi } from '@buildbase/sdk';

const licenses = new LicensesApi({ serverUrl, version: 'v1', orgId });
const check = await licenses.activate(key, machineId);
if (!check.valid) showWhy(check.reason);
```

- **`validate(key, instance?)`** says whether the key works now and, with an
  instance, whether that instance holds an activation. **`activate`** takes
  one, and is the same answer again for an instance that already holds one.
  **`deactivate`** gives one back.
- **Every answer** is `{ valid, reason, key, item, instance }`. `reason` is
  `not_found`, `revoked`, `expired`, `not_yet`, `limit_reached` or
  `not_activated`. A key of another app and a mistyped key both read
  `not_found`. Keys match without dashes, spaces or case.
- **The buyer** sees their keys and the instances using them, and frees one
  for a new machine. A guest sees their keys on their order page.

```tsx
import { HoldingKeys, useHoldings } from '@buildbase/sdk/react';

const { holdings } = useHoldings({ kind: 'license_key' });
holdings.map((h) => <HoldingKeys key={h._id} holding={h} />);
```

Too many checks from one place are refused with `LICENSE_CHECKS_LIMITED`
(429). Your server makes the same checks with
`/api/v1/server/commerce/licenses/:action`, and frees an activation with
`POST /api/v1/server/commerce/grants/:id/deactivate` (`{ unit, instance }`).
The console and your server see a key's last group and its activations,
never the key itself.

## Subscriptions

A recurring price buys through the same `BuyButton` and checkout as a
one-time price: Stripe's page opens in subscription mode, and the order the
buyer pays carries `subscriptionId`. Each later period is paid on that
subscription, not as a new order.

![HoldingSubscription at 360 pixels wide: the Patron tier at $30.00 every month, moving to Member at the next renewal with Stay on Patron, and Cancel subscription](/images/billing/selling/sdk-holding-subscription.webp)

![The Subscriptions tab on All: each subscription's item, buyer, status, price and next renewal, two marked Billed together with 1 item](/images/billing/selling/console-subscriptions-tab.webp)

```tsx
import {
  HoldingSubscription,
  useItemSubscriptions,
} from '@buildbase/sdk/react';

const { subscriptions } = useItemSubscriptions();
subscriptions.map((s) => <HoldingSubscription key={s._id} subscription={s} />);
```

- **What the buyer holds** runs to the end of the period paid for (a day
  more, so a renewal that lands late shows no gap), and each paid renewal
  moves it on.
- **A failed renewal** makes the subscription `past_due`. What it gives
  stays, in grace, while Stripe retries, and the buyer sees Stripe's page to
  pay it (`latestInvoiceUrl`). Paid, it is `active` again; when Stripe stops
  retrying, it ends, and so does what it gave.
- **Cancel.** The buyer cancels at the end of the period paid for and can
  undo it until then; a guest cancels from their order page. The seller,
  you in the console, and your server can also end one now. Nothing is
  refunded by a cancel; refund the order if you mean to. A subscription
  [billed together](#several-subscriptions-in-one-cart) with others
  cancels on its own; how is your cart setting.
- **A trial** (`trialDays` on the price) starts with nothing charged; the
  first payment is taken when it ends.
- **An introductory price** (`intro: { amount, periods }` on the price)
  is what checkout charges first; after that many payments the regular
  `amount` takes over. A free trial and an intro do not go together.
- **A set number of payments** (`endAfterPayments`) ends the subscription
  after the last one, with `endReason: 'completed'` and
  `item_subscription.ended`. A cancel and a resume keep the count.
- While either runs, the subscription reads `termsRunning: true` and keeps
  its tier. It also says `paymentsMade`, `paymentsLeft` (null when it runs
  until cancelled) and `nextPeriodAmount`, which `<HoldingSubscription>`
  shows ("Intro price $5.00 for the first 2 payments, then $15.00 every
  month", "Payment 2 of 6").

### Moving between tiers

A buyer moves a subscription to another variant of the same item: up now,
with the rest of the period charged at once, and down at the next renewal.
The item type can make either one `now` or `at_renewal`
(`pricing.recurring.changes`, under **Pricing** in the item type builder).
Variants are ordered by `rank`, or by price when they have none.

```tsx
<HoldingSubscription
  subscription={s}
  tiers={[
    { variantId: 'member', label: 'Member' },
    { variantId: 'patron', label: 'Patron' },
  ]}
/>
```

- **Before it is made**, `GET /me/item-subscriptions/:id/change-preview?variantId=`
  says up or down, when, the new price and what is charged today
  (`dueNow`), so the buyer confirms knowing the amount.
- **The new tier** has the same billing period, currency and quantity, at
  a fixed price. Another period, a pay-what-you-want or free tier, or one
  with limited places is refused with the reason: buy it instead.
- **What they hold follows the tier.** A benefit both tiers give stays; one
  only the new tier gives is granted; one only the old tier gave ends when
  the move takes effect.
- **A move at renewal** shows as `pendingChange` until then. Asking for
  the current tier withdraws it.
- **A declined card** on a move up changes nothing:
  `SUBSCRIPTION_CHANGE_NOT_PAID`, with the card's reason.
- Not while a renewal is unpaid (pay it first) or the subscription is set
  to end (keep it going first). The seller cannot move a buyer.

Your server reads and cancels them with `/api/v1/server/commerce/subscriptions`
(`POST /:id/cancel` with `mode: 'at_period_end'` or `'now'`,
`POST /:id/resume`, `POST /:id/change` with `variantId`, and
`GET /:id/change-preview?variantId=`), and gets `item_subscription.created`,
`.renewed`, `.past_due`, `.recovered`, `.canceled`, `.resumed` and
`.ended`, each with `{ subscription }`, and `.changed` with
`{ subscription, change }` when a move takes effect. Codes:
`SUBSCRIPTION_NOT_FOUND`, `SUBSCRIPTION_CHANGE_REFUSED`,
`SUBSCRIPTION_CHANGE_NOT_PAID`. In the storefront shape the subscription
lives on the seller's Stripe account, and its events reach you through the
Connect endpoint: press **Set it up for me** under Selling, Settings so it
sends them.

## Paying in parts

A one-time fixed price can be paid in parts, when its item type allows it
(under **Pricing** in the item type builder, **Deposits** for a deposit or
milestones, **Payment plans** for installments). The buyer pays the first part on Stripe's page, which saves their
card, and each later part is charged on that card. What the order gives is
theirs from the first part.

![OrderParts at 360 pixels wide: three milestones, Sketch and Draft paid, and Final to pay with Approve and pay](/images/billing/selling/sdk-order-parts.webp)

![An order paid in parts in the console: Paid in parts, each part with its amount, status and date, Charge now on the next part, and Stop the rest](/images/billing/selling/console-order-parts.webp)

| On the price                                   | The parts                                    | Who takes the next one                       |
| ---------------------------------------------- | -------------------------------------------- | -------------------------------------------- |
| `deposit: { percentBps }`                      | The deposit at checkout, then the balance    | The seller asks for it when the work is done |
| `milestones: [{ label, percentBps }]` (2 to 6) | One per milestone, the first at checkout     | The buyer approves each one                  |
| `installments: { count, interval }`            | Equal parts, any leftover cents on the first | Charged on its date by the hourly pass       |

```tsx
import { OrderParts, useMyOrder } from '@buildbase/sdk/react';

const { order } = useMyOrder(orderId);
return order ? <OrderParts order={order} /> : null;
```

- **The order** carries `partPlan: { kind }` and `parts`, each with its
  `amount`, `status` (`due`, `charging`, `paid`, `failed`, `canceled`),
  `label`, `dueAt` and `paidAt`. It reads `paymentStatus: 'partially_paid'`
  until the last part is paid; then `order.paid` fires.
- **The buyer** approves a milestone with "Approve and pay"
  (`POST /me/orders/:id/parts/:index/pay`, a guest with the order's token).
  **The seller** asks for a deposit's balance from `<SellerSales>`
  (`POST /me/sales/:id/parts/1/charge`, or the workspace's sales with
  `workspace:selling:manage`). A seller never charges a milestone or an
  installment. Parts go in order.
- **A refused card** leaves the part `failed`, with `order.part_failed`.
  The buyer pays it on Stripe's page ("Pay now"), and has 7 days
  (`graceUntil`); after that, what the order gave is taken back and the
  parts after it are stopped.
- **Each part is its own payment** in `payments` (`deposit`, `balance`,
  `milestone` or `installment`) and is refunded on its own: a refund names
  its `paymentId`.
- **Your server and you in the console** can charge any part now, or stop
  the parts not yet paid, keeping or taking back what the order gave:
  `POST /api/v1/server/commerce/orders/:id/parts/:index/charge` and
  `POST /:id/parts/stop` with `{ revoke }`. **Reconcile now** under Selling
  also runs the hourly pass.

- **With Stripe Tax on**, each part is taxed as it is paid and carries its
  `tax` (see [tax at checkout](/billing/selling#tax-at-checkout)).

Code: `PART_REFUSED` (409) when the part is not the caller's to charge now,
with the reason.

## Carts across sellers

A buyer, signed in or a guest, can pay for items from several sellers in
one checkout. Each seller gets an order of their own, all sharing a
`cartId`. In the marketplace shape it is one Stripe page, and once Stripe
says the money arrived, each seller is sent their share of it (their
lines, less your fee) as a Stripe transfer. In the storefront shape it is
one Stripe page per seller, in turn ([steps](#storefront-carts-in-steps)).

![CartSummary at 360 pixels wide: lines from two sellers, the code SPRING applied, the discount and the new total, and Check out](/images/billing/selling/sdk-cart-summary.webp)

```tsx
import { CartSummary, useCart, useMyCart } from '@buildbase/sdk/react';

// On your item page: put a line in this browser's cart.
const cart = useCart();
cart.add({
  itemId,
  variantId,
  priceId,
  quantity: 1,
  title: item.title,
  unitAmount: price.amount,
  currency: price.currency,
  sellerName: item.seller.displayName,
});

// On the cart page: the lines, the total and one Check out.
<CartSummary />;

// Stripe returns to the success URL with `?cart=<id>`.
const { cart: paid } = useMyCart(cartId, { waitForPayment: true });
```

- **The cart lives in the browser** until checkout (per app, up to 10
  lines). `useCart().checkout(urls)` calls `POST /cart/checkout` with
  `{ lines, successUrl, cancelUrl }` and answers `{ cartId, orders, url }`.
  Call `clear()` once the buyer is back.
- **What a cart takes:** paid prices in one currency, with up to three
  subscriptions ([billed together](#several-subscriptions-in-one-cart); the
  number is yours to set, 1 to 10) or one price paid in parts (never
  both), and Stripe Tax on or off. A refusal names its line in `problems`
  (`lines.<n>`), checked before anything is held:
  - `cart_recurring_many`: more subscriptions than you allow, or the same
    one twice;
  - `cart_recurring_interval`: a subscription on another billing period
    than the others (subscriptions bought together renew together);
  - `cart_recurring_scheduled_many`: a subscription with an introductory
    price or a set number of payments beside another subscription;
  - `cart_parts_many`: a second price paid in parts;
  - `cart_recurring_and_parts`: a subscription and a price in parts;
  - `cart_trial_mixed`: a subscription that starts with a free trial, with
    a one-time line, or beside a subscription with another trial or none
    (subscriptions bought together start their trials together);
  - `cart_coupon_mixed` (at `couponCode`): a coupon on a cart that carries
    a subscription or a price in parts;
  - `cart_currency`, `cart_free`, `cart_empty` or `cart_too_many`.

  An older platform also answered `cart_storefront`, `cart_tax`,
  `cart_recurring` and `cart_parts`; these are no longer sent.

- **A subscription in a cart** (marketplace shape) is one Stripe page in
  subscription mode: the subscription's first period and every other line
  are on its first invoice. The subscription gets its order and its
  subscription, as when bought on its own, and the other lines their
  orders. Its seller is paid per invoice: your account keeps each invoice
  and sends the seller their share from it once paid (less your fee and the
  tax), the first invoice's other lines to their own sellers. Renewals pay
  the subscription's seller only. Several subscriptions in one cart are
  [billed together](#several-subscriptions-in-one-cart).
- **A price paid in parts in a cart** (marketplace shape): the page charges
  every other line in full and the first part, and saves the card; the
  later parts are charged on it as for a [sale in parts](#paying-in-parts).
- **With Stripe Tax on** (marketplace shape), Stripe works out the tax on
  the one page, on your account, and each order and line records its own
  share of it (from Stripe's line items). Sellers are sent their share
  without the tax. See [tax at checkout](/billing/selling#tax-at-checkout).
- **Guests**, when the app sells to them (as for [guests](#guests)): pass
  the name and email you collected, `<CartSummary guest={{ name, email }} />`
  or `useCart().checkout(urls, { name, email })`. It calls
  `POST /api/v1/guest/:orgId/cart/checkout` and answers the cart's `token`
  too; Stripe returns with `?cart=<id>&token=<token>`, and
  `useGuestCart(cartId, token, { waitForPayment: true })` reads the cart back
  (`GET /api/v1/guest/:orgId/carts/:cartId` with the token in
  `x-order-token`), with `tokens`, each order's own token for its order
  page. Each order also sends its receipt as a guest's order does. A wrong
  token reads as "not found".
- **Stock** for every line is held before any order is opened; a line
  short of stock answers `OUT_OF_STOCK` with its line, and nothing is held.
  An abandoned cart gives every hold back.
- **Bookings:** a line booked by time names its `slotStart` (a bundle line
  its `slotStarts`), and every booking is held after the orders open, all
  or nothing; a full slot answers `409 SLOT_UNAVAILABLE` at
  `lines.<n>.slotStart` and holds nothing. A booking is paid once, so
  beside a subscription it goes on the first invoice, and beside a price
  in parts it is paid in full with the first part. See
  [Booking calendars](/billing/bookings#in-a-cart).
- **Reading it back:** `GET /me/carts/:cartId` (`useMyCart`) answers the
  cart's orders; another person's cart is not found. The console shows the
  others under **Bought together** on each order (with each order's tax and
  the cart's when Stripe Tax is on, even at 0), and lists filter by
  `cartId`. A subscription bought in a cart reads **Paid per invoice**
  (`payout: 'per_invoice'` on the subscription), and each payment the app
  paid its seller from shows that transfer. An order read by your server
  says `taxMode: 'stripe_tax'` when Stripe Tax worked out its tax.
- **Refunds and disputes** stay per order: refunding one seller's order
  takes the money back from that seller's transfer only. A dispute on the
  cart's charge is recorded on every order in it. A refund made in Stripe's
  dashboard on a cart's charge names no order, so it is divided: all of it
  on one order when it is exactly what is left of that one, otherwise over
  the cart's orders in proportion to what is left of each (a price in
  parts counts its first part, paid on that charge). Each seller then
  gives back their share from their transfer, by your fee policy, as for a
  refund made in the app; if that refund later fails, each seller is paid
  their share back. Refunding from the order keeps the choice yours. A
  refund of a renewal of a subscription bought in a cart takes back that
  invoice's transfer to its seller.
- **Referrals:** the cart's `referral` is read once, at checkout, and
  every order the cart makes carries it: each seller's order on the one
  page, a subscription paid per invoice, a price in parts and each
  storefront step. Each line earns as it would bought on its own: per paid
  invoice on the subscription, per part, per step. A seller's program comes
  off that seller's share (a smaller transfer, per invoice too, or a larger
  fee on a step); yours comes off your side. A refund, a dashboard refund
  divided over the cart included, takes back each commission's share. See
  [Affiliates](/billing/affiliates#carts).

### Several subscriptions in one cart

A cart can carry more than one subscription: up to three unless you change
it (**Subscriptions per cart** under Selling, Settings, Carts, 1 to 10;
`carts.maxSubscriptions` in the settings). They are billed together: one
Stripe subscription with an item for each, renewing on one date, on one
invoice and one card.

- **What goes together:** the same currency and billing period (a monthly
  and a yearly subscription are bought apart), the same free trial or none,
  and no introductory price or set number of payments (those run on a
  schedule of their own). No coupon, and no item that ships or that only
  some countries may buy, beside them. Each subscription is still an order
  of its own, with its own subscription, grants and refunds.
- **Marketplace shape:** one page for the whole cart. Each paid invoice is
  split by item: each subscription records its own payment (its line, its
  tax and any proration of its own tier move), and each seller is sent
  their own share of that invoice by a transfer from its charge, less your
  fee, keyed on the invoice and the subscription. One seller's renewal
  never pays another, and a seller's affiliate program earns on their item
  only.
- **Storefront shape:** one seller's subscriptions in the cart are one step,
  one page on their account and one subscription there. Your share of each
  invoice is one percent, each item's own fee weighted by what it bills,
  and it is set again when an item leaves or moves tier. Another seller's
  subscriptions are their own step.
- **One card for all of them.** A failed renewal makes every subscription
  billed together `past_due` at once, whoever sells it; paying the invoice
  brings them all back. The seller's view says so.
- **Cancelling one** leaves the others going. By default (`carts.itemCancel:
'period_end'`) its item leaves the shared subscription at once with
  nothing prorated, so the next invoice no longer bills it, and the buyer
  keeps what it gives to the end of the period paid for (`endsAt`); a
  resume before then puts it back. With `'immediate_prorated'` it ends at
  once and the unused part of its period is refunded on its order, its
  seller giving their share back as for any refund. Ending one now (the
  seller, you, your server) ends it at once with no refund. Cancelling the
  last one cancels the subscription itself, as on its own.
- **Moving between tiers** works on the subscription's own item, with the
  same billing period, as for one bought alone.
- **Reading it:** each subscription says `groupId` and, in its own reads,
  `siblings` (each with `_id`, `status`, `sellerId`, `itemTitle`,
  `variantLabel` and `endsAt`); a seller sees another seller's item as
  there, without its title. `endsAt` says when one set to end ends. The
  console shows **Billed together with N items**, and its subscriptions
  list filters by group. Every `item_subscription.*` event is sent per
  subscription and carries `groupId`.
- **Refunds** stay per order. A refund made in Stripe's dashboard on an
  invoice that billed several is spread over their payments by what is
  left of each.

### Storefront carts in steps

In the storefront shape each seller's account takes its own charge, and one
charge cannot pay several accounts. So a cart is paid in steps: every order
is made and its stock held when the buyer checks out, and then each order
gets its own Stripe page on its seller's account, in turn (a price in
parts is a step of its own, and so are a seller's subscriptions, billed
together on one page). Your fee on each step is the sum
of its lines' fees; Stripe Tax runs on each seller's account when on.

![CartSummary in the storefront shape at 360 pixels wide: one step per seller, the first Paid, the second Waiting, and Pay next seller](/images/billing/selling/sdk-cart-steps-storefront.webp)

- `POST /cart/checkout` answers `steps` beside `url`: each with `orderId`,
  `sellerName`, `total`, `status` (`pending`, `open`, `paid` or `expired`)
  and the open step's `url`. `url` is step 1's page. A step that pays
  several orders (a seller's subscriptions) also lists them in `orderIds`,
  and its `total` is theirs together.
- After a step is paid, Stripe returns to your success URL with
  `?cart=<id>&step=<n>` (and a guest's `token`). Call
  `POST /me/carts/:cartId/next` (a guest:
  `POST /api/v1/guest/:orgId/carts/:cartId/next` with the token in
  `x-order-token`) to open the next step; it answers the cart with its
  steps and the open step's `url`. Calling it again while a step is open
  answers the same page. A buyer back before Stripe's event for the step
  arrives is not sent to the paid page again: the call reads the open
  step's page from Stripe and, once paid, completes it as the event would
  (the event then changes nothing) before opening the next step.
- **Each step stands alone.** A paid step stays paid if the buyer stops. A
  step's stock is held while the steps before it are paid, and for its own
  page's time once opened (up to 24 hours from checkout); a step whose hold
  runs out, or whose page is left unpaid, expires by itself and gives its
  stock back. A coupon's use is held per step.
- **Bookings in steps:** every step's bookings are held up front with the
  stock, all or nothing across the cart, and for as long as that step's
  stock: opening a step moves its bookings' hold with its page's. A step
  that expires gives its seats back; the steps already paid keep theirs.
- `GET /me/carts/:cartId` (and the guest read) answers `steps` for such a
  cart, and `tax` for any cart. Each order says its `cartStep` and
  `cartStepStatus`, which the console shows under **Bought together**.
- With Stripe Tax, a step of several lines records each line's share of
  its tax, from Stripe's line items on the seller's account.

## Your server

```ts
const res = await fetch(
  `${serverUrl}/api/v1/server/commerce/orders?paymentStatus=paid`,
  {
    headers: { Authorization: `Bearer ${orgId}:${apiKey}` },
  }
);
const { docs } = await res.json();
// docs[0]: { _id, checkout, paymentStatus, buyer, seller, total, appFee, sellerAmount, lines }
```

`GET /api/v1/server/commerce/orders/:id` reads one order. Filter by
`paymentStatus`, `checkout` (`open`, `complete`, `expired`), seller or buyer.
The API key's role needs the **Orders** resource.

`GET /api/v1/server/commerce/grants` lists what buyers hold, filtered by
`holderId`, `orderId`, `itemId`, `kind`, `status` or the object
(`resourceType` and `resourceId`); `/grants/:id` reads one, and
`/grants/access` answers for one person and one object.

`POST /api/v1/server/commerce/orders/:id/refund` refunds `{ lines: [{ lineKey,
amount }] }` or `{ amount }` (all that is left when both are missing), with a
`reason`; `POST /orders/:id/lines/:lineKey/fulfil` marks a line sent, with a
`note`. Both need update on the **Orders** resource, and a refund (an order's
or a return's, `POST /api/v1/server/commerce/returns/:id/refund`) also needs
create on **Refunds** (`marketplace_refunds`), as it does in the console.
`POST /api/v1/server/commerce/grants/:id/revoke` with a `reason` takes a
grant back at once; it gives no money back. The API key's role needs the
**Grants** resource, and update on it to revoke.

### Retrying a write safely

A refund can take longer than your HTTP client waits: Stripe may be slow to
answer, and the refund still goes through after your client has given up.
Send an `Idempotency-Key` header with each commerce write, and send the same
key again when you retry it, so a retry never refunds, charges or cancels
twice:

```ts
const key = crypto.randomUUID(); // once per action, kept for its retries

const res = await fetch(
  `${serverUrl}/api/v1/server/commerce/orders/${orderId}/refund`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${orgId}:${apiKey}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': key,
    },
    body: JSON.stringify({ amount: 4500, reason: 'Requested by the buyer' }),
  }
);
```

The key is 1 to 255 printable ASCII characters (no spaces); anything else is
a `400` before anything runs. A key belongs to the API key's owner, the route
and the environment it was sent in, and is kept for 24 hours:

| You send the key again                            | You get                                                                                                |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| After the first request finished with a `2xx`     | The first answer again, the same status and body, with `Idempotent-Replayed: true`. Nothing runs twice |
| While the first request is still running          | `409` `IDEMPOTENCY_IN_PROGRESS`. Wait a moment and retry with the same key                             |
| With another body, or on another order            | `422` `IDEMPOTENCY_KEY_REUSED`. Nothing runs. A new action takes a new key                             |
| After the first was refused or failed (not `2xx`) | It runs again: a refused or failed request keeps nothing                                               |

A refund made under a key is made under the same id at Stripe, so a retry
after a failure never refunds twice there either. If Stripe refused that
refund, the same key answers `502` `REFUND_FAILED` again; send a new key to
try again. Without the header nothing changes: every request is a new one.

The server routes that take the key: an order's refund, a part's charge and
stopping the parts (`/orders/:id/refund`, `/orders/:id/parts/:index/charge`,
`/orders/:id/parts/stop`); a return's approve, reject and refund; a new
shipment (`POST /shipments`, which may buy its label); a subscription's
cancel, resume and change; a booking's cancel; an affiliate's approve,
pause, resume and ban; and a grant's revoke and deactivate. Your refunds in
the console take it too.

Through the SDK, as the signed-in person, these take it as well:

- **A seller** (`/me/...` and `/workspaces/:id/sales/...`): a sale's refund,
  a return's approve, reject and refund, a new shipment, a part's charge, a
  subscription's cancel, a booking's cancel, and marking an affiliate payout
  paid.
- **A buyer** (`/me/...`): their own booking's cancel, which can refund
  (`bookings.cancelMine`), a part paid, and their subscription's cancel,
  resume and change.
- **A guest**, with the order's token (`/guest/:orgId/orders/:id/...`): the
  subscription's cancel, a part paid and the booking's cancel. A guest's
  key belongs to the order token it was sent with.

### As the signed-in person

Your server can also act as one of your users, with their session, through
the SDK. It sees what that person sees: their own orders and what they hold
as a buyer, and their own sales as a seller.

```ts
import BuildBase from '@buildbase/sdk';

const bb = BuildBase({ serverUrl, orgId }).withSession(sessionId);

const { docs: items } = await bb.commerce.items({ typeKey: 'course' });
const start = await bb.commerce.checkout(items[0]._id, {
  variantId,
  priceId,
  successUrl: 'https://example.com/thanks',
  cancelUrl: 'https://example.com/shop',
});
// start.url: Stripe's page to send them to, or null for a free claim

const { docs: orders } = await bb.commerce.orders();
const { hasAccess } = await bb.commerce.access('course', courseId);

// As a seller: { user: true } for their own sales, or a workspace id
const { docs: sales } = await bb.commerce.sales(
  { user: true },
  { paymentStatus: 'paid' }
);
```

`commerce` also has `item`, `order`, `holdings`, `subscriptions`,
`subscription`, `cancelSubscription`, `resumeSubscription`,
`previewSubscriptionChange`, `changeSubscription`, `sale`, `refundSale` and
`fulfilSale`. Amounts are minor units. Checkout and refunds are never
retried: each call is a new one. A refund sent with an `Idempotency-Key`
header ([retrying a write safely](#retrying-a-write-safely)) can be retried
with the same key without refunding twice.

## Agents

The SDK's MCP server gives an agent that acts for one of your users the same
reach, as built-in tools that run under that user's session (see
[MCP for agents](/reference/mcp-agents#commerce-tools)):

| Tool                                                                                                                                         | Does                                                                       |
| -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `search_items`, `get_item`                                                                                                                   | Reads the published items and their prices                                 |
| `list_my_orders`, `get_my_order`, `list_my_holdings`, `check_item_access`, `list_my_item_subscriptions`                                      | Reads what the user bought and holds                                       |
| `preview_item_subscription_change`                                                                                                           | Says what a tier change would charge, changing nothing                     |
| `list_my_sales`, `get_sale`                                                                                                                  | Reads the user's sales as a seller                                         |
| `start_item_checkout`, `cancel_item_subscription`, `resume_item_subscription`, `change_item_subscription`, `fulfil_sale_line`, `refund_sale` | Acts, and only when you expose writes (`builtinTools: 'all'` or `include`) |

The reads are in the default read-only set. An agent never pays by itself:
`start_item_checkout` answers Stripe's page for the user to open.

## Webhooks

| Event               | When                                                                             |
| ------------------- | -------------------------------------------------------------------------------- |
| `order.created`     | A buyer started checkout: the order is unpaid, its stock held                    |
| `order.paid`        | It was paid, or a free claim completed                                           |
| `order.refunded`    | Money went back to the buyer, with the refund                                    |
| `order.fulfilled`   | A line was marked sent, with its key                                             |
| `order.disputed`    | The buyer disputed the charge, or the dispute closed                             |
| `order.part_paid`   | A part of an order paid in parts was paid                                        |
| `order.part_failed` | The saved card was refused for a part                                            |
| `order.shipped`     | The order shipped: its first shipment on its way ([shipping](/billing/shipping)) |
| `grant.created`     | A buyer was granted one benefit of what they bought                              |
| `grant.revoked`     | A grant was taken back, with its reason                                          |

The `order.*` events carry `{ order }` (and `order.refunded` the `refund`,
`order.fulfilled` the `lineKey`, `order.part_*` the `partIndex`): the order as your server reads it,
with your fee (`appFee`) and the seller's share (`sellerAmount`), amounts in
minor units. An order refused after payment because the item is not sold in
the buyer's country keeps its true sequence (`order.paid`, then
`order.refunded`, and for a subscription `item_subscription.canceled` and
`item_subscription.ended`), and those refund and cancel events add
`reason: "buyer_country"`; other refunds and cancels carry no `reason`. The `grant.*` events carry `{ grant }`: its holder and payer,
the order line, the benefit, units, dates and, for `access`, the object.

## In the console

**Billing > Selling > Orders** lists every order, paid first, with your fee
beside each total. Each order's page shows the buyer, the seller, what was
bought, how the money split, and what the buyer got, where you can revoke a
grant with a reason only you see. It also shows each refund and refunds any
part of what is left, a dispute, and Mark fulfilled on a line the seller
delivers. A subscription's order lists its payments, each renewal refunded
on its own. An order paid in parts lists its parts, charges the next one
now, and stops the rest; the **Partly paid** tab lists those still being paid. A file grant shows its downloads, and an item that sells files
lists each series' releases with their malware scan. The console's **Orders** resource governs
the orders and **Grants** what the buyer got. Refunding (an order, a line, a
renewal or a return) also needs **Refunds** (`marketplace_refunds`), so a role
can work orders without sending money back; owners and admins hold all three,
and other roles only when you add them.

![An order page in the marketplace shape: buyer, seller, Coaching session at $45.00, your fee $4.50, the seller's share $40.50, the buyer's grants, and Mark fulfilled on the line](/images/billing/selling/console-order-marketplace.webp)

![An order page in the storefront shape: $45.00 charged on the seller's Stripe account, your fee $4.50 sent to you, the seller's share $40.50, and the cart it was bought in](/images/billing/selling/console-order-storefront.webp)

## Payouts

Stripe pays each seller's balance to their bank on their payout schedule.
`<SellerHome>` gives the seller **Payouts and balance**, Stripe's own
payouts component: what is ready, what is on its way, and every payout. A
workspace member without the manage permission sees it too; paying out
early is the manager's. In the console each seller's page shows their
balance and latest payouts, read from Stripe.

![A seller's page in the console: Active, Can sell and Can get paid, the Stripe balance, and the latest payouts with their arrival dates](/images/billing/selling/console-seller-payouts.webp)

Your server gets `payout.created` when one starts, `payout.paid` when the
bank has it and `payout.failed` when the bank refused it (the money goes
back to the seller's balance), each with `{ payout }`: its seller, amount,
currency, status, method, arrival date and, on a failure, the bank's
reason. The payout is read again from Stripe before each is sent, and its
status always matches the event's name: a paid payout that later fails sends
`payout.failed`, never a late `payout.paid` that says `failed`.

## Missed events

Every hour BuildBase reads your sales' events back from Stripe (on each
seller's account, and on yours for the marketplace shape) and processes any
that never arrived, for example while your server was down for longer than
Stripe retries. An event whose processing failed is tried again, three times
at most. Selling, Settings shows the last pass and how many events failed;
**Reconcile now** runs a pass at once (owners and admins). Processing an
event twice changes nothing, so a pass is always safe to run.

An event from the other Stripe mode (a test event when your saved key is
live, or the reverse) and an event type selling does not handle are stored
as **Skipped** with their reason under **Stripe events**, and are never
processed: neither a pass nor a redelivery queues them.

## Selling already on?

Storefront sales arrive through your Connect endpoint, which now needs
Stripe's Checkout events too. An endpoint made before checkout arrived shows
as missing events under **Billing > Selling > Settings**, "Your sellers' Stripe
events"; **Set it up for me** adds them.
