Capabilities & limits (AMB)

Two reference matrices Apple expects every MSP to maintain: the OS-minimum / device-scope matrix for each interactive feature, and the timeouts & TTLs matrix by message type and flow stage. Cite these values — do not improvise.

Apple's Experience Review expects every Messaging Service Provider to maintain two living matrices and drive real behaviour from them: one that gates each interactive feature by minimum OS version and device, and one that pins every timeout and TTL to Apple's published value. Mismatches between an MSP's timeouts and Apple's are a common Experience Review finding, so the values here are copied from Apple's spec verbatim — cite them, don't improvise, and update the matrices whenever Apple ships new OS floors.

Both matrices are exported as code below so you can import them directly to drive the bot's branch logic and the agent console's affordance lock, rather than hard-coding versions in scattered places.

Capability gating — OS minimum & device scope

Before sending an interactive payload, check the customer device's capability-list and the OS floor for the feature. The agent console must not permit sending a payload the device can't render, and bots should branch on these values to pick a supported payload.

Feature Min iOS / iPadOS Min macOS iPhone iPad Mac Apple Watch capability-list token
Text, emoji, international characters 11.3 10.13.4 ✓ ✓ ✓ ✓ (watchOS 4.3+) —
Attachments (≤ 100 MB) 11.3 10.13.4 ✓ ✓ ✓ view only —
Rich Link 11.3 10.13.4 ✓ ✓ ✓ ✗ — (all device types)
Apple Pay 11.3 10.13.4 ✓ ✓ ✓¹ authorize only — (all device types)
List Picker 11.3 10.13.4 ✓ ✓ ✓ ✗ LIST
Time Picker 11.3 10.13.4 ✓ ✓ ✓ ✗ TIME
Quick Reply 15.1 12.0 ✓ ✓ ✓ ✗ QUICK
Authentication (legacy OAuth) 11.3 10.13.4 deprecated deprecated deprecated ✗ AUTH
New Authentication (OAuth 2.0) 16.0 13.0 ✓ ✓ ✓ ✗ AUTH2
iMessage apps (custom interactive data) 11.3 — ✓ ✓ ✗ ✗ —
Form Message 18.4 — ✓ ✓ ✗ ✗ FORM
Invitation Messages 18.0 15.0 ✓ ✓ ✓ ✗ —²
Typing indicator 11.3 10.13.4 ✓ ✓ ✓ ✗ —
Tapback reactions (iOS 18 set) 18.0 15.0 ✓ ✓ ✓ ✗ —

¹ On Mac, Apple Pay requires a Mac with Touch ID or an Apple Pay-capable iPhone / Apple Watch to authorize. On third-party browsers the user needs an Apple Pay-capable iPhone running iOS 18+. ² Invitations have no capability-list token — instead, every device linked to the recipient's Apple Account must be on iOS 18.0 / macOS 15.0 or later.

Note Base Apple Messages for Business support starts at iOS 11.3 / iPadOS 13 / macOS 10.13.4 / watchOS 4.3. Apple Watch can view conversations and authorize Apple Pay but does not render interactive message types — treat Watch as text-only when gating.

The capability-list header

Every inbound message carries a capability-list header — a case-insensitive, comma-separated list of the tokens above (e.g. AUTH2,LIST,TIME,QUICK,FORM). The MSP must read it, remember it per opaque user id, and use it to gate outbound payloads. Tokens are only defined for features Apple gates this way; Apple Pay and Rich Links are supported on all device types and versions, so they have no token, and iMessage apps and Forms are only supported on iPhone and iPad.

Drive gating from code, not from memory

// amb-capabilities.ts — single source of truth for feature gating.
// Update the OS floors here whenever Apple ships a new feature floor.

export type Device = "iphone" | "ipad" | "mac" | "watch";
export type CapabilityToken = "QUICK" | "LIST" | "TIME" | "AUTH" | "AUTH2" | "FORM";

export interface FeatureCap {
  /** capability-list token, or null for features gated only by OS/device. */
  token: CapabilityToken | null;
  /** Minimum OS floors ("—" means the feature is not offered on that platform). */
  minOs: { ios: string | null; ipados: string | null; macos: string | null };
  /** Device classes that can render the feature. */
  devices: Device[];
}

