# Commerce

Buyers choose an item in a conversation with a seller. mondochat creates the purchase and posts a card, and your site charges the money. mondochat never holds balances, refunds or payouts.

## Before you start

- Roles: a seller has `seller: true` in your `roles`. See [Tenants and apps](/guides/tenants-and-apps/).
- Policy: the row from the buyer's role to the seller's role has `buy` on. A seller may post offers when the row from the seller to the buyer has `offers` on. A guest cannot buy unless the row for their role has `buy` on.
- Settings: `commerce.enabled` is on. It is off by default.
- An endpoint marked **Answers callouts** if you use the `webhook` catalog or `server_charge`. See [Webhooks overview](/webhooks/overview/) and [Verify signatures](/webhooks/verify/).

```sh
export MC=https://app.mondochat.io/v1
export SESSION=<session token>
auth=(-H "Authorization: Bearer $MONDOCHAT_SERVER_SECRET" -H "Mondochat-App-Id: $MONDOCHAT_APP_ID" -H 'content-type: application/json')
```

## Steps

1. Choose where the catalog comes from with `commerce.catalog_source`.

   - `webhook`: mondochat calls your `catalog` callout each time a buyer opens the shop, so prices are always yours.
   - `dashboard`: mondochat uses the items you manage in the dashboard, for the whole app or for one seller.

   The `catalog` callout sends `{viewer, seller, conversation_id, lang}` and expects `{items: [{id, kind, title, description?, price_minor, currency, image_url?}], balance?: {amount_minor, currency, label?}}`. The balance shows when `commerce.show_balance` is on, and `label` names the unit, for example Credits. Read the catalog yourself with `GET /v1/conversations/{id}/catalog`.

2. Choose a payment mode with `commerce.payment_mode`.

   | Mode | What happens |
   |---|---|
   | `server_charge` | mondochat calls your `charge` callout and gets the answer at once. Answer `{status: paid \| insufficient \| failed \| pending, external_ref?, topup_url?}`. |
   | `page_callback` | The chat tells your page, which takes the payment and then confirms through the server API. |
   | `checkout_url` | The buyer is sent to `checkout_url_template`, and your server confirms afterwards. |

   In every mode a paid purchase posts one `purchase` card to the conversation and publishes `purchase.updated`.

3. With `server_charge`, handle the `charge` callout. It sends `{purchase_id, buyer, seller, item: {id, kind, title, description}, price_minor, currency}` with an `Idempotency-Key` of the purchase id. Charge each purchase once.

   - `paid` marks the purchase paid.
   - `insufficient` sends the buyer to your top-up page: the `topup_url` you answer, else `commerce.topup_url_template`. With neither, the frame sends `navigate` to the host with reason `topup`, so your site opens its own top-up.
   - `failed` marks the purchase failed.
   - `pending`, a timeout or a failed callout leaves it `pending`. mondochat then asks your `charge_status` callout, which sends `{purchase_id}` and takes the same answer as `charge`, after 30 seconds, 2 minutes, 10 minutes and 1 hour.

