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

# Apple Messages for Business

> Connect Apple Messages for Business to Devotel Orbit for customer-initiated conversations, rich interactions, and Apple Pay checkout in Messages.

# Apple Messages for Business

Apple Messages for Business (AMB) lets customers start a conversation with your business from Apple surfaces such as **Apple Maps, Spotlight, Safari, and Siri**. The conversation opens in the Messages app, where your team can reply from the unified inbox.

AMB is a business channel, not person-to-person iMessage. Customers message an approved business identity; they do not use Orbit to send or receive private iMessages. In Orbit, an **AMB agent** is the sender identity that connects an Apple Business Register `business_id` to your tenant. The agent's approval status and capabilities determine which interactions you can use.

## Capability matrix

| Capability | Orbit channel API | Apple-side handoff |
| - | - | - |
| Text replies | Send through the unified messages API with `channel: "amb"`. | Apple renders the message in the customer's Messages thread. |
| Rich links | Send an AMB rich-link payload when the agent has `richLink` enabled. | Apple renders the link preview and controls its device presentation. |
| List pickers | Send provider-specific `listPickerJson` in message metadata when `interactive` is enabled. | The customer selects an item in Messages; the structured selection returns through the AMB webhook. |
| Forms | Send an AMB `formJson` payload when `form` is enabled. | Apple renders the form and returns the submitted values to your webhook. |
| Time pickers | Send `timePickerJson` when `timePicker` is enabled. | Apple displays available times and returns the customer's selection. |
| Apple Pay | Resolve an AMB checkout with the tenant's `applePay` capability and merchant identifier. | Apple presents the payment sheet. Your payment service receives and reconciles the resulting token. |
| Commerce checkout | Use the AMB checkout and shared cart APIs. | The customer completes payment in Apple Pay or follows the hosted-link fallback. |

Apple-specific payloads are not interchangeable with WhatsApp, RCS, or web-chat payloads. Keep each channel's template and capability checks separate.

## Onboarding at a glance

Before you create an agent, complete Apple's approval process and collect the values Orbit needs:

