> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Conversational Commerce Checkout

> Walk a customer from a WhatsApp product message to a paid order — one persistent cart across channels, native in-thread checkout where available, a hosted pay-by-link fallback everywhere else, and server-side reconciliation.

# Conversational Commerce Checkout

This guide walks a purchase end to end: a customer picks a product in WhatsApp, the order folds into one persistent cart, checkout resolves to a native in-thread payment where possible (falls back to a hosted pay-by-link otherwise), and the captured payment reconciles server-side against the cart. It closes with the abandonment hook you can use to re-engage a stalled checkout.

**You will:**

1. [See what commerce ships](#1-what-commerce-ships)
2. [Take a WhatsApp product into the cart](#2-take-a-whatsapp-product-into-the-cart)
3. [Resolve the checkout channel — native pay or hosted pay-by-link](#3-resolve-the-checkout-channel)
4. [Reconcile the payment with server-side totals](#4-reconcile-the-payment-server-side)
5. [Check for abandonment and re-engage](#5-check-for-abandonment-and-re-engage)
6. [Go further: mandates, carrier billing, pay links, subscriptions](#6-go-further)

All requests go to a single base URL:

```
https://api.orbit.devotel.io/api/v1/commerce
```

Every request carries your key in the `X-API-Key` header (or a session JWT). Use a sandbox key (`dv_test_sk_…`) while you build; swap in a live key (`dv_live_sk_…`) for production.

## 1. What commerce ships

Commerce gives you one checkout spine instead of per-channel islands:

* **One persistent cart across channels** — contributions from WhatsApp catalog orders, RCS catalog carousel postbacks, Apple Messages for Business (AMB), and Instagram DM product shares fold into a single cart snapshot. A customer who adds two items on WhatsApp and one over RCS checks out once, on one cart.

* **Native in-thread checkout where it exists** — WhatsApp Pay where Meta has rolled payments out (India UPI, Brazil Pix, Singapore PayNow/cards), Apple Pay inside AMB. The customer pays without leaving the conversation.

* **Hosted pay-by-link fallback** — when `checkout-channel` returns no native option, Orbit mints a channel-agnostic hosted checkout URL. Render it for SMS, email, Telegram, Viber, or a voice-IVR readout with a QR code; it also opens cleanly in WhatsApp's in-app browser.

* **AI-shopping storefronts** — the ACP/UCP protocol surface third-party shopping agents (e.g. ChatGPT Instant Checkout, Google AP2-style agents) drive against your catalog, priced server-side.

* **Agent payment mandates** — scoped, revocable, spend-capped consent that lets an AI agent complete a purchase on the customer's behalf without re-prompting.

Every handler re-derives totals server-side, so a client-forged subtotal can never pass reconciliation. Every snapshot (cart, payment request, mandate) is a serializable object your application round-trips in the request body — the API computes the next valid state; you persist it.

## 2. Take a WhatsApp product into the cart

Create once, then fold channel contributions in:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/commerce/cart \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "cartId": "cart_whatsapp_9842" }'
```

The response is the cart snapshot in the `browsing` state. When a WhatsApp order arrives (catalog or interactive product message), merge it — value the `items` from the validated inbound payload, not from a client-rendered page:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/commerce/cart/merge \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cart": <previous cart snapshot>,
    "channel": "whatsapp",
    "contribution": {
      "items": [
        {
          "productRetailerId": "sku_chai_500g",
          "name": "Chai Blend 500g",
          "quantity": 2,
          "unitPrice": 12.5,
          "currency": "USD"
        }
      ]
    }
  }'
```

Same-line contributions from different channels merge by product id (`productRetailerId`) and re-derive `subtotal`, `itemCount`, and `lineTotal` server-side. The first item moves the cart from `browsing` to `cart_active`; the returned snapshot is the source of truth — persist it verbatim.

Move the buyer toward payment by advancing the state machine. Illegal transitions reject with `VALIDATION_ERROR`, so state bugs surface as 422s instead of dead carts:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/commerce/cart/transition \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cart": <cart snapshot>,
    "event": "INITIATE_CHECKOUT"
  }'
```

## 3. Resolve the checkout channel

Ask Orbit for the ordered checkout-channel preference, restricted to the channels your tenant has connected and marked commerce-capable:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/commerce/cart/checkout-channel \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cart": <cart snapshot>,
    "capableChannels": ["whatsapp", "rcs", "amb", "instagram"]
  }'
```

The response's `channels` is an ordered preference — the buyer's last channel leads, then the platform default order (WhatsApp, AMB, RCS, Instagram). An **empty array means no channel can take the payment natively: fall back to a hosted pay-by-link.**

**Native in-thread checkout.** For a WhatsApp buyer, call `POST /commerce/whatsapp-checkout`; for RCS, `POST /commerce/rcs-checkout`. The adapter decides per buyer region and your tenant config between the native rail and the hosted fallback, and returns either a native `order_details` payload or a rendered hosted link — both mint the same `PaymentRequest` snapshot that one reconciliation endpoint closes out.

**Hosted pay-by-link.** Mint one with `POST /commerce/payment-request`, then render it for the customer's channel:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/commerce/payment-request \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "req_chai_9842",
    "amount": 25.0,
    "currency": "USD",
    "hostedBaseUrl": "https://pay.yourshop.example",
    "description": "Chai Blend 500g ×2"
  }'
```

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/commerce/payment-request/render \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "request": <payment request snapshot>,
    "channel": "whatsapp"
  }'
```

Hand the rendered content to your per-channel send infrastructure (the Commerce API produces the link content; it does not send). Send it with the same call you'd make for any text — for WhatsApp, `POST /messages/whatsapp`.

## 4. Reconcile the payment server-side

Capture the payment, then close out one transaction with one reconciliation call. Because totals are re-derived server-side, a forged subtotal never passes — match on amount + currency, not on channel. A checkout begun on WhatsApp can be paid via Apple Pay on AMB; reconciliation is cross-channel by design.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/commerce/cart/reconcile-payment \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cart": <cart snapshot>,
    "payment": {
      "amount": 25.0,
      "currency": "USD",
      "channel": "whatsapp",
      "reference": "psp_txn_8842a"
    }
  }'
```

On an exact match the response returns `matched: true`:

```json theme={null}
{
  "matched": true,
  "delta": 0,
  "reason": "ok",
  "reference": "psp_txn_8842a",
  "channel": "whatsapp"
}
```

On a mismatch the response says why — `empty_cart`, `mixed_currency`, `currency_mismatch`, or `amount_mismatch` — with `delta` carrying the signed difference (payment minus subtotal). A mismatch is terminal for that payment: refund or retry; do not advance the cart. When the payment was captured through a pay-by-link, close out `POST /commerce/payment-request/reconcile` with its snapshot instead; the snapshot advances to `paid` on an exact match.

After a matched payment, advance the cart:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/commerce/cart/transition \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "cart": <cart snapshot>, "event": "PAYMENT_CONFIRMED" }'
```

## 5. Check for abandonment and re-engage

The state machine treats `abandoned` as recoverable, not terminal. Check whether a cart has gone idle while still holding items in a pre-payment state:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/commerce/cart/abandonment-check \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cart": <cart snapshot>,
    "idleThresholdMs": 1800000
  }'
```

`idleThresholdMs` defaults to 30 minutes (capped at 30 days) — omit it to use the default. The response returns `abandoned: true`, the measured `idleMs`, and the current `state`.

When it comes back true, send a recovery nudge — a WhatsApp message with a rendered pay-by-link, or a catalog re-offer — then resume the cart so the follow-up merge resumes from the saved state, not a new cart:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/commerce/cart/transition \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "cart": <cart snapshot>, "event": "RESUME" }'
```

## 6. Go further

The Commerce API also covers the surfaces the guide above deliberately skipped:

* **Agent payment mandates** (`/agent-mandate` and `/mpp-session`) — scoped, revocable, spend-capped consent for AI-agent purchases, plus metered machine-to-machine sessions.
* **Direct carrier billing** (`/dcb/*`) — charge to the customer's mobile carrier bill instead of a card.
* **Hosted pay links** (`/payment-request/*`) — the full lifecycle of the fallback link.
* **Subscriptions** (`/subscriptions/*`) — recurring "subscribe & save" renewals that reuse the customer's mandate.
* **AI-shopping storefronts** (`/agentic/*`, `/ucp/*`) — the protocol surface for third-party shopping agents.

## Cross-link

In the dashboard, open **Messages → Commerce** (`/messages/commerce`) for the commerce hub — the agent-mandate ledger, carrier-billing settlement, hosted pay links, AI storefront, and subscription schedule surfaces in one place. Use hub pages to watch what the API calls above mutate: pay links render their captured status, subscriptions show next-charge dates, and mandates show spend against caps.

## See also

* [Commerce API reference](/api-reference/commerce) — every endpoint and field
* [WhatsApp channel](/channels/whatsapp) — the native catalog and checkout surface
* [RCS channel](/channels/rcs) — the catalog carousel surface
* [Messaging API](/api-reference/endpoints/messaging) — sending the rendered pay-by-link