export const AMB_CAPABILITIES: Record<string, FeatureCap> = {
  richLink:        { token: null,   minOs: { ios: "11.3", ipados: "13.0", macos: "10.13.4" }, devices: ["iphone", "ipad", "mac"] },
  applePay:        { token: null,   minOs: { ios: "11.3", ipados: "13.0", macos: "10.13.4" }, devices: ["iphone", "ipad", "mac"] },
  listPicker:      { token: "LIST", minOs: { ios: "11.3", ipados: "13.0", macos: "10.13.4" }, devices: ["iphone", "ipad", "mac"] },
  timePicker:      { token: "TIME", minOs: { ios: "11.3", ipados: "13.0", macos: "10.13.4" }, devices: ["iphone", "ipad", "mac"] },
  quickReply:      { token: "QUICK", minOs: { ios: "15.1", ipados: "15.1", macos: "12.0" },   devices: ["iphone", "ipad", "mac"] },
  authentication:  { token: "AUTH2", minOs: { ios: "16.0", ipados: "16.0", macos: "13.0" },   devices: ["iphone", "ipad", "mac"] },
  imessageApp:     { token: null,   minOs: { ios: "11.3", ipados: "13.0", macos: null },      devices: ["iphone", "ipad"] },
  form:            { token: "FORM", minOs: { ios: "18.4", ipados: "18.4", macos: null },      devices: ["iphone", "ipad"] },
  invitation:      { token: null,   minOs: { ios: "18.0", ipados: "18.0", macos: "15.0" },    devices: ["iphone", "ipad", "mac"] },
};

/** True only when the device advertises the token (when one is required). */
export function deviceSupports(
  feature: keyof typeof AMB_CAPABILITIES,
  ctx: { device: Device; capabilityList: string[] },
): boolean {
  const cap = AMB_CAPABILITIES[feature];
  if (!cap) return false;
  if (!cap.devices.includes(ctx.device)) return false;
  if (cap.token && !ctx.capabilityList.includes(cap.token)) return false;
  return true;
}

What the API offers you at runtime

You don't have to reproduce this table yourself. Every AMB chat resource (GET /chats/{id}) includes a supported_interactive_kinds array derived from the device's advertised capability-list — use it to grey out or branch away from a kind the current device can't render before you compose, instead of finding out at send time. Kinds gated only by OS/device (apple_pay, imessage_app) are always listed; Apple Pay is all-device, and an iMessage app degrades natively to an App Store card when the extension isn't installed.

Fallback decision tree (when a device can't render the preferred type)

Sending an interactive type the device doesn't advertise returns 422 amb_capability_unavailable by default — a named, actionable error rather than a dead-end bubble. To make the use case complete on an unsupported device (Apple's Experience Review criterion), attach a fallback. This follows the same sender-declared-fallback convention as Twilio's Content API, Slack Block Kit, and RCS→SMS: you keep control of the response shape rather than the platform silently changing it.

There are three modes, in order of precedence:

  1. Caller-declared fallback (recommended). Put an alternative under the interactive content's fallback field — a single content object or an ordered array. When the preferred type is unsupported, Blooio sends your fallback instead. You own the shape, so your automation sees exactly what you specified.
{
  "to": "+15551234567",
  "interactive": {
    "kind": "form",
    "dynamic": { "data": { "pages": [ /* ... */ ] } },
    "fallback": [
      { "kind": "quick_reply", "summary_text": "Pick a size", "items": [
        { "id": "s", "title": "Small" }, { "id": "l", "title": "Large" }
      ] }
    ]
  }
}
  1. Automatic transform (fallback: "auto", or the force-fallback header). Opt in and Blooio applies Apple's documented §17.5 downgrade tree — but opportunistically: it scans the form and maps each page onto the richest type the recipient's device actually advertises, so nothing is dropped that the device could have shown.

    • Form → one message per page. The splash cover is dropped — it's a pre-form screen, not an answerable step.
    • select / picker page → a List Picker that keeps its icons when the device advertises LIST. Otherwise a plain-text prompt — Choose one of the following: (single-select) or Choose multiple of the following: (multi-select) — followed by the numbered options. Icons are omitted in the text form: only List rows render art natively, and a burst of loose images reads worse than a clean list.
    • datePicker page → a plain-text question carrying the date format it expected, e.g. When works for you? (format: MM/dd/yyyy).
    • input (open-ended) page → the plain-text question.
    • any downgraded piece the device still can't render → plain text (universal).
{ "to": "+15551234567", "fallback": "auto", "interactive": { "kind": "form", "dynamic": { "data": {} } } }