1. Apply for approval in [Apple Business Register](https://register.apple.com/business).
2. Obtain the approved business's `business_id`.
3. Confirm the Messaging Service Provider (MSP) arrangement and `msp_id`, if you use one.
4. Obtain the business HMAC secret key. Orbit encrypts the key at rest and only exposes whether a secret is stored.
5. Create the agent in **Messages → Apple** or with `POST /api/v1/channels/amb/agents`.
6. Wait for the agent status to become `approved` before routing live conversations.

Follow [Apple Messages for Business onboarding](/guides/apple-messages-for-business-onboarding) for the dashboard wizard, request bodies, status polling, and secret rotation. Agent records and credentials are tenant-scoped; set only the capabilities and business identities your tenant owns.

### Create an AMB agent

Register the agent that binds your Apple `business_id` to this tenant. Pass the `business_id` Apple issued, the `msp_id` when you bring your own Messaging Service Provider, the base64 HMAC `secret_key`, and the `capabilities` array the agent may use.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.orbit.devotel.io/api/v1/channels/amb/agents \
    -H "X-API-Key: dv_live_sk_..." \
    -H "Content-Type: application/json" \
    -d '{
      "business_id": "com.example.support",
      "display_name": "Acme Support",
      "secret_key": "b2HHqW1YxQ==...",
      "msp_id": "orbit-cpaas",
      "logo_url": "https://example.com/logo.png",
      "capabilities": ["text", "interactive", "applePay"]
    }'
  ```

  ```typescript Node.js theme={null}
  import { Devotel } from '@devotel-orbit/node';

  const orbit = new Devotel({ apiKey: process.env.ORBIT_API_KEY });

  // The typed AMB agent resource wraps POST /channels/amb/agents.
  const agent = await orbit.channels.amb.agents.create({
    business_id: 'com.example.support',
    display_name: 'Acme Support',
    secret_key: process.env.AMB_SECRET_KEY,
    msp_id: 'orbit-cpaas',
    logo_url: 'https://example.com/logo.png',
    capabilities: ['text', 'interactive', 'applePay'],
  });

  console.log(agent.data.id);        // e36f1a2b-…  — address the agent by this id
  console.log(agent.data.status);    // 'pending'     — not cleared for live traffic yet
  ```

  ```python Python theme={null}
  from orbit_sdk import OrbitClient

  client = OrbitClient.from_env()  # reads ORBIT_API_KEY

  # No typed AMB agent helper in the Python SDK — reach the endpoint
  # through the client.request escape hatch (raw JSON returned).
  agent = client.request(
      "POST",
      "/channels/amb/agents",
      json_body={
          "business_id": "com.example.support",
          "display_name": "Acme Support",
          "secret_key": os.environ["AMB_SECRET_KEY"],
          "msp_id": "orbit-cpaas",
          "logo_url": "https://example.com/logo.png",
          "capabilities": ["text", "interactive", "applePay"],
      },
  )

  print(agent["data"]["id"])      # e36f1a2b-…
  print(agent["data"]["status"])  # 'pending'
  ```
</CodeGroup>

A fresh registration returns `201` with the agent record at `status: "pending"`. Keep `data.id` — the opaque Orbit UUID — because every later call addresses the agent by that id, never by the Apple `business_id`.

### Poll until the agent is approved

Apple clears the agent on their side; Orbit reflects that as `status: "approved"`. Poll the agent (or the list endpoint) with backoff until the transition lands — do not send live traffic against a `pending` agent.

<CodeGroup>
  ```bash curl theme={null}
  # Re-fetch the agent until status flips to approved.
  curl https://api.orbit.devotel.io/api/v1/channels/amb/agents/e36f1a2b-... \
    -H "X-API-Key: dv_live_sk_..."
  # { "data": { "id": "e36f1a2b-…", "status": "approved", ... } }
  ```

  ```typescript Node.js theme={null}
  import { Devotel } from '@devotel-orbit/node';

  const orbit = new Devotel({ apiKey: process.env.ORBIT_API_KEY });
  const agentId = 'e36f1a2b-…';

  // Exponential backoff until Apple clears the agent.
  function sleep(ms: number) {
    return new Promise((r) => setTimeout(r, ms));
  }

  let delay = 5_000;  // start at 5s, double up to 60s
  let status = 'pending';
  while (status !== 'approved') {
    const agent = await orbit.channels.amb.agents.get(agentId);
    status = agent.data.status;
    if (status === 'approved') break;
    if (status === 'suspended') throw new Error(`Agent ${agentId} suspended — repair before sending`);
    await sleep(delay);
    delay = Math.min(delay * 2, 60_000);
  }
  console.log('Agent approved:', agentId);
  ```

  ```python Python theme={null}
  import os, time
  from orbit_sdk import OrbitClient

  client = OrbitClient.from_env()
  agent_id = "e36f1a2b-…"

  # Backoff poll on the list endpoint — re-fetch the agent row.
  delay = 5
  while True:
      agent = client.request("GET", f"/channels/amb/agents/{agent_id}")
      status = agent["data"]["status"]
      if status == "approved":
          break
      if status == "suspended":
          raise RuntimeError(f"Agent {agent_id} suspended — repair before sending")
      time.sleep(delay)
      delay = min(delay * 2, 60)

  print("Agent approved:", agent_id)
  ```
</CodeGroup>

Known `status` values are `pending` (saved, not cleared), `approved` (cleared for live AMB traffic), and `suspended` (taken down — repair or re-register before the next send). Treat any status regression as out-of-routing-set.

## Sending and receiving

Use the unified send surface for an approved agent. A basic AMB message identifies the channel and the Apple business in metadata. The same send ships as a typed `messages.send` call where the channel string is `amb` — the per-channel body shape carries `businessId` in `metadata`.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.orbit.devotel.io/api/v1/messages \
    -H "X-API-Key: dv_live_sk_..." \
    -H "Content-Type: application/json" \
    -d '{
      "channel": "amb",
      "to": "c_9f8b2e",
      "body": "How can we help?",
      "metadata": { "businessId": "com.example.support" }
    }'
  ```

  ```typescript Node.js theme={null}
  import { Devotel } from '@devotel-orbit/node';

  const orbit = new Devotel({ apiKey: process.env.ORBIT_API_KEY });

  // The typed messages.send helper takes channel: 'amb' and the
  // AMB business identity in metadata. No per-channel AMB helper
  // exists — the unified send is the documented send path.
  const message = await orbit.messages.send({
    channel: 'amb',
    to: 'c_9f8b2e',
    body: 'How can we help?',
    metadata: { businessId: 'com.example.support' },
  });

  console.log(message.data.id);      // msg_amb_a1b2…
  console.log(message.data.status);  // 'queued'
  ```

  ```python Python theme={null}
  from orbit_sdk import OrbitClient

  client = OrbitClient.from_env()  # reads ORBIT_API_KEY

  # The typed send_sms/send_whatsapp helpers cover their channels only;
  # for AMB, use the generic messages.send with channel='amb'.
  message = client.messages.send(
      channel="amb",
      to="c_9f8b2e",
      body="How can we help?",
      metadata={"businessId": "com.example.support"},
  )

  print(message["data"]["id"])       # msg_amb_a1b2…
  print(message["data"]["status"])   # 'queued'
  ```

  ```go Go theme={null}
  // No typed AMB send helper in the Go SDK — reach the endpoint through
  // the client.Request escape hatch (raw JSON out).
  var message map[string]any
  err := client.Request(
      context.Background(),
      "POST",
      "/messages",
      nil,
      map[string]any{
          "channel": "amb",
          "to":      "c_9f8b2e",
          "body":    "How can we help?",
          "metadata": map[string]any{
              "businessId": "com.example.support",
          },
      },
      &message,
      "",
  )
  if err != nil {
      panic(err)
  }

  fmt.Println(message["data"].(map[string]any)["id"])      // msg_amb_a1b2…
  fmt.Println(message["data"].(map[string]any)["status"])  // "queued"
  ```

  ```ruby Ruby (escape hatch) theme={null}
  # A raw HTTP escape hatch for languages without a typed AMB send.
  message = client.request(
    "POST",
    "/messages",
    json_body: {
      channel: "amb",
      to: "c_9f8b2e",
      body: "How can we help?",
      metadata: { businessId: "com.example.support" }
    }
  )

  puts message.dig("data", "id")       # msg_amb_a1b2…
  puts message.dig("data", "status")   # 'queued'
  ```

  ```php PHP (escape hatch) theme={null}
  // A raw HTTP escape hatch — the curl extension works the same way.
  $message = $client->request('POST', '/messages', [
    'json' => [
      'channel' => 'amb',
      'to' => 'c_9f8b2e',
      'body' => 'How can we help?',
      'metadata' => ['businessId' => 'com.example.support'],
    ],
  ]);

  echo $message['data']['id'];       // msg_amb_a1b2…
  echo $message['data']['status'];   // 'queued'
  ```
