--- title: App Cards API | Sendblue Docs description: Send and continue interactive iMessage App Cards with the Sendblue API --- App Cards let you send content created for an iMessage app extension, including interactive experiences and stateful updates. Sendblue transports the card’s extension identity, URL state, and visible layout through the standard Messages API. Your application defines the state URL and the Messages extension interprets it. For interactive behavior, the recipient needs the corresponding Messages extension installed; otherwise the card uses its static layout. App Cards require a compatible V2 line and an iMessage-capable recipient. They are unavailable on the free plan and never fall back to SMS. ## How App Card sessions work An App Card begins with a normal `POST /api/send-message` request. Sendblue creates an App Card session and returns the original message handle. To continue that card later, call the update endpoint with the **original** message handle. | Event | Endpoint or source | Session | Message handle | | ---------------- | ------------------------------------------------------ | ------------------------ | --------------------------- | | Initial card | `POST /api/send-message` | Creates a session | Returns the original handle | | Card update | `POST /api/messages/{original_handle}/update-app-card` | Reuses the session | Returns a new handle | | Recipient update | Receive webhook or `GET /api/v2/messages` | Reports the same session | Has its own inbound handle | Every continuation is a new iMessage with its own delivery and read status. It does not edit the original message in place. Sendblue retains the extension identity, sender line, session identifier, and inline-reply context from the initial card. ## Send an App Card ``` POST https://api.sendblue.com/api/send-message ``` See the generated [`send-message` API reference](/api/resources/messages/methods/send/index.md) for the complete shared message request and response schema. The `app_card` object is added to the standard send-message request. `content` is optional for an App Card; when it is omitted, Sendblue uses the card’s `layout.summary`, caption, subcaption, or app name as fallback text. Terminal window ``` curl -X POST 'https://api.sendblue.com/api/send-message' \ -H 'sb-api-key-id: YOUR_API_KEY' \ -H 'sb-api-secret-key: YOUR_API_SECRET' \ -H 'Content-Type: application/json' \ -d '{ "number": "+19175551234", "from_number": "+14155559876", "status_callback": "https://example.com/webhooks/message-status", "app_card": { "appName": "Example Orders", "appStoreId": 1234567890, "extensionBundleId": "com.example.orders.MessagesExtension", "teamId": "ABCDE12345", "url": "https://example.com/orders/order_123", "interactive": true, "layout": { "caption": "Order confirmed", "subcaption": "Tap to view order 123", "trailingCaption": "$42.00", "imageUrl": "https://example.com/images/order-123.png", "imageTitle": "Order 123", "summary": "Your order is confirmed" } } }' ``` Replace the example app identity and URLs with values for the Messages extension you are integrating. `extensionBundleId` identifies the Messages extension, rather than its containing iOS app. ### Initial request fields | Field | Type | Required | Description | | ----------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------- | | `number` | string | Yes | Recipient in E.164 format. | | `from_number` | string | Yes | A V2 line on your account, in E.164 format. | | `content` | string | No | Message text stored with the send. Derived from the card when omitted; it does not replace the visible layout. | | `status_callback` | string | No | HTTPS endpoint for this message’s status updates. | | `reply_to` | object | No | Inline-reply target. See [Inline replies](/guides/inline-replies/index.md). | | `app_card` | object | Yes | App Card identity, state, and visible layout. | ### `app_card` fields | Field | Type | Required | Description | | ------------------- | ------- | -------- | ----------------------------------------------------------------------------------------- | | `appName` | string | Yes | Display name, up to 100 characters. | | `appStoreId` | integer | No | Positive numeric App Store ID associated with the app. | | `extensionBundleId` | string | Yes | Bundle identifier of the Messages extension, up to 255 characters. | | `teamId` | string | Yes | The extension’s 10-character Apple Team ID, using uppercase letters and digits. | | `url` | string | Yes | State or deep link delivered to the extension. See [URL requirements](#url-requirements). | | `interactive` | boolean | No | Defaults to `true`. Set to `false` to always use the static template layout. | | `sessionIdentifier` | UUID | No | App Card session UUID. Sendblue generates one when omitted. | | `fallbackText` | string | No | Convenience fallback, up to 1,024 characters, used when `layout.summary` is omitted. | | `layout` | object | Yes | Visible card content. At least one caption field is required. | You do not need to generate `sessionIdentifier` unless another system must know the session UUID before the send request. Whether supplied or generated, retain the value returned by Sendblue and treat it as the stable session identifier. `updateMessageHandle` is response metadata for continuations; do not include it in an initial send request. App Card extension identities do not require a separate Sendblue registration or allowlist step. Your integration is responsible for supplying a syntactically valid identity and state URL for the extension it intends to use. ### Layout fields | Field | Type | Required | Description | | -------------------- | ------ | ----------- | ------------------------------------------------------------------------- | | `caption` | string | Conditional | Primary caption. | | `subcaption` | string | Conditional | Secondary caption. | | `trailingCaption` | string | Conditional | Trailing primary caption. | | `trailingSubcaption` | string | Conditional | Trailing secondary caption. | | `imageUrl` | string | No | Public HTTPS preview image URL. | | `imageTitle` | string | No | Text displayed over the image. Requires `imageUrl`. | | `imageSubtitle` | string | No | Secondary image text. Requires `imageUrl`. | | `summary` | string | No | Fallback text for notifications and surfaces that cannot render the card. | At least one of `caption`, `subcaption`, `trailingCaption`, or `trailingSubcaption` is required on the initial send. An image by itself is not enough. Each provided text field must be non-empty and is limited to 1,024 characters; `imageUrl` has its own limit below. Preview images must be JPEG, PNG, HEIC, HEIF, or WebP, no larger than 10 MiB (10,485,760 bytes). `imageUrl` must be a public HTTPS URL no longer than 2,048 characters that returns the image directly with a supported image Content-Type. Redirects and private-network destinations are rejected. ### URL requirements `url` must be one of: - An absolute HTTPS URL up to 2,048 characters. - A Messages data URL beginning with `data:?ver=` up to 16,384 characters. The URL format and state payload are defined by the app extension. Sendblue transports that value without interpreting the app-specific state. ### Initial response A newly queued request returns HTTP `202` and a message handle. Acceptance does not confirm delivery; track the returned handle for subsequent status updates. The response includes the normalized App Card and its session identifier. This example shows selected response fields. ``` { "status": "QUEUED", "message_handle": "5a17319e-cbcf-443e-897e-d8b0c04b1b09", "from_number": "+14155559876", "number": "+19175551234", "service": "iMessage", "app_card": { "appName": "Example Orders", "appStoreId": 1234567890, "extensionBundleId": "com.example.orders.MessagesExtension", "teamId": "ABCDE12345", "url": "https://example.com/orders/order_123", "interactive": true, "sessionIdentifier": "d4f28f24-49bb-43ce-b82f-d2f1db74ad74", "layout": { "caption": "Order confirmed", "subcaption": "Tap to view order 123", "trailingCaption": "$42.00", "imageUrl": "https://example.com/images/order-123.png", "imageTitle": "Order 123", "summary": "Your order is confirmed" } } } ``` Wait until the original message reaches `SENT`, `DELIVERED`, or `READ` before sending a continuation. ## Continue an App Card ``` POST https://api.sendblue.com/api/messages/{original_message_handle}/update-app-card ``` See the generated [`update-app-card` API reference](/api/resources/messages/methods/update_app_card/index.md) for the complete endpoint schema. Use the handle returned by the initial App Card send. Continue using that same original handle for every update; do not substitute a continuation’s handle. The update inherits the latest outbound card state that is not `ERROR` or `DECLINED`. It does not automatically use a newer inbound card event as its baseline. Only include fields that changed from that outbound state. The card’s app identity, sender line, session identifier, and optional inline-reply context cannot be changed through this endpoint. Terminal window ``` curl -X POST \ 'https://api.sendblue.com/api/messages/5a17319e-cbcf-443e-897e-d8b0c04b1b09/update-app-card' \ -H 'sb-api-key-id: YOUR_API_KEY' \ -H 'sb-api-secret-key: YOUR_API_SECRET' \ -H 'Content-Type: application/json' \ -d '{ "url": "https://example.com/orders/order_123?state=shipped", "fallback_text": "Order 123 has shipped", "layout": { "caption": "Order shipped", "subcaption": "Tap to track your delivery" }, "idempotency_key": "order_123_shipped_v1" }' ``` ### Update fields | Field | Type | Required | Description | | ----------------- | ------- | -------- | -------------------------------------------------- | | `url` | string | No | Replacement extension state or deep link. | | `interactive` | boolean | No | Replacement interactive/static preference. | | `fallback_text` | string | No | Replaces `layout.summary`, up to 1,024 characters. | | `layout` | object | No | Layout values to merge into the current state. | | `send_style` | string | No | Supported iMessage expressive message style. | | `idempotency_key` | string | No | Retry identity, from 1 to 255 characters. | Use a unique idempotency key for each logical update, and reuse the same key when retrying that update after a timeout or lost response. A repeated request with the same original handle and idempotency key returns the existing update instead of sending a duplicate, even if that update ended in an error. Concurrent update requests for the same original card are serialized. A `409` can mean another request is being processed or the original card is not yet `SENT`, `DELIVERED`, or `READ`. Wait for the relevant condition to clear, then retry with the same idempotency key. ### Update response A newly queued update returns `202`. An idempotent replay returns the HTTP code for the existing update’s current status: | Existing update status | HTTP status | | ------------------------------ | ----------- | | `QUEUED` | `202` | | `SENT`, `DELIVERED`, or `READ` | `200` | | `TIMEOUT` | `408` | | `ERROR` or `DECLINED` | `400` | | `RATE_LIMIT_EXCEEDED` | `429` | These replay responses all identify the same logical update and return its original message handle. Reusing its idempotency key never creates a second send. After a terminal failure, use a new key only when you deliberately want a new send attempt. The following example shows selected fields from a newly queued update. ``` { "status": "QUEUED", "message_handle": "80891ae5-d769-4bd7-a523-0416389d9d48", "from_number": "+14155559876", "number": "+19175551234", "service": "iMessage", "app_card": { "appName": "Example Orders", "appStoreId": 1234567890, "extensionBundleId": "com.example.orders.MessagesExtension", "teamId": "ABCDE12345", "url": "https://example.com/orders/order_123?state=shipped", "interactive": true, "sessionIdentifier": "d4f28f24-49bb-43ce-b82f-d2f1db74ad74", "updateMessageHandle": "5a17319e-cbcf-443e-897e-d8b0c04b1b09", "layout": { "caption": "Order shipped", "subcaption": "Tap to track your delivery", "trailingCaption": "$42.00", "imageUrl": "https://example.com/images/order-123.png", "imageTitle": "Order 123", "summary": "Order 123 has shipped" } } } ``` Track each returned `message_handle` through an account-level [`outbound` webhook](/getting-started/webhooks/index.md) or [`GET /api/v2/messages/{message_handle}`](/api-v2/messages/index.md). The update endpoint does not accept `status_callback` and does not inherit the initial message callback. It also does not inherit `send_style`; provide it on each update that should use an effect. ## Receive App Card state When an iMessage contact sends or updates an App Card, Sendblue includes an `app_card` object on the normal inbound message webhook and message API result. The following example shows selected fields; inbound metadata does not include the outbound `layout` object. ``` { "status": "RECEIVED", "message_handle": "76f61f71-3151-48e7-99f5-858652887d92", "is_outbound": false, "service": "iMessage", "from_number": "+19175551234", "sendblue_number": "+14155559876", "app_card": { "extensionBundleId": "com.example.orders.MessagesExtension", "teamId": "ABCDE12345", "balloonBundleId": "com.apple.messages.MSMessageExtensionBalloonPlugin:ABCDE12345:com.example.orders.MessagesExtension", "url": "https://example.com/orders/order_123?state=confirmed", "sessionIdentifier": "d4f28f24-49bb-43ce-b82f-d2f1db74ad74", "revision": 1 } } ``` | Field | Description | | ------------------- | --------------------------------------------------------------- | | `extensionBundleId` | Bundle identifier reported for the Messages extension. | | `teamId` | Apple Team ID reported for the extension. | | `balloonBundleId` | Complete Messages extension identity reported with the card. | | `url` | App-defined state or deep link received with this event. | | `sessionIdentifier` | Stable UUID shared by events in the App Card session. | | `revision` | Positive, retry-stable revision assigned to the received state. | Use the opaque `message_handle` to deduplicate webhook deliveries; do not assume it is a UUID or parse its structure. Correlate `sessionIdentifier` within the same conversation, sending line, and extension identity. Use `revision` to distinguish received state occurrences; retries retain the same revision, and a later occurrence can revisit an earlier URL. Treat inbound URLs and data payloads as untrusted input: validate the extension identity, URL scheme, size, and app-specific state before using them. When continuing a card in response to an inbound event, calculate the next app state from that event’s `app_card.url` and include the resulting `url` in your update request. If `url` is omitted, Sendblue reuses the latest outbound URL, not the recipient’s inbound URL. Inbound App Cards are also available when listing or retrieving messages: Terminal window ``` curl \ 'https://api.sendblue.com/api/v2/messages?is_outbound=false&service=iMessage&limit=50' \ -H 'sb-api-key-id: YOUR_API_KEY' \ -H 'sb-api-secret-key: YOUR_API_SECRET' ``` ## Inline replies An initial App Card can also be an iMessage inline reply. Add `reply_to` beside `app_card` in the initial send request: ``` { "number": "+19175551234", "from_number": "+14155559876", "reply_to": { "message_handle": "99DCC379-DD76-4712-BA65-11EFB33B8CD6" }, "app_card": { "appName": "Example Orders", "extensionBundleId": "com.example.orders.MessagesExtension", "teamId": "ABCDE12345", "url": "https://example.com/orders/order_123", "layout": { "caption": "Order confirmed" } } } ``` Later calls to `update-app-card` retain that inline-reply context automatically. See [Inline replies](/guides/inline-replies/index.md) for target and thread semantics. ## Errors and operational guidance | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | Invalid input, a dispatch failure, or an idempotent replay of an `ERROR`/`DECLINED` update. Inspect the response fields and message handle. | | `401` | Missing or invalid API credentials. | | `403` | App Cards are unavailable for the account’s plan. | | `404` | The original outbound App Card was not found, is not owned by the account, or the supplied handle is not the original handle. | | `408` | The request timed out. Check message status and retry with the same idempotency key. | | `409` | The original card has not reached a send state, or another update is in progress. | | `429` | The account’s API rate limit was exceeded. | | `500` | An internal error prevented the request from completing. | - App Cards cannot be combined with `media_url` or `media_object` in the same send request. Use `layout.imageUrl` for the card preview image. - App Cards are one-to-one iMessage sends. They do not fall back to SMS. - Interactive behavior requires the recipient to have the corresponding Messages extension installed. Set `interactive: false` when you always want the static card presentation. - Store the original message handle and the session identifier with your application state. You need the original handle for every continuation. - Keep extension-specific state generation in your application. Sendblue treats the card URL as opaque transport data.