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:
- Caller-declared fallback (recommended). Put an alternative under the
interactive content's
fallbackfield — 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" }
] }
]
}
}Automatic transform (
fallback: "auto", or theforce-fallbackheader). 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) orChoose 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- Reject (default). No fallback and
fallbackabsent (or"reject"), and noforce-fallbackheader: the422stands 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_payandimessage_apphave nocapability-listtoken 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 (populateapp_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:
- 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.
- 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.
- 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
- Apple — Type Interactive (Quick Reply iOS 15.1 / macOS 12.0;
capability-listtokens; Forms & iMessage apps iPhone/iPad only): register.apple.com/resources/messages/msp-rest-api/type-interactive - Apple — Form Message (iOS 18.4+) and Invitation Messages (iOS 18.0 / macOS 15.0) in the same REST API spec.
- Apple — Messages Sent / Common Specs (30 s send response; 60 s typing-indicator timeout; JWT
iatin seconds). - Apple — Requesting an Apple Pay payment session (session expires after five minutes, single-use, server-side only): developer.apple.com/documentation/applepayontheweb/requesting-an-apple-pay-payment-session
- Apple — Configuring Your Environment (Payment Processing certificate valid 25 months; Merchant ID never expires).
- Apple — device OS floors: Messages for Business FAQ (iOS 11.3 / macOS 10.13.4 / watchOS 4.3).