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. A card can carry a preview image, but for a plain image or link with no app behind it, the simpler parts are the right tool:

You want…Use
A plain image or link, with no app behind ita rich link or media attachment
A branded, interactive card — optionally with a preview imagean iMessage app (this guide)

Prerequisite: your own iMessage app. A card renders the Messages extension named by team_id + bundle_id, drawing its interactive content from your url — so recipients only get the live experience when that extension is a real, shipping app they have installed. What you supply directly through the API is the static card: the captions and an optional preview image.

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 show a preview image on the card, set layout.image_url — optionally with image_title and image_subtitle overlaid on it:

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",
"image_url": "https://cdn.linqapp.com/example/card-preview.jpg",
"image_title": "Table for 2",
"image_subtitle": "Tonight, 7:30 PM"
}
}
]
}
}'

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 layout field — any of the four captions, or image_url — must be set, or the card renders as an empty bubble.

FieldPosition
captiontop-left, bold (primary label)
subcaptionleft, below caption
trailing_captiontop-right
trailing_subcaptionright, below trailing_caption
image_urlpreview image at the top of the card
image_titlebold text overlaid on the image
image_subtitleoverlaid on the image, below image_title

The small icon shown beside the caption is not a layout field: it’s always the app’s own icon (the installed app’s, or the App Store icon from app_store_id), and can’t be set per message.

image_url — a preview image everyone sees

Section titled “image_url — a preview image everyone sees”

Set layout.image_url to an HTTPS URL of an image (max 2048 chars) and it renders as a preview image at the top of the card — for all recipients, whether or not they have your app installed. The URL is fetched at send time; an unreachable URL, or one that doesn’t resolve to an image, is rejected with a validation error.

Two optional text fields render overlaid on the image (max 512 chars each):

  • image_title — bold, overlaid on the image.
  • image_subtitle — beneath image_title.

Both require image_url — setting either without it is rejected, since there’s no image to overlay.

The image needs an established chat. The preview image renders in chats with inbound activity — the recipient has sent you at least one message. In a brand-new, outbound-only chat the card still delivers, but with captions only.

Updating a card with a new image_url replaces the image in place.

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 static layout card — the captions, plus the preview image when you set image_url — and a Get the app affordance when you set app_store_id. 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 card content you supply directly is the layout — captions and preview image — in either mode.

The image can come from you; the icon always comes from the app. The static card’s image is your layout.image_url. Without one, an image only appears for recipients whose installed extension draws its own from your url. The small icon beside the caption is always the app’s icon (the installed app’s, or the App Store icon from app_store_id) — it isn’t something you set per message.

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.
  • A new layout.image_url swaps the card’s preview image in place — the delivered card redraws with the new image (and any new image_title/image_subtitle).
  • 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.