Skip to content
Get started
Messaging

iMessage Apps

Send interactive messages backed by an iMessage app (a Messages app extension).

An iMessage app is a Messages app extension — a mini-app that runs inside Messages. With the imessage_app message part you send a tappable card that opens your app at a URL you provide. Use it to hand a conversation off into an interactive experience — a game move, a checkout, an RSVP — that lives inside Messages.

iMessage apps are sent as a message part with type: "imessage_app", in place of the text, media, and link parts you already use.

App cards are for branded, interactive experiences backed by your own iMessage app — not for sending an image or a link.

You want…Use
An image everyone can seea rich link or media attachment
A branded, interactive card for users who have your appan iMessage app (this guide)

Prerequisite: your own iMessage app. A card renders the Messages extension named by team_id + bundle_id, drawing its content from your url — so that extension must be a real, shipping app the recipient has installed. There is no way to supply the card’s content (image, etc.) directly through the API.

iMessage app parts have stricter rules than other parts:

  • iMessage only. They never fall back to SMS or RCS. If you explicitly request SMS or RCS alongside an app part, the send is rejected with 2018 (IMessageAppServiceUnsupported). If the recipient simply isn’t reachable over iMessage, the send is accepted and then fails asynchronously with a message.failed webhook carrying 4005 (RecipientUnsupportedMessageType). Check capability before sending.
  • Must be the only part. An imessage_app part cannot be combined with text, media, or link parts in the same message.

Send one as the first message in a new chat with Create Chat:

Terminal window
curl -X POST https://api.linqapp.com/api/partner/v3/chats \
-H "Authorization: Bearer $LINQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "+12052535597",
"to": [
"+12052532136"
],
"message": {
"parts": [
{
"type": "imessage_app",
"app": {
"name": "Example App",
"team_id": "A1B2C3D4E5",
"bundle_id": "com.example.app.MessageExtension"
},
"url": "https://app.example.com/card?id=abc123",
"fallback_text": "Open in Example App",
"layout": {
"caption": "Example App",
"subcaption": "You said: hello"
}
}
]
}
}'

To send into an existing chat, post the same part to Send Message:

Terminal window
curl -X POST https://api.linqapp.com/api/partner/v3/chats/{chatId}/messages \
-H "Authorization: Bearer $LINQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": {
"parts": [
{
"type": "imessage_app",
"app": {
"name": "Example App",
"team_id": "A1B2C3D4E5",
"bundle_id": "com.example.app.MessageExtension"
},
"url": "https://app.example.com/card?id=abc123",
"fallback_text": "Open in Example App",
"layout": {
"caption": "Example App",
"subcaption": "You said: hello"
}
}
]
}
}'

To always show the static layout card — even to recipients who have your app — set interactive: false:

Terminal window
curl -X POST https://api.linqapp.com/api/partner/v3/chats/{chatId}/messages \
-H "Authorization: Bearer $LINQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": {
"parts": [
{
"type": "imessage_app",
"app": {
"name": "Example App",
"team_id": "A1B2C3D4E5",
"bundle_id": "com.example.app.MessageExtension"
},
"url": "https://app.example.com/card?id=abc123",
"fallback_text": "Open in Example App",
"interactive": false,
"layout": {
"caption": "Example App",
"subcaption": "You said: hello"
}
}
]
}
}'
FieldRequiredDescription
nameyesDisplay name, shown by Messages’ fallback UI (1–64 chars).
team_idyesThe app’s 10-character uppercase team identifier.
bundle_idyesBundle identifier of the Messages app extension.
app_store_idnoApp Store id (integer). When set, recipients without the app installed see a Get the app affordance.

The identity is the rendering key, not just a label: the card becomes the app you name and is drawn by that app’s extension, so you normally pass your own app’s identity.

