---
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** — not for sending an image or a link.

| You want…                                               | Use                                                                                                                        |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| An image everyone can see                               | a [rich link](/guides/messaging/rich-link-previews/index.md) or [media attachment](/guides/messaging/attachments/index.md) |
| 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 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.

## 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 always show the static `layout` card — even to recipients who have your app — set `interactive: false`:

- [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",
            "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 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](#how-the-card-renders)).

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

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

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