---
title: Experiences | API Docs
description: Send a native card into a conversation — a payment request, a wallet prompt, a link — rendered by Linq's iMessage app.
---

An **experience** renders inside Linq’s iMessage app as a native card, instead of as text or a link. You invoke one by name; Linq resolves the recipient, mints any session it needs, composes the card and sends it.

Send it to `POST /v3/chats/{chatId}/messages`:

```
{
  "message": {
    "experience": {
      "name": "agentpay",
      "action": "request_payment",
      "params": { "checkout_url": "https://zero.linqapp.com/pay/acme?session=tok_..." }
    }
  }
}
```

The key is `experience` — what you’re invoking. Nested under it is its `name`, the action you’re invoking on it, and that action’s params. A card **is** the whole message on Apple’s side, so a message carries either `experience` or `parts`, never both, and an action goes to exactly one recipient.

### What you can invoke

| Experience  | Action            | What the customer sees                                                                        |
| ----------- | ----------------- | --------------------------------------------------------------------------------------------- |
| `agentpay`  | `request_payment` | A payment request they can pay in the app. Turns itself into “Paid” in place once it settles. |
| `agentcard` | `attach_card`     | A prompt to add a card to their wallet.                                                       |
| `agentcard` | `approve_card`    | A passkey approval for a virtual card.                                                        |
| `link`      | `open`            | A card that opens a URL you supply.                                                           |

`GET /v3/experiences` is the authoritative list for your account, with every action and the fields each accepts — an action missing there cannot be sent. Fields are display copy unless documented otherwise.

### Params are checked before the card is sent

Unknown fields are **rejected rather than ignored**, so copy that would never have rendered fails for you now instead of arriving wrong on somebody’s phone. Some fields are read rather than sent: `agentpay`’s `request_payment` takes only a `checkout_url` and resolves the amount and reason from that payment request, so a card can never claim a figure the checkout will not charge.

Cards are **iMessage-only**. Recipients without the app see a static version built from the same copy; SMS and RCS recipients cannot receive one at all (error codes 2018 and 4005).

An **experience** is a card that renders inside Linq’s own iMessage app. You invoke one by name and Linq does the rest — resolves the recipient, mints any session the card needs, composes it, and sends it.

This is the counterpart to [iMessage apps](/guides/messaging/imessage-apps/index.md): that page is for partners shipping **their own** Messages extension. An experience needs no app of your own, no App Store release, and no `team_id`/`bundle_id` — you send one field and Linq renders it.

## Sending one

Invoke an experience with the `experience` key on the message, in place of `parts`:

- [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": {
        "experience": {
          "name": "agentpay",
          "action": "request_payment",
          "params": {
            "checkout_url": "https://zero.linqapp.com/pay/acme?session=tok_abc123"
          }
        }
      }
    }'
