Messages

Rich links

Send a URL as a preview card, why you do not control how it looks, and why nothing can name the message afterwards.

The rich_link payload sends a single URL and asks Messages to render it as a preview card — the tile with a title, an image and a domain, rather than a line of blue text. An iMessage line — live or test — sends one as a message.send whose payload has kind: "rich_link".

{
  "kind": "rich_link",
  "url": "https://example.com/orders/4821"
}

That is the whole payload. kind and url, nothing else — a text, a caption or any other key beside them is 422 validation_failed at that key.

URL constraints

Prop

Type

Three things are worth checking before you post:

  • Absolute, with a scheme. https://example.com/orders/4821 works; example.com/orders/4821 and /orders/4821 do not. Only http and https are accepted — no mailto:, tel: or app schemes.
  • 2048 characters, ceiling included. Long tracking parameters get there faster than you expect. Shorten the URL rather than trimming query strings you need.
  • No control characters. Anything below 0x20, plus 0x7f, is refused. If you are interpolating a URL out of user input, strip whitespace and newlines first.

You do not control the preview

This is the part that surprises people. You send a URL and only a URL. The recipient's phone fetches the destination itself and builds the card from what the page publishes — its Open Graph tags, its title, its image. There is no field for a title, a description, a thumbnail or a fallback.

Practically:

  • A destination with good og:title, og:description and og:image tags gets a full card.
  • A destination with none of them gets a thin card, often no more than the domain.
  • A destination behind a login, a paywall or a bot-blocking WAF may render as little more than a URL — the phone is fetching the page as an anonymous client, not as your authenticated user.

If the card matters to you, the work is on the destination page, not in the payload. And test the exact URL you intend to send: a redirect to a different host can produce a different card than the final URL would.

Nothing reports back what the card looked like. A succeeded settlement means the line sent the link, not that a preview rendered — there is no field on the response stream describing the card.

Its id

On a live line the Mac names the rich link it sent, as it names a text. The command frame that settles the send carries Mapier's messageId and an externalId, the line reports message.delivered (and message.read when the phone sends one), and a later command can name the message: a tapback, a sticker stuck to it, a reply, an edit or an unsend.

A rich link that settled with externalId: null keeps none, and no later command can name it. That is every rich link sent before the line's Mac was updated to name them, which also stays sent, and every rich link on a test line.

A rich link travels on the line's conversation path and nowhere else — the path a thread takes once the transport has confirmed it is a conversation with that one contact, which happens the first time the contact writes into it, and the path a group chat the transport has reported takes, where the card reaches every member. See which path a send takes.

Into any other thread it is refused before anything is written: 422 validation_failed at target.conversationId — or at the recipient key you wrote — with reason: conversation_not_confirmed. Send the person a text first; once they answer, the thread is confirmed and the card goes. A group conversation the transport has not named yet is refused at target.conversationId too. A rich link to a line on any other channel is 404 unsupported_command with reason: not_on_channel.

A reply is target.replyToMessageId, as on any send: a rich link can quote the message the contact sent.

What the line may still refuse

What the line's device can send is not known before a rich link is sent. A line whose device has not announced rich links — or replies on a rich link, for a reply — is sent nothing: the command is accepted with a 202 and then settles failed on the stream with capability_constraint_violation, failureClass: capability. Watch the stream, and fall back to the URL as text rather than retrying unchanged. See Capabilities.

In test mode the sandbox phone draws a rich link as the link beside a card naming its host, and fetches nothing. Live, the recipient's iPhone fetches the page and draws its title and picture — so what the card says is something to check on a real handset, not in the sandbox.

A worked example

A receipt link into a conversation the contact opened:

curl -X POST https://api.mapier.ai/v1/commands/message.send \
  -H "Authorization: Bearer $MAPIER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-4821-receipt-link" \
  -d '{
    "target": {
      "kind": "conversation",
      "conversationId": "a0000000-0000-4000-8000-000000000001"
    },
    "payload": {
      "kind": "rich_link",
      "url": "https://example.com/orders/4821"
    }
  }'

Validate the URL client-side all the same: the API refuses a malformed one at payload.url with 422 validation_failed, and a refusal caught before the request is one fewer round trip.

Versus a URL in plain text

You can also put a URL inside an ordinary { text } payload, and it will still be tappable. The difference is how it is sent: rich_link asks Messages to compose the message as a link, whereas a text payload sends characters and leaves the rendering entirely to the receiving client — and a text goes into any thread, a confirmed one or not.

Use rich_link when the link is the message. Use plain text when the URL is one part of a sentence — rich_link has no text field, so you cannot say anything alongside it. If you want both, send two messages.

On this page