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.
What you can invoke
Section titled “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
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.
Sending one
Section titled “Sending one”Invoke an experience with the experience key on the message, in place of parts:
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
Section titled “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
Section titled “Rules worth knowing before you send”- A card is the whole message. Apple’s
MSMessagecan’t sit beside text, so a message carries eitherexperienceorparts, 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.
When the recipient isn’t on iMessage
Section titled “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 — 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 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.
:::
Updating a card after you send it
Section titled “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 for the constraints.
agentpay does this on its own: once the payment succeeds, the card turns itself into “Paid” without you sending anything.
Related
Section titled “Related”- Sending payment links — a payment request as a card, or as a link
- iMessage apps — cards backed by your own Messages extension
- Sending messages — parts, recipients, and line selection