---
title: iMessage Apps | API Docs
description: 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

App cards are for **branded, interactive experiences backed by your own iMessage app**. A card can carry a [preview image](#image_url--a-preview-image-everyone-sees), 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 it                  | a [rich link](/channel/imessage/guides/messaging/rich-link-previews/index.md) or [media attachment](/channel/imessage/guides/messaging/attachments/index.md) |
| A card, but no iMessage app of your own                       | an [experience](/channel/imessage/guides/messaging/experiences/index.md) — Linq hosts and renders it                                                         |
| A branded, interactive card — optionally with a preview image | an **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.
>
> No extension of your own? Send an [experience](/channel/imessage/guides/messaging/experiences/index.md) instead — a card Linq renders in its own iMessage app, invoked with the `experience` key.

## 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)](/channel/imessage/error/codes/2xxx/2018/index.md). 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)](/channel/imessage/error/codes/4xxx/4005/index.md). [Check capability](/channel/imessage/guides/messaging/protocol-selection#protocol-capabilities/index.md) before sending.
- **Must be the only part.** An `imessage_app` part cannot be combined with `text`, `media`, or `link` parts in the same message.

## Sending an iMessage app

Send one as the first message in a new chat with [Create Chat](/channel/imessage/api/resources/chats/methods/create/index.md):

- [cURL](#tab-panel-0-0)
- [TypeScript](#tab-panel-0-1)
- [Python](#tab-panel-0-2)
- [Go](#tab-panel-0-3)

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"
            }
          }
        ]
      }
    }'
```

```
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](/channel/imessage/api/resources/chats/subresources/messages/methods/send/index.md):

- [cURL](#tab-panel-1-0)
- [TypeScript](#tab-panel-1-1)
- [Python](#tab-panel-1-2)
- [Go](#tab-panel-1-3)

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"
            }
          }
        ]
      }
    }'
```

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

- [cURL](#tab-panel-2-0)
- [TypeScript](#tab-panel-2-1)
- [Python](#tab-panel-2-2)
- [Go](#tab-panel-2-3)

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"
            }
          }
        ]
      }
    }'
```

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

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

```
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"),
          ImageUrl: linq.F("https://cdn.linqapp.com/example/card-preview.jpg"),
          ImageTitle: linq.F("Table for 2"),
          ImageSubtitle: linq.F("Tonight, 7:30 PM"),
        }),
      },
    }),
  }),
})
```

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

- [cURL](#tab-panel-3-0)
- [TypeScript](#tab-panel-3-1)
- [Python](#tab-panel-3-2)
- [Go](#tab-panel-3-3)

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"
            }
          }
        ]
      }
    }'
```

```
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

### `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_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` 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 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. Its content also affects whether the card renders as a card at all — see [How the card renders](#how-the-card-renders).
- **`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](#how-the-card-renders).

### `layout` — what the recipient sees

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.

| Field                 | Position                                   |
| --------------------- | ------------------------------------------ |
| `caption`             | top-left, bold (primary label)             |
| `subcaption`          | left, below `caption`                      |
| `trailing_caption`    | top-right                                  |
| `trailing_subcaption` | right, below `trailing_caption`            |
| `image_url`           | preview image at the top of the card       |
| `image_title`         | bold text overlaid on the image            |
| `image_subtitle`      | overlaid on the image, below `image_title` |

image\_url

image\_title

image\_subtitle

app

caption

trailing\_caption

subcaption

trailing\_subcaption

> 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

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](#updating-a-card-in-place).

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

### When a card renders as plain text instead

A card can drop to a plain text bubble even when the identity is right and the app is installed. The card carries a **message body**, resolved as the first of `fallback_text` → `caption` → `app.name`. When that body contains specific times, dates, addresses, phone numbers, or day words like “today”/“yesterday”, the card doesn’t render — the recipient sees the body as an ordinary text message instead.

Provide a clean `fallback_text` (e.g. `"Open in the app"`) and it’s always the body, so nothing else can reach it — `caption`, `subcaption`, and `app.name` are free to carry rich detail. Omit `fallback_text` and the body falls through to `caption`, so the same rule applies there.

| Avoid in `fallback_text` | Use instead        |
| ------------------------ | ------------------ |
| “Starts at 11:30 AM”     | “Meeting reminder” |
| “123 Example Ave”        | “Location inside”  |
| “Call 555 0100 back”     | “Tap to call back” |
| “Due Jan 15”             | “Deadline inside”  |
| “Delivered yesterday”    | “Delivery update”  |

The other cause is an [unrecognized app identity](#app--which-extension-backs-the-card) — if `team_id` + `bundle_id` don’t match an installed extension, the card falls back to caption text with no error.

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

## Receiving iMessage apps

Inbound messages that contain an iMessage app carry an `imessage_app` part in the [`message.received`](/channel/imessage/guides/webhooks/events/index.md) 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

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](/channel/imessage/api/resources/messages/methods/update_app_card/index.md), referencing the original message:

- [cURL](#tab-panel-4-0)
- [TypeScript](#tab-panel-4-1)
- [Python](#tab-panel-4-2)
- [Go](#tab-panel-4-3)

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"
      }
    }'
```

```
await client.messages.updateAppCard({messageId}, {
  url: "https://app.example.com/card?game=7f3a&move=2",
  fallback_text: "Score update",
  layout: {
    caption: "Score: 2 – 1",
  },
});
```

```
client.messages.update_app_card(
    {message_id},
    url="https://app.example.com/card?game=7f3a&move=2",
    fallback_text="Score update",
    layout={
        "caption": "Score: 2 – 1",
    },
)
```

```
client.Messages.UpdateAppCard(context.TODO(), {messageId}, linq.MessageUpdateAppCardParams{
  Url: linq.F("https://app.example.com/card?game=7f3a&move=2"),
  FallbackText: linq.F("Score update"),
  Layout: linq.F(map[string]any{
    Caption: linq.F("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.
