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.
When to use an app card
Section titled “When to use an app card”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 see | a rich link or media attachment |
| A branded, interactive card for users who have your app | an 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 yoururl— 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.
Constraints
Section titled “Constraints”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 amessage.failedwebhook carrying4005(RecipientUnsupportedMessageType). Check capability before sending. - Must be the only part. An
imessage_apppart cannot be combined withtext,media, orlinkparts in the same message.
Sending an iMessage app
Section titled “Sending an iMessage app”Send one as the first message in a new chat with Create Chat:
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" } } ] } }'await client.chats.create({ 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", }, }, ], },});client.chats.create( 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", }, }, ], },)client.Chats.Create(context.TODO(), linq.ChatNewParams{ From: linq.F("+12052535597"), To: linq.F([]string{"+12052532136"}), Message: linq.F(map[string]any{ Parts: linq.F([]any{ map[string]any{ Type: linq.F("imessage_app"), App: linq.F(map[string]any{ Name: linq.F("Example App"), TeamId: linq.F("A1B2C3D4E5"), BundleId: linq.F("com.example.app.MessageExtension"), }), Url: linq.F("https://app.example.com/card?id=abc123"), FallbackText: linq.F("Open in Example App"), Layout: linq.F(map[string]any{ Caption: linq.F("Example App"), Subcaption: linq.F("You said: hello"), }), }, }), }),})To send into an existing chat, post the same part to Send Message:
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" } } ] } }'await client.chats.messages.send({chatId}, { 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", }, }, ], },});client.chats.messages.send( {chat_id}, 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", }, }, ], },)client.Chats.Messages.Send(context.TODO(), {chatId}, linq.ChatMessageSendParams{ Message: linq.F(map[string]any{ Parts: linq.F([]any{ map[string]any{ Type: linq.F("imessage_app"), App: linq.F(map[string]any{ Name: linq.F("Example App"), TeamId: linq.F("A1B2C3D4E5"), BundleId: linq.F("com.example.app.MessageExtension"), }), Url: linq.F("https://app.example.com/card?id=abc123"), FallbackText: linq.F("Open in Example App"), Layout: linq.F(map[string]any{ Caption: linq.F("Example App"), Subcaption: linq.F("You said: hello"), }), }, }), }),})To always show the static layout card — even to recipients who have your app — set interactive: false:
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" } } ] } }'await client.chats.messages.send({chatId}, { 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", }, }, ], },});client.chats.messages.send( {chat_id}, 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", }, }, ], },)client.Chats.Messages.Send(context.TODO(), {chatId}, linq.ChatMessageSendParams{ Message: linq.F(map[string]any{ Parts: linq.F([]any{ map[string]any{ Type: linq.F("imessage_app"), App: linq.F(map[string]any{ Name: linq.F("Example App"), TeamId: linq.F("A1B2C3D4E5"), BundleId: linq.F("com.example.app.MessageExtension"), }), Url: linq.F("https://app.example.com/card?id=abc123"), FallbackText: linq.F("Open in Example App"), Interactive: linq.F(false), Layout: linq.F(map[string]any{ Caption: linq.F("Example App"), Subcaption: linq.F("You said: hello"), }), }, }), }),})The imessage_app part
Section titled “The imessage_app part”app — which extension backs the card
Section titled “app — which extension backs the card”| Field | Required | Description |
|---|---|---|
name | yes | Display name, shown by Messages’ fallback UI (1–64 chars). |
team_id | yes | The app’s 10-character uppercase team identifier. |
bundle_id | yes | Bundle identifier of the Messages app extension. |
app_store_id | no | App 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_iddon’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 and fallback_text
Section titled “url and fallback_text”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 theurlto 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(defaulttrue) — whether the card renders as your app’s live, interactive experience for recipients who have your app installed. Leave ittruefor the rich in-Messages experience; set it tofalseto always show the staticlayoutcard 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.
layout — what the recipient sees
Section titled “layout — what the recipient sees”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.
| Field | Position |
|---|---|
caption | top-left, bold (primary label) |
subcaption | left, below caption |
trailing_caption | top-right |
trailing_subcaption | right, 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, orimageSubtitle? 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.
How the card renders
Section titled “How the card renders”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 yoururl— 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 theurl. 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 staticlayoutcard instead of the live experience — the same card a recipient without the app would see. - Doesn’t have the app → they see your
layoutcaptions, plus a Get the app affordance when you setapp_store_id. No image renders. Theinteractiveflag 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 fromapp_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.
Receiving iMessage apps
Section titled “Receiving iMessage apps”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.
Updating a card in place
Section titled “Updating a card in place”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:
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_appcard you sent — inbound cards can’t be updated (400). - The card must already be delivered — if you get a
409, retry after itsmessage.deliveredwebhook. - Only
url,fallback_text,interactive, andlayoutchange. 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
interactiveon the update — sendinteractive: falseto convert a live card to a static one, orinteractive: trueto convert it back.interactivedefaults totruewhen omitted and is not inherited from the original card, so re-sendinteractive: falseon 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.failedlifecycle. To update again, reference the new message id.