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
Section titled “Before you start”- Roles: a seller has
seller: truein yourroles. See Tenants and apps. - Policy: the row from the buyer’s role to the seller’s role has
buyon. A seller may post offers when the row from the seller to the buyer hasofferson. A guest cannot buy unless the row for their role hasbuyon. - Settings:
commerce.enabledis on. It is off by default. - An endpoint marked Answers callouts if you use the
webhookcatalog orserver_charge. See Webhooks overview and Verify signatures.
export MC=https://app.mondochat.io/v1export SESSION=<session token>auth=(-H "Authorization: Bearer $MONDOCHAT_SERVER_SECRET" -H "Mondochat-App-Id: $MONDOCHAT_APP_ID" -H 'content-type: application/json')-
Choose where the catalog comes from with
commerce.catalog_source.webhook: mondochat calls yourcatalogcallout 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
catalogcallout 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 whencommerce.show_balanceis on, andlabelnames the unit, for example Credits. Read the catalog yourself withGET /v1/conversations/{id}/catalog. -
Choose a payment mode with
commerce.payment_mode.Mode What happens server_chargemondochat calls your chargecallout and gets the answer at once. Answer{status: paid | insufficient | failed | pending, external_ref?, topup_url?}.page_callbackThe chat tells your page, which takes the payment and then confirms through the server API. checkout_urlThe buyer is sent to checkout_url_template, and your server confirms afterwards.In every mode a paid purchase posts one
purchasecard to the conversation and publishespurchase.updated. -
With
server_charge, handle thechargecallout. It sends{purchase_id, buyer, seller, item: {id, kind, title, description}, price_minor, currency}with anIdempotency-Keyof the purchase id. Charge each purchase once.paidmarks the purchase paid.insufficientsends the buyer to your top-up page: thetopup_urlyou answer, elsecommerce.topup_url_template. With neither, the frame sendsnavigateto the host with reasontopup, so your site opens its own top-up.failedmarks the purchase failed.pending, a timeout or a failed callout leaves itpending. mondochat then asks yourcharge_statuscallout, which sends{purchase_id}and takes the same answer ascharge, after 30 seconds, 2 minutes, 10 minutes and 1 hour.
-
With
page_callback, take the payment on your page. The embed calls youronPurchasewith thePurchase. Resolve with{status: 'paid' | 'failed' | 'cancelled' | 'pending', external_ref?}. A rejected promise counts asfailed. The browser’s answer only changes what the buyer sees. The purchase is settled when your server confirms it:Terminal window curl -X POST $MC/server/purchases/$PURCHASE_ID/confirm "${auth[@]}" \-H "Idempotency-Key: confirm-$PURCHASE_ID" \-d '{"status": "paid", "external_ref": "order-1234"}'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' }),})statusispaid,failedorcancelled. The answer is thePurchase. See Embed. -
With
checkout_url, fillcommerce.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_templatetakes the same placeholders for the purchase that ran short, and atopup_urlfrom your callout wins over it.commerce.payment_windowpicks where these pages open:popupopens a window in the click and re-fetches the purchase and the balance when it closes, andsame_tabsendsnavigatewith reasoncheckoutortopupand a return URL that reopens the conversation. A blocked popup falls back tosame_tab. -
Let a buyer start a purchase. The chat does it for you; from your own client, send
POST /v1/purchaseswith exactly one ofitem_id,offer_message_idoramount_minor, plus anIdempotency-Key.Terminal window 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
Purchaseandnext: {type, url?}.typeisdone,page_callbackorredirect. Read it later withGET /v1/purchases/{id}. Withcommerce.custom_amount.enabled, a buyer may type an amount frommin_minortomax_minor(a tip). It buys an item of kindcustomthrough the same payment mode. -
Let a seller post an offer with
POST /v1/conversations/{id}/offersand{title, description?, price_minor, currency}. The answer is the offer card as aMessage. Buying it uses the card as the item. Users never post cards any other way. -
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.Terminal window 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_refreturns the first purchase. - A user that does not exist yet is created from
buyer_profileorseller_profile, which have the shape of thePUT /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.createdis 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.
- A repeat with the same
-
Receive
purchase.updated. It is sent when the status changes, with{purchase_id, status, buyer, seller, price_minor, currency, external_ref}. -
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 ini18n.copy, else byitem.title.commerce.trim_whole_priceswrites a euro price as5 €or5.50 €.links.services_pageadds a Services entry for buyers, which sendsnavigatewith reasonserviceswhen you set nolinks.services_url_template.
Check it works
Section titled “Check it works”- Set
commerce.enabledto true and apayment_mode, and give a buyer and a seller the policy rows. - As the buyer, buy an item in a conversation with the seller. In
server_charge, yourchargecallout runs. Inpage_callback, youronPurchaseruns. - Settle it as
paid. Onepurchasecard is in the conversation and your endpoint got apurchase.updatedwebhook. - Settle it again with the same
Idempotency-Key. No second card appears.
When it fails
Section titled “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
Section titled “Limits”- Your callout answers within 5 seconds, as 2xx JSON under 64 KB. A network error is retried once, nothing else.
server_chargechecks a pending purchase at 30 seconds, 2 minutes, 10 minutes and 1 hour.- Server calls that take
Idempotency-Key:/server/purchasesand/server/purchases/{id}/confirm. A repeat within 24 hours returns the first answer withIdempotent-Replayed: true. - Prices are in minor units, so 1500 is 15.00.
- A
custom_amountis in the catalog’s currency. - mondochat does not hold balances, refund, or pay out.