Tapbacks
Tapbacks are iMessage's inline reactions — the heart, thumbs-up, "haha", eyes, and friends that attach to a specific message bubble. Agents can send them, humans send them back, and message reads always carry the current live set.
Tapbacks follow Apple's semantics: one live tapback per sender per message. Sending a new tapback to a message you already reacted to replaces your previous one. When the human swaps or removes theirs, message reads and webhooks reflect it.
Send tapback POST
POST /reactionsReact to a message a human sent in one of your agent's 1:1 or ready group conversations. iMessage has no concept of reacting to your own messages, so an agent can only tapback an inbound message — targeting one of the agent's own messages returns 422 in either conversation type.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
message_id | UUID | Yes | The message being reacted to |
reaction | string | Yes | One of "love", "like", "dislike", "laugh", "emphasize", "question", or "eyes". "eyes" displays as 👀. Arbitrary emoji and "custom" are not valid outbound values |
part_index | integer | No | Part of a multi-part message to react to. Defaults to 0 |
Request example
Response (201)
If the agent already had a live tapback on that message part, it is replaced — exactly what the human sees happen on their device.
For a group reaction, assignment_id is null and remote_number identifies the participant who sent the target message. The request and the rest of the response use the same shape as 1:1.
Error responses
| Status | Description |
|---|---|
| 400 | The conversation's identity is not enabled for iMessage, or the target message cannot accept reactions yet |
| 403 | A current recipient is blocked by a contact rule or has opted out (recipient_opted_out) |
| 404 | Message not found, or not visible to the caller |
| 409 | The recipient disconnected, group_conversation_not_ready because the group is not ready on its current dedicated outbound number, or current group membership cannot be resolved consistently |
| 422 | Unknown reaction value (including an arbitrary emoji or "custom"), invalid part_index, or the target is the agent's own message (only the human's messages can be reacted to) |
| 502 | Upstream delivery failure — safe to retry |
| 503 | group_membership_unavailable because current membership could not be verified; retry with backoff |
| 504 | Reaction could not be sent before the request deadline; delivery did not begin, so it is safe to retry |
Receiving tapbacks
Tapbacks from humans in 1:1 and group conversations arrive two ways:
- On message reads — every message object carries a
reactionsarray with the live tapbacks targeting it, oldest first. - As webhooks —
imessage.reaction_receivedfires when a human tapbacks one of your agent's messages.
Humans can react with any emoji, not just the classic six. Those arrive with reaction: "custom" and the literal emoji in custom_emoji:
Arbitrary custom-emoji tapbacks are receive-only. Outbound sends accept the classic six plus "eyes"; pass the named "eyes" value rather than the literal 👀 emoji.
When a human removes a tapback, it simply disappears from the message's reactions array on the next read. Removals do not fire a webhook.
Group tapbacks use the same target-message semantics: every reaction attaches to one target_message_id and part_index, a new reaction from the same sender replaces their previous reaction on that message part, and a removal clears it. On group reactions, assignment_id is null and remote_number identifies the participant associated with the reaction.
Reaction object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Reaction ID |
conversation_id | string (UUID) | The conversation containing the target message |
assignment_id | string (UUID) | null | The 1:1 connection carrying the conversation; null for group reactions |
target_message_id | string (UUID) | The message this tapback targets |
direction | string | "inbound" (from the human) or "outbound" (from the agent) |
reaction | string | "love", "like", "dislike", "laugh", "emphasize", "question", "eyes", or "custom" |
custom_emoji | string | null | The literal emoji when reaction is "custom"; null for named outbound reactions, including "eyes" |
remote_number | string | The human's phone number (E.164) |
part_index | integer | Message part the tapback targets (0 for single-part messages) |
created_at | string (ISO 8601) | Creation timestamp |
updated_at | string (ISO 8601) | Last update timestamp |
The compact reaction entries embedded in a message's reactions array omit the conversation/assignment/target IDs (they're implied by the message) and the updated_at field.