message.read

Mark a one-to-one conversation read on the line — the contact sees "Read" only when the line's Apple ID has read receipts on. Fire-and-forget, never retried.

POST /v1/commands/message.read

Marks a one-to-one conversation read on the line — the whole conversation, in one the contact has written into. The contact sees "Read" under their message only when the line's Apple ID has read receipts turned on. With them off, the command still settles succeeded and the contact sees nothing.

It is not a queued command. On an iMessage line the transport serves it beside typing.start, as a route of its own, fire-and-forget — nothing is journaled or replayed — and the platform settles the command itself from that route's answer. So the 202 is followed by a command frame, type: "message.read", messageId: null.

On a live line Idempotency-Key is ignored, as on typing.start: the same key again is a new signal, and the key comes back as the settlement's logicalActionId. Nothing coalesces repeats either — every message.read is sent. In test the sandbox honours the key.

Target

Prop

Type

On an iMessage line, live or test, the conversation at target.conversationId must be one the transport has named and confirmed is with that one person, as for typing.start. Each of these is 422 validation_failed at target.conversationId before anything is sent:

  • a group;
  • a conversation the transport has never named;
  • one it has named but not confirmed, with reason: conversation_not_confirmed — send the person a text first;
  • a recipient address instead of a conversation: a read is done in a conversation.

Payload

Prop

Type

payload may be {} or absent. payload.externalId is a 422 validation_failed at payload.externalId when it is not an id or names a message of another conversation. The whole conversation is marked read either way.

What happens

The 202 says the signal was accepted. What the command frame then says:

  • succeeded once the transport took the signal — never a promise that the person saw it.
  • failed, with the code of what the transport answered instead: not_found, rate_limited or upstream_unavailable (the line's device is offline). It is never retried.

Every message.read is sent, none held back inside a window, because one held back would leave a message that arrived since the last unread.

The budget it shares with typing.start

typing.start and message.read share the transport's budget of 12 signals per conversation in 60 seconds, counted from the first: past it either is accepted and settles failed with rate_limited without being sent. typing.start's own five-second window is asked first, so a typing signal held back inside it spends none of the budget. Marking a conversation read and then showing the typing bubble spends two signals.

This is the line reading the contact's messages. The contact reading the line's arrives as before: a receipt frame on a stream opened with ?events=receipts, and the message.read webhook — the same name as this command, and a different thing. See Receipts and delivery verdicts.

In test

A test iMessage line accepts message.read after the same refusals and settles it succeeded, with nothing drawn: the sandbox phone shows no "Read", and the shared budget is not rehearsed there.

Request

curl https://api.mapier.ai/v1/commands/message.read \
  -H "Authorization: Bearer $MAPIER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "target": {
      "kind": "conversation",
      "conversationId": "a0000000-0000-4000-8000-000000000001"
    }
  }'

A common shape: when a message frame arrives, mark the conversation read, then show the typing bubble while the answer is written.

Response

202 Accepted
{
  "commandId": "cmd-10",
  "status": "queued",
  "disposition": "queued",
  "lineId": "0192a7b0-3e4f-7051-8c6d-7e8f9a0b1c2d",
  "lineSelection": "reused_active_chat"
}

Then, on the response stream:

event: command — succeeded
{
  "commandId": "cmd-10",
  "logicalActionId": "…",
  "status": "succeeded",
  "errorCode": null,
  "conversationId": "a0000000-0000-4000-8000-000000000001",
  "type": "message.read",
  "messageId": null,
  "failureClass": null
}

Errors

A command answers the one set of HTTP refusals POST /v1/commands/{type} publishes, so that table is the one to handle. What is particular to this type:

  • 422 validation_failed at target.conversationId for a group, a conversation the transport has never named, or one it has not confirmed — the last with reason: conversation_not_confirmed — and for a recipient address.
  • 422 validation_failed at payload.externalId when it is not an id or names a message of another conversation.
  • 404 unsupported_command with reason: not_on_channel on a line whose channel does not carry it.

Settlement codes you will see for this command: not_found, rate_limited, upstream_unavailable. See Error codes.

On this page