</CodeGroup>

For interactive messages, add the appropriate serialized Apple payload to `metadata` and ensure the agent capability is enabled. Orbit exposes the AMB agent lifecycle at `/api/v1/channels/amb/agents`; Apple delivers inbound events to the public `/api/v1/channels/amb/webhook`, where the registered agent secret authenticates the event.

Inbound messages and interaction results are persisted as AMB conversations and appear in the unified inbox. Use the [Inbox setup guide](/guides/inbox-setup) to configure routing, assignments, macros, and SLAs for the `apple_messages` channel.

## Rich interaction payloads

When the agent carries the `interactive`, `form`, or `timePicker` capability, send the matching Apple payload as a serialized JSON string in `metadata`. Each send is a normal unified send with `channel: "amb"` and a `msgType` of `interactive`; the picker/form/timer shape travels in its `*Json` metadata key. The customer's tap returns through your registered AMB webhook as an inbound `message.received` event carrying structured `interactiveData`.

### List picker

Send a list picker so the customer can tap one item from sections. Store the picker JSON as a template string, not a hand-pasted body per send.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.orbit.devotel.io/api/v1/messages \
    -H "X-API-Key: dv_live_sk_..." \
    -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\"}]}]}"
      }
    }'
  ```

  ```typescript Node.js theme={null}
  import { Devotel } from '@devotel-orbit/node';

  const orbit = new Devotel({ apiKey: process.env.ORBIT_API_KEY });

  const listPickerJson = JSON.stringify({
    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' },
        ],
      },
    ],
  });

  await orbit.messages.send({
    channel: 'amb',
    to: 'c_9f8b2e',
    metadata: {
      businessId: 'com.example.support',
      conversationId: 'c_9f8b2e',
      msgType: 'interactive',
      listPickerJson,
    },
  });
  ```
</CodeGroup>

The customer's tap arrives on your AMB webhook as `message.received` with the picked `identifier` in `interactiveData`:

```json theme={null}
{
  "event": "message.received",
  "channel": "amb",
  "data": {
    "id": "msg_amb_inbound_7d3",
    "businessId": "com.example.support",
    "conversationId": "c_9f8b2e",
    "interactiveData": {
      "type": "listPicker",
      "identifier": "SKU-1042"
    }
  }
}
```

The `identifier` is the join key into your catalog and the cart merge below — use your canonical SKU, never a display label.

### Form

Send an Apple dynamic form when the agent has `form` enabled.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.orbit.devotel.io/api/v1/messages \
    -H "X-API-Key: dv_live_sk_..." \
    -H "Content-Type: application/json" \
    -d '{
      "channel": "amb",
      "to": "c_9f8b2e",
      "metadata": {
        "businessId": "com.example.support",
        "conversationId": "c_9f8b2e",
        "msgType": "interactive",
        "formJson": "{\"receivedMessage\":{\"title\":\"Shipping details\",\"subtitle\":\"Where should we send it?\"},\"replyMessage\":{\"title\":\"Thanks\"},\"sections\":[{\"title\":\"Address\",\"items\":[{\"identifier\":\"fullName\",\"title\":\"Full name\",\"type\":\"text\",\"required\":true},{\"identifier\":\"city\",\"title\":\"City\",\"type\":\"text\",\"required\":true}]}]}"
      }
    }'
  ```

  ```typescript Node.js theme={null}
  const formJson = JSON.stringify({
    receivedMessage: { title: 'Shipping details', subtitle: 'Where should we send it?' },
    replyMessage: { title: 'Thanks' },
    sections: [
      {
        title: 'Address',
        items: [
          { identifier: 'fullName', title: 'Full name', type: 'text', required: true },
          { identifier: 'city', title: 'City', type: 'text', required: true },
        ],
      },
    ],
  });

  await orbit.messages.send({
    channel: 'amb',
    to: 'c_9f8b2e',
    metadata: { businessId: 'com.example.support', conversationId: 'c_9f8b2e', msgType: 'interactive', formJson },
  });
  ```
