App Cards API
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.
How App Card sessions work
Section titled “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
Section titled “Send an App Card”POST https://api.sendblue.com/api/send-messageSee the generated send-message API reference
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.
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
Section titled “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. |
app_card | object | Yes | App Card identity, state, and visible layout. |
app_card fields
Section titled “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. |
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
Section titled “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
Section titled “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
Section titled “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
Section titled “Continue an App Card”POST https://api.sendblue.com/api/messages/{original_message_handle}/update-app-cardSee the generated update-app-card API reference
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.
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
Section titled “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
Section titled “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 or
GET /api/v2/messages/{message_handle}. 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
Section titled “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:
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
Section titled “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 for target and thread semantics.
Errors and operational guidance
Section titled “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_urlormedia_objectin the same send request. Uselayout.imageUrlfor 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: falsewhen 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.