4. With `page_callback`, take the payment on your page. The embed calls your `onPurchase` with the `Purchase`. Resolve with `{status: 'paid' | 'failed' | 'cancelled' | 'pending', external_ref?}`. A rejected promise counts as `failed`. The browser's answer only changes what the buyer sees. The purchase is settled when your server confirms it:

   ```sh
   curl -X POST $MC/server/purchases/$PURCHASE_ID/confirm "${auth[@]}" \
     -H "Idempotency-Key: confirm-$PURCHASE_ID" \
     -d '{"status": "paid", "external_ref": "order-1234"}'
   ```

   ```js
   await fetch(`https://app.mondochat.io/v1/server/purchases/${purchaseId}/confirm`, {
     method: 'POST',
     headers: {
       authorization: `Bearer ${process.env.MONDOCHAT_SERVER_SECRET}`,
       'mondochat-app-id': process.env.MONDOCHAT_APP_ID,
       'idempotency-key': `confirm-${purchaseId}`,
       'content-type': 'application/json',
     },
     body: JSON.stringify({ status: 'paid', external_ref: 'order-1234' }),
   })
   ```

   `status` is `paid`, `failed` or `cancelled`. The answer is the `Purchase`. See [Embed](/guides/embed/).

5. With `checkout_url`, fill `commerce.checkout_url_template`. The placeholders are `{lang}`, `{purchase_id}`, `{item_id}`, `{amount_minor}`, `{currency}`, `{seller_external_id}`, `{buyer_external_id}` and `{return_url}`, all URL-encoded. `commerce.topup_url_template` takes the same placeholders for the purchase that ran short, and a `topup_url` from your callout wins over it. `commerce.payment_window` picks where these pages open: `popup` opens a window in the click and re-fetches the purchase and the balance when it closes, and `same_tab` sends `navigate` with reason `checkout` or `topup` and a return URL that reopens the conversation. A blocked popup falls back to `same_tab`.

6. Let a buyer start a purchase. The chat does it for you; from your own client, send `POST /v1/purchases` with exactly one of `item_id`, `offer_message_id` or `amount_minor`, plus an `Idempotency-Key`.

   ```sh
   curl -X POST $MC/purchases -H "Authorization: Bearer $SESSION" -H 'content-type: application/json' \
     -H 'Idempotency-Key: buy-7d6f3c0e' -d "{\"conversation_id\": \"$ID\", \"item_id\": \"photo-set\"}"
   ```

   The answer is the `Purchase` and `next: {type, url?}`. `type` is `done`, `page_callback` or `redirect`. Read it later with `GET /v1/purchases/{id}`. With `commerce.custom_amount.enabled`, a buyer may type an amount from `min_minor` to `max_minor` (a tip). It buys an item of kind `custom` through the same payment mode.

7. Let a seller post an offer with `POST /v1/conversations/{id}/offers` and `{title, description?, price_minor, currency}`. The answer is the offer card as a `Message`. Buying it uses the card as the item. Users never post cards any other way.

8. Record a purchase made outside the chat with `POST /v1/server/purchases`. It posts the buyer's card into the buyer and seller's conversation.

   ```sh
   curl -X POST $MC/server/purchases "${auth[@]}" -H 'Idempotency-Key: order-77' \
     -d '{"buyer": "alice", "seller": "bob", "item": {"title": "Photo set"}, "price_minor": 1500, "currency": "EUR", "external_ref": "order-77", "buyer_profile": {"name": "Alice", "role": "buyer"}}'
   ```

   - A repeat with the same `external_ref` returns the first purchase.
   - A user that does not exist yet is created from `buyer_profile` or `seller_profile`, which have the shape of the `PUT /v1/server/users/{external_id}` body. An existing user keeps their profile. A missing user with no profile answers 404.
   - With no conversation yet, one is opened, started by the buyer, and `conversation.created` is sent.
   - The card is delivered even when one of them blocked the other, because it records a payment.
   - A guest buyer or seller answers 422. A refused call creates no user.

9. Receive `purchase.updated`. It is sent when the status changes, with `{purchase_id, status, buyer, seller, price_minor, currency, external_ref}`.

10. Style the card and the shop. The card heads "You bought" for the buyer and "New purchase" for the seller, and names the item by the copy `commerce.item.<item.kind>` when you set one in `i18n.copy`, else by `item.title`. `commerce.trim_whole_prices` writes a euro price as `5 €` or `5.50 €`. `links.services_page` adds a Services entry for buyers, which sends `navigate` with reason `services` when you set no `links.services_url_template`.

## Check it works

1. Set `commerce.enabled` to true and a `payment_mode`, and give a buyer and a seller the policy rows.
2. As the buyer, buy an item in a conversation with the seller. In `server_charge`, your `charge` callout runs. In `page_callback`, your `onPurchase` runs.
3. Settle it as `paid`. One `purchase` card is in the conversation and your endpoint got a `purchase.updated` webhook.
4. Settle it again with the same `Idempotency-Key`. No second card appears.

## When it fails

| Answer | Cause |
|---|---|
| 403 `policy_denied`, reason `commerce` | `commerce.enabled` is off. |
| 403 `policy_denied`, reason `buy` or `offers` | The policy row from the user's role to the peer's has `buy` (catalog, purchase) or `offers` (offer) off. Reason `inbound` when the peer takes messages from members only and the user is a guest. |
| 403 `policy_denied`, reason `custom_amount` | The purchase gives `amount_minor` and `commerce.custom_amount.enabled` is off. An amount outside `min_minor` and `max_minor` answers 422. |
| 403 `policy_denied`, reason `checkout_url_template` | `payment_mode` is `checkout_url` and `commerce.checkout_url_template` is empty. |
| 403 `forbidden` | The buyer blocked the seller (reason `you_blocked`), or the seller blocked the buyer, which the buyer is not told. |
| 404 | The item is not in the catalog, the offer is not a sent offer card from the peer in this conversation, or the purchase is not one of the caller's. |
| 409 `conflict` | The catalog has no currency for a custom amount, or `confirm` hits a purchase already `paid` or `cancelled` with a different status. |
| 409 `idempotency_conflict` | The same `Idempotency-Key` came with a different body. |
| 404 | A missing user on `POST /v1/server/purchases` with no profile. |
| 422 | A guest buyer or seller, or a bad body. |
| 502 `upstream_failed` | No endpoint answers callouts, or your `catalog` callout failed or answered badly. A failed `charge` callout does not answer 502: the purchase stays `pending`. |
| Purchase stays `pending` | Your `charge` callout timed out or answered `pending`. Answer `charge_status` with a final status. |

## Limits

- Your callout answers within 5 seconds, as 2xx JSON under 64 KB. A network error is retried once, nothing else.
- `server_charge` checks a pending purchase at 30 seconds, 2 minutes, 10 minutes and 1 hour.
- Server calls that take `Idempotency-Key`: `/server/purchases` and `/server/purchases/{id}/confirm`. A repeat within 24 hours returns the first answer with `Idempotent-Replayed: true`.
- Prices are in minor units, so 1500 is 15.00.
- A `custom_amount` is in the catalog's currency.
- mondochat does not hold balances, refund, or pay out.

## Related topics

- [Get the catalog](/api/operations/get-catalog/), [create a purchase](/api/operations/create-purchase/), [get a purchase](/api/operations/get-purchase/), [create an offer](/api/operations/create-offer/)
- [Confirm a purchase](/api/operations/confirm-purchase/), [record a purchase](/api/operations/record-purchase/)
- [catalog callout](/api/webhooks/catalog/), [charge callout](/api/webhooks/charge/), [charge_status callout](/api/webhooks/charge-status/), [purchase.updated webhook](/api/webhooks/purchase-updated/)
- [Webhooks overview](/webhooks/overview/)
- [Settings reference](/reference/settings/) for `commerce`, `links` and `policy`
- [Errors](/errors/)