```

```
await client.chats.messages.send({chatId}, {
  message: {
    experience: {
      name: "agentpay",
      action: "request_payment",
      params: {
        checkout_url: "https://zero.linqapp.com/pay/acme?session=tok_abc123",
      },
    },
  },
});
```

```
client.chats.messages.send(
    {chat_id},
    message={
        "experience": {
            "name": "agentpay",
            "action": "request_payment",
            "params": {
                "checkout_url": "https://zero.linqapp.com/pay/acme?session=tok_abc123",
            },
        },
    },
)
```

```
client.Chats.Messages.Send(context.TODO(), {chatId}, linq.ChatMessageSendParams{
  Message: linq.F(map[string]any{
    Experience: linq.F(map[string]any{
      Name: linq.F("agentpay"),
      Action: linq.F("request_payment"),
      Params: linq.F(map[string]any{
        CheckoutUrl: linq.F("https://zero.linqapp.com/pay/acme?session=tok_abc123"),
      }),
    }),
  }),
})
```

Read it as a chain: **`experience`** is what you’re invoking, **`name`** picks which one, **`action`** is what you’re asking it to do, and **`params`** fills that action in.

Post it to an existing chat with `POST /v3/chats/{chatId}/messages`, or let Linq pick the line and chat with `POST /v3/messages`.

## What you can invoke

| Experience  | Action            | What the customer sees                                                                                              |
| ----------- | ----------------- | ------------------------------------------------------------------------------------------------------------------- |
| `agentpay`  | `request_payment` | A payment request they can pay without leaving the conversation. Becomes “Paid” in the same bubble once it settles. |
| `agentcard` | `attach_card`     | A prompt to add a card to their wallet.                                                                             |
| `agentcard` | `approve_card`    | A passkey approval for a virtual card.                                                                              |
| `link`      | `open`            | A card that opens a URL you supply.                                                                                 |

`GET /v3/experiences` is the authoritative list for **your** account, with every action and the fields each one accepts. An action missing there cannot be sent.

## Rules worth knowing before you send

- **A card is the whole message.** Apple’s `MSMessage` can’t sit beside text, so a message carries either `experience` or `parts`, never both. Copy plus a card is two sends.
- **One recipient.** The card is resolved for the person receiving it, so an action goes to exactly one handle.
- **iMessage only.** Recipients without the app see a static version built from the same copy. SMS and RCS recipients can’t receive one at all — see error codes [2018](/error/codes/2xxx/2018/index.md) and [4005](/error/codes/4xxx/4005/index.md).
- **Unknown params are rejected, not ignored.** A field the action doesn’t declare is a `400`, so copy that would never have rendered fails for you now instead of arriving wrong on someone’s phone.

Some fields are read, not sent

`agentpay`’s `request_payment` takes only a `checkout_url` and reads the amount and reason from that payment request itself. There’s no `amount` field to pass — which is the point: the card can never claim a figure the checkout won’t charge.

## When the recipient isn’t on iMessage

A card is never downgraded to SMS or RCS. If the recipient can’t receive iMessage the send **fails** rather than arriving as something else — a card silently becoming a text would be a worse surprise than a failure you can act on.

Because delivery is asynchronous, that failure does **not** come back on the send call. The send returns `202 Accepted` with `delivery_status: "pending"` as usual, and what happened to the message is confirmed over your [webhooks](/guides/webhooks/events/index.md) — `message.sent` and `message.delivered` when it lands, and **`message.failed`** when it can’t:

```
// message.failed
{
  "chat_id": "564834eb-...",
  "message_id": "3846ae1a-...",
  "code": 4005,
  "reason": "Recipient does not support this message type",
  "service": "iMessage",
  "preferred_service": "iMessage"
}
```

`service: "iMessage"` here is what was *attempted*, not what the recipient supports — read it alongside the code, not on its own. Subscribe to [`message.failed`](/guides/webhooks/events/index.md) rather than polling; the event is published within milliseconds of the failure.

A `4005` means no experience card can reach that person on that device — not this one, not any of them. For a payment, fall back to a [`link` part](/guides/payments/sending-payment-links/index.md): it delivers over SMS and RCS, and the `checkout_url` itself works everywhere. On a supported iPhone it opens the Apple Pay App Clip; on Android, desktop, or any other device the same URL opens the **web checkout in the browser**. Either way they can pay — only the card is iMessage-bound, never the payment.

:::note Explicitly asking for a non-iMessage service **is** caught up front: `preferred_service: "sms"` or `"rcs"` alongside an `experience` card returns [2018](/error/codes/2xxx/2018/index.md) synchronously. That validates the request, not the recipient — an unreachable recipient still surfaces as `4005` after the send. :::

## Updating a card after you send it

An experience action can also rebuild a card **in place** with `POST /v3/messages/{messageId}/update`, using the same `experience` shape instead of a `url`. The card changes in the original bubble for everyone in the conversation — no second message. See [Updating a card in place](/guides/messaging/imessage-apps#updating-a-card-in-place/index.md) for the constraints.

`agentpay` does this on its own: once the payment succeeds, the card turns itself into “Paid” without you sending anything.

## Related

- [Sending payment links](/guides/payments/sending-payment-links/index.md) — a payment request as a card, or as a link
- [iMessage apps](/guides/messaging/imessage-apps/index.md) — cards backed by your own Messages extension
- [Sending messages](/guides/messaging/sending-messages/index.md) — parts, recipients, and line selection