You can also request this per send with the force-fallback request header instead of the body field — send force-fallback: true (also accepts 1, on, yes). It is exactly equivalent to fallback: "auto" and exists so a UI can toggle it without rewriting the request body (the dashboard's Send with fallback split button uses it). A caller-declared fallback array still takes precedence over both.

curl https://api.blooio.com/v4/messages \
  -H "Authorization: Bearer $BLOOIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "force-fallback: true" \
  -d '{ "to": "+15551234567", "interactive": { "kind": "form", "dynamic": { "data": {} } } }'
Try it
  1. Reject (default). No fallback and fallback absent (or "reject"), and no force-fallback header: the 422 stands and you choose how to recover. Nothing about existing sends changes unless you opt in.

When a downgrade happens the response carries downgraded_from (the original kind) and, if the fallback expanded into more than one message, fallback_messages — so your automation can tell a structural downgrade occurred rather than silently receiving a different shape.

Note apple_pay and imessage_app have no capability-list token and are never blocked at send time. Apple Pay is supported on all devices; an iMessage app renders Apple's native App Store fallback card when the extension isn't installed (populate app_id / app_name / app_icon).

Timeouts & TTLs

Every value below is Apple's published spec value. Pin your MSP timeouts to these exactly — a shorter MSP timeout that fires before Apple's is a common Experience Review finding.

Stage / object Value Applies to Rule
Typing indicator auto-expiry 60 s typing_start with no typing_end Show the indicator until typing_end or 60 s elapse, then clear it.
Bot typing-indicator lead time 1 s Bot before each message Send a 1-second typing indicator before each bot message.
Apple message-send response 30 s POST to Apple's /message endpoint & interactive-data fetch Set the client timeout to 30 s; if Apple doesn't return a status code, cancel and retry later.
Apple Pay merchant session 5 min, single-use The paymentSession object Request a fresh session per transaction; it expires 5 minutes after creation and can be used once. Never request it from the client.
Apple Pay endpoint response 30 s paymentGateway, orderTracking, shippingContactUpdate, shippingMethodUpdate, paymentMethodUpdate Each endpoint must respond within 30 s or Apple times the request out.
Payment Processing certificate 25 months Cert renewal Renew before expiry; an expired cert makes Apple Pay transactions fail.
Merchant ID never expires Apple Pay merchant identifier Reusable across apps/websites; no rotation needed.
Web merchant domain registration never expires Verified Apple Pay domain No periodic re-verification.
MSP auth JWT HS256; iat in seconds; no Apple-mandated exp Authorization header on every request Sign per request; iat must be seconds (not ms). Rotate the shared secret in Apple Business Register if it leaks.
OAuth 2.0 refresh token non-rotating New Authentication flow Configure refresh-token behaviour as non-rotating.
Attachment URL (Apple MMCS) not published by Apple; decryption key single-use Inbound/outbound attachments (≤ 100 MB) Fetch and decrypt promptly, then re-host — see the ratchet below.
Conversation session terminal on delete Customer removing the conversation Apple returns 410 session expired; the customer is no longer reachable.

The attachment TTL ratchet (Apple → MSP → CDN)

Apple deliberately does not publish a fixed lifetime for MMCS attachment URLs, and the decryption key travels inline in the message payload and is single-use. Treat the credential as ratcheting down at every hop and never forward an upstream credential downstream:

  1. Apple → MSP. Apple hands Blooio a short-lived MMCS URL plus the single-use decryption key. Blooio fetches and decrypts immediately on receipt — the window is short and undocumented, so don't defer.
  2. MSP → you. Blooio re-hosts the decrypted file and gives you a Blooio-signed URL with its own TTL. Apple's MMCS URL and key are never exposed.
  3. You → client / CDN. Before showing the file to an end user, re-host it on your CDN with your TTL. Don't hand a Blooio or Apple URL to a browser — each layer mints a fresh, shorter-lived credential.

Timeouts as code

// amb-timeouts.ts — Apple's published timeouts & TTLs (milliseconds unless noted).
// These are spec values; do not shorten below Apple's without a reason.

export const AMB_TIMEOUTS = {
  typingIndicatorExpiryMs: 60_000,        // clear typing UI after 60 s with no typing_end
  botTypingLeadMs: 1_000,                 // 1 s typing indicator before each bot message
  messageSendResponseMs: 30_000,          // Apple /message response ceiling
  applePayEndpointResponseMs: 30_000,     // paymentGateway / orderTracking / *Update endpoints
  applePaySessionTtlMs: 5 * 60_000,       // merchant session validity — single use
  paymentProcessingCertMonths: 25,        // renew before expiry
} as const;

export const AMB_LIFETIMES = {
  merchantId: "never",                    // reusable across apps/websites
  webMerchantDomain: "never",             // no periodic re-verification
  oauth2RefreshToken: "non-rotating",
  attachmentUrl: "unpublished-single-use", // fetch, decrypt & re-host immediately
} as const;

Sources