An unrecognized identity silently renders as plain text. If team_id + bundle_id don’t match a Messages extension the recipient has installed, the card falls back to caption text with no error — the usual cause of “my card shows text only.” Verify the identity against your shipping app.

  • url — an HTTPS URL the backing app’s extension reads to render the card. It’s opaque to Messages; the extension interprets it and draws the rich content from it (for example, a reservation app might resolve a specific listing from a query parameter and render its photo and details). Change the url to change what the card shows. Max 2048 characters.
  • fallback_text — text shown where the card can’t render (notifications, lock screen). Defaults to the caption when omitted.
  • interactive (default true) — whether the card renders as your app’s live, interactive experience for recipients who have your app installed. Leave it true for the rich in-Messages experience; set it to false to always show the static layout card instead — even to recipients who have your app. Recipients without your app always see the static card regardless of this flag. See How the card renders.

The message renders as a card. At least one of caption, subcaption, trailing_caption, or trailing_subcaption must be set, or it renders as an empty bubble.

FieldPosition
captiontop-left, bold (primary label)
subcaptionleft, below caption
trailing_captiontop-right
trailing_subcaptionright, below trailing_caption

These four caption fields are the complete set of layout properties — there are no others. In particular there’s no field for an image, or for the small icon shown beside the caption: both come from the app, not the request (details below).

Looking for image, mediaFileURL, imageTitle, or imageSubtitle? Those fields belong to your app’s own Messages extension, not this API. They only render when an app composes the message on-device through its extension — they can’t be set here, and a sender-supplied image (even delivered as an attachment) won’t render.

What a recipient sees depends on whether they have the backing app installed and on the interactive flag:

  • Has the app, interactive: true (default) → the app’s Messages extension renders a rich, interactive card from your url — photo, details, and any interactive UI. The platform doesn’t draw this; the extension does, keyed off the app identity (team_id + bundle_id) and the url. If the identity doesn’t match an extension the recipient has, the card falls back to text.
  • Has the app, interactive: false → they see the static layout card instead of the live experience — the same card a recipient without the app would see.
  • Doesn’t have the app → they see your layout captions, plus a Get the app affordance when you set app_store_id. No image renders. The interactive flag makes no difference here.

This is why a card “renders the app inside the bubble” by default: that is the extension drawing your url. Set interactive: false when you’d rather everyone see the same static caption card — for example a status update that shouldn’t open an interactive surface. The only card content you supply directly is the caption text in either mode.

The image and icon come from the app, not you. A card has no image field, and the platform never injects one — the image is always the installed app’s, drawn from your url. The small icon beside the caption is the app’s own icon as well (the installed app’s, or the App Store icon from app_store_id); it isn’t something you set per message. Recipients without the app see your captions only. For an image everyone can see, use a rich link or media attachment instead.

Inbound messages that contain an iMessage app carry an imessage_app part in the message.received webhook payload, in place of the usual text, media, and link parts. The part mirrors the structure above, so your handler can read the app identity, url, and layout of a card a recipient sends you.

A delivered iMessage app card can be replaced in place — useful for live sessions like a game move redrawing the board, or an order status that changes after delivery. Send the new content to Update App Card, referencing the original message:

Terminal window
curl -X POST https://api.linqapp.com/api/partner/v3/messages/{messageId}/update \
-H "Authorization: Bearer $LINQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://app.example.com/card?game=7f3a&move=2",
"fallback_text": "Score update",
"layout": {
"caption": "Score: 2 – 1"
}
}'

The update inherits the original card’s app identity and replaces the delivered card rather than posting a second bubble. A few rules:

  • The referenced message must be an imessage_app card you sent — inbound cards can’t be updated (400).
  • The card must already be delivered — if you get a 409, retry after its message.delivered webhook.
  • Only url, fallback_text, interactive, and layout change. The app identity (team_id, bundle_id, name) is fixed for the life of the card.
  • You can switch a card between interactive and static in place by setting interactive on the update — send interactive: false to convert a live card to a static one, or interactive: true to convert it back. interactive defaults to true when omitted and is not inherited from the original card, so re-send interactive: false on each update to keep a static card static.
  • The update is delivered as a new message with its own id and its own message.sent / message.delivered / message.failed lifecycle. To update again, reference the new message id.