</CodeGroup>

The submitted values arrive on the webhook keyed by each field's `identifier`:

```json theme={null}
{
  "event": "message.received",
  "channel": "amb",
  "data": {
    "id": "msg_amb_inbound_7d4",
    "businessId": "com.example.support",
    "conversationId": "c_9f8b2e",
    "interactiveData": {
      "type": "form",
      "values": { "fullName": "Alice Reyes", "city": "Lisbon" }
    }
  }
}
```

### Time picker

Send a time picker when the agent has `timePicker` enabled, with an `event` and at least one timeslot.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.orbit.devotel.io/api/v1/messages \
    -H "X-API-Key: dv_live_sk_..." \
    -H "Content-Type: application/json" \
    -d '{
      "channel": "amb",
      "to": "c_9f8b2e",
      "metadata": {
        "businessId": "com.example.support",
        "conversationId": "c_9f8b2e",
        "msgType": "interactive",
        "timePickerJson": "{\"receivedMessage\":{\"title\":\"Book a slot\",\"subtitle\":\"Pick a time that works\"},\"replyMessage\":{\"title\":\"Booked\"},\"event\":{\"identifier\":\"apt-7361\",\"title\":\"Acme support appointment\",\"timeslots\":[{\"identifier\":\"slot-1\",\"title\":\"Mon 10:00\",\"duration\":30},{\"identifier\":\"slot-2\",\"title\":\"Mon 11:00\",\"duration\":30}]}}"
      }
    }'
  ```

  ```typescript Node.js theme={null}
  const timePickerJson = JSON.stringify({
    receivedMessage: { title: 'Book a slot', subtitle: 'Pick a time that works' },
    replyMessage: { title: 'Booked' },
    event: {
      identifier: 'apt-7361',
      title: 'Acme support appointment',
      timeslots: [
        { identifier: 'slot-1', title: 'Mon 10:00', duration: 30 },
        { identifier: 'slot-2', title: 'Mon 11:00', duration: 30 },
      ],
    },
  });

  await orbit.messages.send({
    channel: 'amb',
    to: 'c_9f8b2e',
    metadata: { businessId: 'com.example.support', conversationId: 'c_9f8b2e', msgType: 'interactive', timePickerJson },
  });
  ```
</CodeGroup>

The chosen timeslot arrives as the slot `identifier`:

```json theme={null}
{
  "event": "message.received",
  "channel": "amb",
  "data": {
    "id": "msg_amb_inbound_7d5",
    "businessId": "com.example.support",
    "conversationId": "c_9f8b2e",
    "interactiveData": {
      "type": "timePicker",
      "identifier": "slot-2",
      "eventIdentifier": "apt-7361"
    }
  }
}
```

A send missing the matching `*Json` metadata key is rejected with `422` naming the key — fix the template and resend; nothing queues.

## Commerce checkout

Use an AMB list picker or form to collect a product or appointment choice, then merge the result into the shared commerce cart. The AMB checkout resolver can return a native Apple Pay request when the agent has `applePay` enabled and the merchant identifier is configured. Otherwise, send the hosted checkout link in the conversation. Reconcile the payment server-side before marking the order paid.

### Resolve an AMB checkout

Call the AMB checkout resolver with the order line items and the Apple Pay configuration. When both `applePayEnabled` and `merchantIdentifier` are set, the response is `method: "native"` and carries the Apple Pay sheet payload; otherwise it degrades to `method: "hosted"` with a pay-by-link.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.orbit.devotel.io/api/v1/commerce/amb-checkout \
    -H "X-API-Key: dv_live_sk_..." \
    -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"
      }
    }'
  ```

  ```typescript Node.js theme={null}
  import { Devotel } from '@devotel-orbit/node';

  const orbit = new Devotel({ apiKey: process.env.ORBIT_API_KEY });

  // No typed amb-checkout helper in the Node SDK — reach the endpoint
  // through orbit.request (raw JSON returned).
  const checkout = await orbit.request('POST', '/commerce/amb-checkout', {
    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',
    },
  });

  console.log(checkout.data.method);  // 'native' when Apple Pay is eligible, else 'hosted'
  ```
