---
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](/guides/messaging/rich-link-previews/index.md) or [media attachment](/guides/messaging/attachments/index.md) |
| 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.

## 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)](/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)](/error/codes/4xxx/4005/index.md). [Check capability](/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](/api/resources/chats/methods/create/index.md):

- [cURL](#tab-panel-69)
- [TypeScript](#tab-panel-70)
- [Python](#tab-panel-71)
- [Go](#tab-panel-72)

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

- [cURL](#tab-panel-73)
- [TypeScript](#tab-panel-74)
- [Python](#tab-panel-75)
- [Go](#tab-panel-76)

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-77)
- [TypeScript](#tab-panel-78)
- [Python](#tab-panel-79)
- [Go](#tab-panel-80)

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-81)
- [TypeScript](#tab-panel-82)
- [Python](#tab-panel-83)
- [Go](#tab-panel-84)

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

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

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

- [cURL](#tab-panel-68)

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.
