Skip to content
Get Started
API v2

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.

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.

EventEndpoint or sourceSessionMessage handle
Initial cardPOST /api/send-messageCreates a sessionReturns the original handle
Card updatePOST /api/messages/{original_handle}/update-app-cardReuses the sessionReturns a new handle
Recipient updateReceive webhook or GET /api/v2/messagesReports the same sessionHas 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.

POST https://api.sendblue.com/api/send-message

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

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.

FieldTypeRequiredDescription
numberstringYesRecipient in E.164 format.
from_numberstringYesA V2 line on your account, in E.164 format.
contentstringNoMessage text stored with the send. Derived from the card when omitted; it does not replace the visible layout.
status_callbackstringNoHTTPS endpoint for this message’s status updates.
reply_toobjectNoInline-reply target. See Inline replies.
app_cardobjectYesApp Card identity, state, and visible layout.
FieldTypeRequiredDescription
appNamestringYesDisplay name, up to 100 characters.
appStoreIdintegerNoPositive numeric App Store ID associated with the app.
extensionBundleIdstringYesBundle identifier of the Messages extension, up to 255 characters.
teamIdstringYesThe extension’s 10-character Apple Team ID, using uppercase letters and digits.
urlstringYesState or deep link delivered to the extension. See URL requirements.
interactivebooleanNoDefaults to true. Set to false to always use the static template layout.
sessionIdentifierUUIDNoApp Card session UUID. Sendblue generates one when omitted.
fallbackTextstringNoConvenience fallback, up to 1,024 characters, used when layout.summary is omitted.
layoutobjectYesVisible 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.

FieldTypeRequiredDescription
captionstringConditionalPrimary caption.
subcaptionstringConditionalSecondary caption.
trailingCaptionstringConditionalTrailing primary caption.
trailingSubcaptionstringConditionalTrailing secondary caption.
imageUrlstringNoPublic HTTPS preview image URL.
imageTitlestringNoText displayed over the image. Requires imageUrl.
imageSubtitlestringNoSecondary image text. Requires imageUrl.
summarystringNoFallback 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 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.

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.

POST https://api.sendblue.com/api/messages/{original_message_handle}/update-app-card

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

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"
}'
FieldTypeRequiredDescription
urlstringNoReplacement extension state or deep link.
interactivebooleanNoReplacement interactive/static preference.
fallback_textstringNoReplaces layout.summary, up to 1,024 characters.
layoutobjectNoLayout values to merge into the current state.
send_stylestringNoSupported iMessage expressive message style.
idempotency_keystringNoRetry 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.

A newly queued update returns 202. An idempotent replay returns the HTTP code for the existing update’s current status:

Existing update statusHTTP status
QUEUED202
SENT, DELIVERED, or READ200
TIMEOUT408
ERROR or DECLINED400
RATE_LIMIT_EXCEEDED429

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.

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
}
}
FieldDescription
extensionBundleIdBundle identifier reported for the Messages extension.
teamIdApple Team ID reported for the extension.
balloonBundleIdComplete Messages extension identity reported with the card.
urlApp-defined state or deep link received with this event.
sessionIdentifierStable UUID shared by events in the App Card session.
revisionPositive, 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'

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.

StatusMeaning
400Invalid input, a dispatch failure, or an idempotent replay of an ERROR/DECLINED update. Inspect the response fields and message handle.
401Missing or invalid API credentials.
403App Cards are unavailable for the account’s plan.
404The original outbound App Card was not found, is not owned by the account, or the supplied handle is not the original handle.
408The request timed out. Check message status and retry with the same idempotency key.
409The original card has not reached a send state, or another update is in progress.
429The account’s API rate limit was exceeded.
500An 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.