</CodeGroup>

The response carries both branches so a native failure can fall back without a second call:

```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 }
}
```

When `method` is `"native"`, present the `native` Apple Pay request in the thread — the customer authorizes with Face ID and your payment service receives a token addressed to it. When the agent lacks `applePay` or the `merchantIdentifier` is unset, `method` is `"hosted"` and `native` is `null`; send `hosted.url` (or the pre-rendered `hosted.body`) into the thread instead. Both paths reconcile against the same `paymentRequest`, so which one the customer completed does not fork your order logic.

Follow [AMB commerce: interactive flows and Apple Pay checkout](/guides/apple-messages-commerce-checkout) for the payloads, cart merge, token capture, reconciliation, and the decline-and-retry loop.

## Limitations

* **Apple approval is required.** Orbit cannot approve a business or bypass Apple's Business Register review. An agent remains `pending` until it is cleared for live traffic.
* **Availability follows Apple.** AMB entry points, Apple Pay eligibility, and supported features vary by Apple policy, merchant setup, device, and geography. Confirm availability for the markets and capabilities you plan to serve.
* **Conversations are customer-initiated.** Start with an Apple entry point or an existing customer thread; do not treat AMB as an unrestricted outbound broadcast channel.
* **Capabilities are agent-specific.** Rich interactions and Apple Pay require the corresponding capability and, for native payment, a merchant identifier. Unsupported or malformed Apple payloads are rejected rather than rendered by another channel.
* **A valid secret is required for routing.** Rotating or deleting an agent changes webhook verification or routing for that Apple business.

## Related guides

* [Apple Messages for Business onboarding](/guides/apple-messages-for-business-onboarding)
* [AMB commerce: interactive flows and Apple Pay checkout](/guides/apple-messages-commerce-checkout)
* [Omnichannel Inbox setup](/guides/inbox-setup)
* [WhatsApp](/channels/whatsapp) — compare another rich messaging channel and its provider-specific approval and payload model.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.