Skip to content

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.

  • Roles: a seller has seller: true in your roles. See 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 and Verify signatures.
Terminal window
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')
  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:

    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' }),
    })

    status is paid, failed or cancelled. The answer is the Purchase. See 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.

    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 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.

    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_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.

  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.
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.
  • 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.