Skip to content
LinqCopy agent prompt
Messaging

Experiences

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.

ExperienceActionWhat the customer sees
agentpayrequest_paymentA payment request they can pay in the app. Turns itself into “Paid” in place once it settles.
agentcardattach_cardA prompt to add a card to their wallet.
agentcardapprove_cardA passkey approval for a virtual card.
linkopenA 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

Section titled “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: 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.

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

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

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.

ExperienceActionWhat the customer sees
agentpayrequest_paymentA payment request they can pay without leaving the conversation. Becomes “Paid” in the same bubble once it settles.
agentcardattach_cardA prompt to add a card to their wallet.
agentcardapprove_cardA passkey approval for a virtual card.
linkopenA 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.

  • 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 and 4005.
  • 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.

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 webhooksmessage.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 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: 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 synchronously. That validates the request, not the recipient — an unreachable recipient still surfaces as 4005 after the send. :::

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 for the constraints.

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