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

# AMB commerce: interactive flows and Apple Pay checkout

> Walk Apple Messages for Business commerce end to end — send an interactive list-picker, fold the customer's pick into the omni-cart, request Apple Pay natively in the thread, capture the token, and reconcile the order.

# AMB Commerce: Interactive Flows and Apple Pay Checkout

The [onboarding guide](/guides/apple-messages-for-business-onboarding) gets an Apple Messages for Business (AMB) agent registered and approved. This guide picks up where onboarding stops: what you can build **inside the thread** once the agent is live. It walks the AMB-specific commerce loop end to end — interactive list-picker messages, the customer's pick folding into your cart, the native Apple Pay request, and the checkout resolution route that anchors everything to a reconciled order.

**You will:**

1. [Understand the AMB commerce loop](#1-the-amb-commerce-loop)
2. [Author and send an interactive flow](#2-author-and-send-an-interactive-flow)
3. [Request Apple Pay in the thread](#3-request-apple-pay-in-the-thread)
4. [Handle the result: token capture and the retry loop](#4-handle-the-result)
5. [Run the worked example end to end](#5-worked-example-end-to-end)
6. [Place AMB against the channel-agnostic guides](#6-where-amb-fits-next-to-the-channel-agnostic-guides)
7. [Know the limits](#7-limits)

All requests go to two base prefixes:

```
https://api.orbit.devotel.io/api/v1/messages
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.

Prerequisite: the agent must be registered and `approved` — follow the [onboarding guide](/guides/apple-messages-for-business-onboarding) first. Register it with the `interactive` capability set on the agent's `capabilities` object, and with `applePay: true` when you intend to request Apple Pay.

## 1. The AMB commerce loop

AMB commerce is a conversation, not a page. Compared to a web checkout:

* **Interactive messages replace product grids.** Instead of rendering a category page you send the customer an interactive message inside Messages: a **list picker** (sections of items, single or multiple selection), a **time picker**, a **rich link** with an inline preview, or a **form** (Apple's dynamic-form JSON). The customer answers with a tap; the reply lands on your webhook as structured data.
* **Quick replies are plain taps.** A list picker's `receivedMessage` and `replyMessage` blocks define what bubbles the customer sees before and after they pick — the wire is provider-specific JSON, but the interaction model is the same one WhatsApp quick replies use.
* **Apple Pay replaces the hosted pay page where the business is eligible.** When your AMB agent carries the `applePay` capability and you have a merchant identifier from Apple Business Register, the pay step surfaces as a **payment sheet inside the thread** — the customer authorizes with Face ID, no browser, no redirect.
* **The cart is one object across channels.** AMB picks fold into the same persistent **omni-cart** a WhatsApp catalog order or an RCS carousel postback joins — Apple Pay in AMB is one of the folds that cart closes through.

The loop in one line: **interactive message → customer taps → cart merges the pick → checkout resolves (native Apple Pay or hosted link) → payment reconciled server-side**.

## 2. Author and send an interactive flow

An interactive AMB message travels as a normal send with a `msgType` and a provider-specific JSON blob in `metadata`. The shape is Apple's Messages Business Chat envelope — author the JSON once, validate it against the shapes below, and reuse it.

### The list-picker JSON

The workhorse shape for product selection is the list picker:

```json theme={null}
{
  "receivedMessage": { "title": "Pick your size", "subtitle": "Tap a size to add it to your order" },
  "replyMessage": { "title": "Selected" },
  "sections": [
    {
      "title": "Running shoes",
      "multipleSelection": false,
      "items": [
        { "identifier": "SKU-1042", "title": "Acme Runner 42", "subtitle": "Blue · €89", "style": "default" },
        { "identifier": "SKU-1043", "title": "Acme Runner 43", "subtitle": "Blue · €89" }
      ]
    }
  ]
}
```

| Field                              | Rule                                                                                                                                                    |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `receivedMessage` / `replyMessage` | Required. The bubble the customer sees pre- and post-pick.                                                                                              |
| `sections[]`                       | One or more. `multipleSelection: true` allows multi-tap; keep it `false` for a single-product pick.                                                     |
| `items[].identifier`               | The catalog's retailer/product id. This is the value that later becomes `productRetailerId` at checkout — use your canonical SKU, not a display string. |
| `items[].title`                    | The visible label; `subtitle` carries price/spec context.                                                                                               |

### Validation gates

Catch these before production traffic does:

* `metadata.listPickerJson` must parse as JSON and match the shape above — a send without it is rejected with `422 metadata.listPickerJson is required for AMB interactive messages`. Store the JSON as a **template string**, not hand-pasted bodies per campaign.
* The same gate applies to the other interactive kinds: `formJson` must conform to Apple's dynamic-form schema, `richLinkJson` needs at least `{ url, title }`, and `timePickerJson` needs an `event` with at least one timeslot.
* The agent's capability list governs what it can carry — an agent registered without `interactive: true` should not be targeted by an interactive template.

### Save it as a template, assign it, send it

Keep the picker JSON next to your other channel templates (dashboard: **Flows** / **Templates**, or your own versioned template store) — treat it as campaign content, not code. Assign it to the campaign, journey step, or agent reply that starts the flow, exactly as you would assign a WhatsApp template.

Send it over the unified messages surface, with the picker in `metadata` (values are strings — the JSON goes in serialized):

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "amb",
    "to": "c_9f8b2e",
    "metadata": {
      "businessId": "com.example.support",
      "conversationId": "c_9f8b2e",
      "msgType": "interactive",
      "listPickerJson": "{\"receivedMessage\":{\"title\":\"Pick your size\",\"subtitle\":\"Tap a size to add it to your order\"},\"replyMessage\":{\"title\":\"Selected\"},\"sections\":[{\"title\":\"Running shoes\",\"multipleSelection\":false,\"items\":[{\"identifier\":\"SKU-1042\",\"title\":\"Acme Runner 42\",\"subtitle\":\"Blue · €89\"},{\"identifier\":\"SKU-1043\",\"title\":\"Acme Runner 43\",\"subtitle\":\"Blue · €89\"}]}]}"
    }
  }'
```

`conversationId` is the thread the customer opened inbound — AMB is customer-initiated, so the picker always answers a conversation that already exists; the customer-facing body is carried by `receivedMessage.title`, not a separate `body` field.

The customer's tap arrives on your registered AMB webhook as an inbound `interactive` message with a structured `interactiveData` payload carrying the picked item's `identifier`. That identifier is the join key into everything below.

## 3. Request Apple Pay in the thread

Once the pick is in the cart, decide **how** the customer pays. The decision is per-business, not per-buyer — unlike WhatsApp Pay's buyer-region rails, Apple Pay eligibility is a *merchant* capability: the business must carry the `applePay` capability AND have a merchant identifier configured.

### The omni-cart fold

AMB picks do not live in an AMB-only state — they merge into the same cart object every other channel feeds. Create the cart once per customer, then merge the pick the webhook surfaced:

```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_amb_7361" }'
```

```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 '{
    "cartId": "cart_amb_7361",
    "items": [
      { "productId": "SKU-1042", "name": "Acme Runner 42", "quantity": 1, "unitPrice": 89, "currency": "EUR" }
    ]
  }'
```

Merge the **identifier the webhook returned** — never a client-rendered label — and value prices from your catalog server-side, not from anything the customer device sent.

### Native Apple Pay vs the deferred hosted link

Resolve the checkout:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/commerce/amb-checkout \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "order": {
      "items": [
        { "productRetailerId": "SKU-1042", "name": "Acme Runner 42", "quantity": 1, "unitPrice": 89, "currency": "EUR" }
      ],
      "subtotal": 89,
      "currency": "EUR"
    },
    "config": {
      "checkoutId": "chk_amb_7361",
      "hostedBaseUrl": "https://pay.example.com",
      "applePayEnabled": true,
      "merchantIdentifier": "merchant.com.example.acme",
      "description": "Order #7361"
    }
  }'
```

The response decides the method you actually use:

```json theme={null}
{
  "method": "native",
  "currency": "EUR",
  "subtotal": 89,
  "native": {
    "type": "apple_pay_request",
    "referenceId": "chk_amb_7361",
    "merchantIdentifier": "merchant.com.example.acme",
    "currency": "EUR",
    "total": 89,
    "items": [
      { "retailerId": "SKU-1042", "label": "Acme Runner 42", "quantity": 1, "amount": 89 }
    ],
    "fallbackUrl": "https://pay.example.com/chk_amb_7361"
  },
  "hosted": {
    "type": "hosted_link",
    "body": "Pay €89.00 for Order #7361: https://pay.example.com/chk_amb_7361",
    "url": "https://pay.example.com/chk_amb_7361"
  },
  "paymentRequest": { "id": "chk_amb_7361", "status": "requested", "currency": "EUR", "amount": 89 }
}
```

Two paths out of one call:

* **`method: "native"`** — the agent has `applePay` enabled and `merchantIdentifier` set. Present the `native` object to the customer as the Apple Pay request: the merchant identifier, line items, and the decimal total (Apple's line items take *major* units, unlike Meta's minor-unit WhatsApp payload). Apple renders the payment sheet **inside the thread**; the customer authorizes with Face ID. The sheet's request and the PSP result both reconcile against `referenceId` — which is the same id as the minted `paymentRequest`.
* **`method: "hosted"`** — any of: `applePayEnabled` unset, no `merchantIdentifier`, or you deliberately send the link. `native` is `null`; send `hosted.url` (or the pre-rendered `hosted.body`) into the thread, and the customer completes on your hosted pay page.

The **returning-token vs deferred-authorization** split is entirely on the Apple Pay path: a native request asks Apple for the payment sheet *now*, and the customer either authorizes (you receive a token — below) or cancels/retries; the hosted path defers authorization to the hosted page's own flow, and you reconcile on `paymentRequest.status` transitions instead.

## 4. Handle the result

### The native Apple Pay token

When the customer authorizes the sheet, Apple hands back a payment token (the `PKPayment` representation) addressed to your PSP. Forward it to the PSP exactly as you would a web Apple Pay token, then reconcile against the id the response minted:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/commerce/payment-request/reconcile \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "chk_amb_7361",
    "amount": 89,
    "currency": "EUR",
    "reference": "psptxn_5h2k9"
  }'
```

Amount and currency must match the snapshot **exactly** — reconciliation refuses a forged subtotal, which is why totals are always re-derived server-side (see [conversational checkout](/guides/conversational-commerce-checkout#4-reconcile-the-payment-server-side) for the reconciliation semantics). Your order service flips the order to `paid` on the reconcile's success surface, and the cart closes via `POST /commerce/cart/transition`.

### The decline / retry / fallback loop

* **Authorization declined at the PSP → retry in-thread.** Keep `referenceId` stable while you invite the customer to retry the sheet; mint a fresh `checkoutId` (a new `POST /commerce/amb-checkout`) only once the retry budget for that reference is spent — a new id is a new payment request, i.e. a clean after failure.
* **The customer cancels the sheet → offer the hosted fallback.** The `native` payload always carries `fallbackUrl`, and the response always carries the full `hosted` block — send `hosted.body` into the same thread and no state is lost: both paths reconcile to the same `paymentRequest`, so which one the customer completed does not fork your order logic.
* **Publication failure of the native shape → degrade to hosted.** If the interactive Apple Pay request fails to render on an older device, the same rule applies — the hosted link is never absent from a resolved checkout.

## 5. Worked example end to end

A reachable end-to-end sequence you can copy against a sandbox key:

1. **Register + enable.** The [onboarding guide](/guides/apple-messages-for-business-onboarding) registers the agent; register it with `capabilities: { "text": true, "interactive": true, "applePay": true }` and poll until `status: "approved"`. Note the agent id (`data.id`).
2. **Send the list picker.** `POST /api/v1/messages` with `channel: "amb"`, `metadata.businessId` = your Apple `business_id`, `metadata.conversationId` = the thread, `metadata.msgType: "interactive"`, and `metadata.listPickerJson` = the picker JSON from §2.
3. **Read the pick.** The webhook delivers an inbound `interactive` message; read the chosen `identifier` out of `interactiveData` (here: `SKU-1042`).
4. **Merge into the cart.** `POST /api/v1/commerce/cart` once, then `/cart/merge` the picked SKU with the server-side price.
5. **Resolve the checkout.** `POST /api/v1/commerce/amb-checkout` — with `applePayEnabled: true` + `merchantIdentifier` set, the response is `method: "native"` and the `native` block carries the sheet payload keyed to `referenceId`.
6. **Request the sheet in-thread.** Present the `native` Apple Pay request in the conversation; the customer authorizes with Face ID.
7. **Forward the token, reconcile.** `POST /api/v1/commerce/payment-request/reconcile` with the PSP transaction reference → the order service marks the order `paid`.
8. **On a decline/cancel, fall back.** Send `hosted.body` into the same thread — same `paymentRequest`, nothing re-created — and let the customer complete on the hosted link.

## 6. Where AMB fits next to the channel-agnostic guides

* [Conversational commerce rails](/guides/conversational-commerce-rails) covers the **channel-agnostic loop**: persistent cart, checkout-channel selection, reconciliation. This guide adds only the AMB-specific part on top — the interactive message envelope and the Apple Pay shortcut that lets an eligible business skip the hosted page.
* [Conversational commerce checkout](/guides/conversational-commerce-checkout) walks the generic purchase (WhatsApp-led). AMB substitutes for the WhatsApp leg and adds one fold the generic guide leaves as theory: native Apple Pay where the business carries the capability.
* The [RCS checkout](/guides/rcs-onboarding) analogue is the *hosted fallback* — RCS has no native rail, so its checkout always mints the link AMB only falls back to.

Use this guide for the AMB-specific envelope + the Apple Pay branch; use the two channel-agnostic guides for everything cart/reconciliation semantics do not change per channel.

## 7. Limits

* **Onboarding first.** Apple Pay only behaves after the agent is registered and `approved` — the [onboarding guide](/guides/apple-messages-for-business-onboarding) is a hard prerequisite, and any attempt against a `pending` agent is a no-op.
* **Capability and identifier are both mandatory for native.** `checkout…applePayEnabled: true` without a `merchantIdentifier`, or vice versa, silently resolves to `method: "hosted"` — check both on the agent record before promising the sheet to the customer.
* **The provider shape is Apple-specific.** `listPickerJson` / `formJson` / `richLinkJson` / `timePickerJson` are Apple Messages Business Chat envelopes — they will NOT render on WhatsApp, RCS, or web chat; per-channel templates remain per-channel.
* **AMB is customer-initiated.** You answer an open conversation; you cannot launch a picker into a thread that never started — entry points stay Apple-side (Maps, Spotlight, Safari, Siri, an untruncated link).
* **Line items are decimal amounts.** Apple's items/total take **major** units (e.g. `89` for €89), unlike the minor-unit WhatsApp order payload — do not feed one shape into the other's builder.
* **Failed validations are loud.** A missing/invalid interactive blob is a `422` naming the metadata key; treat the send as rejected, fix the template, and resend — nothing queues.
