Reactions (Tapbacks)
iMessage Tapbacks let users react to a specific message with one of several short reactions — a heart, a thumbs-up, an emoji, or a sticker — instead of sending a follow-up message.
Inbound reactions (Mac → CRM) are forwarded via the standard
POST /conversations/messages/inbound call with a structured
reaction block in the bluesendr-metadata:v1
footer embedded in the message body.
Outbound reactions (CRM → Mac) ride inside the existing
POST /crm/webhook request — a reaction block
in the metadata footer tells the Mac to
apply a Tapback to a specific message. See
Outbound: CRM → Mac below.
Three kinds of reaction
| Kind | What it is | Available since |
|---|---|---|
classic | The original 6 Tapbacks: love, like, dislike, laugh, emphasize, question. | macOS 10.12+ |
emoji | Any emoji glyph as a reaction. | macOS 15+ |
sticker | A sticker (Memoji, Genmoji, or pack sticker) as a reaction. | macOS 15+ |
Inbound: Mac → CRM
Reactions arrive via POST /conversations/messages/inbound. The
metadata footer embedded in the
message body carries an extra reaction object:
-- bluesendr-metadata:v1 --
{
"is_group": true,
"chat_id": 680,
"recipients": ["+32456832391", "+32483119453"],
"sender": "+32456832391",
"message_guid": "8F2C6D14-A09E-4133-B84E-72F0E5C9B6A1",
...,
"reaction": {
"action": "add",
"kind": "classic",
"target_message_guid": "1A562E89-FB3A-4F24-8CCE-054FDEC1A9D7",
"type": "like"
}
}
-- end bluesendr-metadata --
The message body itself carries the iMessage system-locale fallback
text (e.g. Liked "Test" in English, or Vond "Test" leuk on a Dutch
Mac). This is fine as a human-readable display fallback for CRMs that
don't render reactions structurally — but never key reaction logic
off the body text. The structured reaction object is locale-
independent and authoritative.
reaction object
| Field | Always present | Notes |
|---|---|---|
kind | yes | "classic" | "emoji" | "sticker". Branch on this before reading kind-specific fields below. |
action | yes | "add" | "remove". (Sticker reactions currently only emit "add" — Apple has not introduced a remove path.) |
target_message_guid | yes | GUID of the message reacted to. Use this for pairing. |
target_message_guid_raw | yes | Unstripped value, kept for debugging. Do not use for pairing. |
type | kind=classic only | One of love, like, dislike, laugh, emphasize, question. |
emoji | kind=emoji only | The emoji glyph as a Unicode string (e.g. "😂"). Populated on both add and remove. |
sticker | kind=sticker only | Object: see Sticker reactions. |
Pairing add / remove
A user can add a Tapback and later remove it. CRMs should pair adds with their corresponding removes by:
(target_message_guid, sender, kind, type-or-emoji-or-sticker.guid)
For classic, the type field disambiguates which of the 6 Tapbacks.
For emoji, pair on kind="emoji" only — the Mac may or may not
populate emoji on remove rows depending on its macOS version, so
don't key off the glyph.
Sender vs contact resolution
metadata.sender is the reactor's handle, not the original
message author. The inbound contactId is resolved using the same
priority as plain group messages — see Sender vs contact resolution
in groups.
Use contactId for contact lookup, metadata.sender for "Bob
reacted to your message" attribution.
Inbound example: classic add
{
"type": "SMS",
"contactId": "<crm contact id>",
"locationId": "<crm location id>",
"message": "Liked \"Test\"\n\n-- bluesendr-metadata:v1 --\n{\"chat_id\":680,\"is_group\":true,\"message_guid\":\"8F2C6D14-A09E-4133-B84E-72F0E5C9B6A1\",\"reaction\":{\"action\":\"add\",\"kind\":\"classic\",\"target_message_guid\":\"1A562E89-FB3A-4F24-8CCE-054FDEC1A9D7\",\"type\":\"like\"},\"recipients\":[\"+32456832391\",\"+32483119453\"],\"sender\":\"+32456832391\",\"service_name\":\"iMessage\"}\n-- end bluesendr-metadata --"
}
Inbound example: classic remove
{
"type": "SMS",
"contactId": "<crm contact id>",
"locationId": "<crm location id>",
"message": "Removed a like from \"Test\"\n\n-- bluesendr-metadata:v1 --\n{\"chat_id\":680,\"is_group\":true,\"message_guid\":\"E0476BBA-9F2C-4A8D-B0E5-3CD8164F291B\",\"reaction\":{\"action\":\"remove\",\"kind\":\"classic\",\"target_message_guid\":\"1A562E89-FB3A-4F24-8CCE-054FDEC1A9D7\",\"type\":\"like\"},\"recipients\":[\"+32456832391\",\"+32483119453\"],\"sender\":\"+32456832391\",\"service_name\":\"iMessage\"}\n-- end bluesendr-metadata --"
}
The CRM should pair this with the previous example to update the reaction state on the target message.
Inbound example: emoji
{
"action": "add",
"kind": "emoji",
"emoji": "😂",
"target_message_guid": "1A562E89-FB3A-4F24-8CCE-054FDEC1A9D7",
"target_message_guid_raw": "p:0/1A562E89-FB3A-4F24-8CCE-054FDEC1A9D7"
}
Body fallback: Reacted 😂 to "Test".
Sticker reactions
Inbound sticker reactions also include the rendered sticker as an
attachment URL in the attachments[] array on the POST /conversations/messages/inbound payload:
{
"action": "add",
"kind": "sticker",
"sticker": {
"guid": "E6D84270-1AF3-4815-ACB1-79D58E91646E",
"mime_type": "image/heic",
"name": "E6D84270-1AF3-4815-ACB1-79D58E91646E.heic",
"short_description": "Sheep waiting in stable, Sheep stable"
},
"target_message_guid": "0AF18B70-3378-43A9-A3FB-AFF3CF1167F9",
"target_message_guid_raw": "bp:0AF18B70-3378-43A9-A3FB-AFF3CF1167F9"
}
| Field | Always present | Notes |
|---|---|---|
guid | yes | Local sticker identifier on the originating Mac. Not portable — do not echo back as a reference for outbound. |
mime_type | best-effort | E.g. "image/heic", "image/png". Best-effort from the source. |
name | best-effort | Filename. |
short_description | best-effort | Human-readable description (often the alt-text for Memoji / Genmoji stickers). |
The attachments[] array on the same inbound payload carries a URL to the
rendered sticker image, suitable for inline rendering. BlueSender
converts non-web-friendly formats (HEIC stickers, animated PNG
stickers) to widely-renderable formats (PNG, animated GIF) before
upload, so CRMs don't have to handle native iMessage formats.
The structured reaction.sticker block remains the authoritative
discriminator; attachments[] is the renderable sibling.
Localization
The body text is iMessage-generated and follows the system locale of the originating Mac. Examples:
| English | Dutch |
|---|---|
Liked "ABCDEFG" | Vond "ABCDEFG" leuk |
Loved "ABCDEFG" | Gaf een hartje aan 'ABCDEFG' |
Removed a like from "ABCDEFG" | Verwijderde een vind-ik-leuk uit 'ABCDEFG' |
Reacted 😂 to "X" | Reageerde met 😂 op "X" |
Sent a sticker to "X" | (locale-equivalent) |
Use the structured reaction object for logic; use the body only
for fallback display.
Outbound: CRM → Mac
CRM-originated reactions ship inside the existing
POST /crm/webhook request — there is no
separate endpoint to call. The Mac detects a reaction block in
the bluesendr-metadata:v1 footer of
the message body and applies the Tapback, returning a
message_status ack on the same channel a regular send-message
would use.
Wire contract
{
"locationId": "<crm location id>",
"contactId": "<crm contact id>",
"phone": "<recipient E.164>",
"messageId": "<your correlation id, becomes hlMessageId on the ack>",
"message": "-- bluesendr-metadata:v1 --\n{\"chat_id\":649,\"reaction\":{\"action\":\"add\",\"kind\":\"classic\",\"target_message_guid\":\"1A562E89-FB3A-4F24-8CCE-054FDEC1A9D7\",\"type\":\"like\"}}\n-- end bluesendr-metadata --"
}
Top-level fields are identical to a regular send-message webhook —
phone, contactId, and messageId are still required for routing
and ack correlation. The message field can be the metadata block on
its own, or you can prefix any display text (e.g. Liked "ABC") that
the CRM should show in its conversation log — anything before the
-- bluesendr-metadata:v1 -- line is purely for CRM display and is
not included on the Tapback itself.
The base64-wrapped sentinel form [[BSMD:v1:<base64-of-the-JSON>]]
is also accepted by the Mac parser if you'd rather not embed the
delimited block inline.
reaction block fields
Same shape as the inbound reaction object — see reaction object
above for the full table. The minimum required fields per kind:
| Kind | Required fields |
|---|---|
classic | kind, action, target_message_guid, type |
emoji | kind, action, target_message_guid, emoji |
sticker | kind, action, target_message_guid, plus a sticker block with either bytes (base64-encoded image) or url (HTTPS image URL — fetched on demand by the Mac). Optional: sticker.name — a human-readable label surfaced in logs and macOS VoiceOver only. Does not cause a lookup against the user's installed stickers; the Mac always renders the image from the supplied bytes / url. If both bytes and url are provided, bytes wins (no extra network round-trip). If neither is provided, the Mac acks INVALID_KIND_FIELD_COMBO. |
target_message_guid is the GUID delivered to the CRM via:
- the inbound
reaction.target_message_guidfield on a previousmessage_received(when reacting to a message that was itself a reaction's target), or - the inbound
message_guidfield on a previousmessage_received(when reacting to a plain message a customer just sent — including in 1:1 chats), or - the outbound message's own GUID when the CRM is reacting to one of its own sends.
The chat_id field on the metadata footer is required so the Mac
knows which conversation to open; it can be reused verbatim from the
inbound metadata of a prior message in the same chat.
Acks
Acks reuse the standard message_status channel keyed by
hlMessageId (= the messageId you sent in the request). Status
values:
delivered— the requested end-state is now true on the Mac.failedwith one of the codes below.
Idempotency
Outbound reaction requests are idempotent and direction-honoring:
the requested action is treated as authoritative, and a request
whose desired end-state is already true is acked delivered as a
no-op.
| Request | Reaction currently present? | Ack |
|---|---|---|
add | yes | delivered (no-op) |
add | no | delivered once applied (or failed) |
remove | yes | delivered once applied (or failed) |
remove | no | delivered (no-op) |
Practical consequences for CRMs:
- Safe to retry. Resending an
addorremoveafter a transient network error or stale local state will not flip the reaction to the wrong direction. actionis authoritative. Pairadd/removepurely on(target_message_guid, kind, type-or-emoji)— adeliveredack means the desired end-state holds.- Apple's single reaction slot still applies. A user can only have
one active reaction per message at a time, regardless of kind.
Sending
add 😂against a message that currently shows❤️from the same user replaces the heart, exactly as it would on a phone. - Scope is per-(kind, type-or-emoji).
add loveandadd likeon the same target are independent — adding aliketo a message that currently carries alovefrom the same user is a state change, not a no-op.
Constraints and error codes by kind
| Kind | Status | Failure codes |
|---|---|---|
classic | Fully supported. add and remove honor direction (see Idempotency above). | INVALID_FRAME, UI_AUTOMATION_DISABLED, plus generic dispatch codes from the Mac-side dispatch table. |
emoji | Fully supported on macOS 26+. Recently-used glyphs land instantly via the inline tapback palette; any other emoji falls through to the full system emoji picker (slower path, same outcome). | EMOJI_NOT_IN_RECENTS when neither the inline palette nor the system picker fallback can land the glyph (legacy name; treat as "couldn't deliver — fall back"). May fire pre- or post-dispatch (see Error codes). Plus the codes above. |
sticker | Fully supported. Provide the image as inline base64 in reaction.sticker.bytes or as an HTTPS URL in reaction.sticker.url (the Mac fetches it). | STICKER_DOWNLOAD_FAILED when a url fetch hits a network failure or non-2xx HTTP status. STICKER_ENCODE_FAILED when the supplied bytes (inline or downloaded) are not a decodable image. STICKER_NOT_FEASIBLE for a build-time kill-switch or when the post-gesture chat-state probe didn't observe a new sticker reaction within the budget. INVALID_KIND_FIELD_COMBO when neither bytes nor url is provided. Treat dispatch failures the same way as other reaction dispatch failures — retry briefly or fall back to a classic Tapback. Plus the codes above. |
| other / unknown | — | NOT_IMPLEMENTED. |
Outbound example: classic add
{
"locationId": "loc_abc",
"contactId": "ct_123",
"phone": "+15555550100",
"messageId": "react-001",
"message": "-- bluesendr-metadata:v1 --\n{\"chat_id\":649,\"reaction\":{\"action\":\"add\",\"kind\":\"classic\",\"target_message_guid\":\"1A562E89-FB3A-4F24-8CCE-054FDEC1A9D7\",\"type\":\"like\"}}\n-- end bluesendr-metadata --"
}
Outbound example: emoji add
{
"action": "add",
"kind": "emoji",
"emoji": "😂",
"target_message_guid": "1A562E89-FB3A-4F24-8CCE-054FDEC1A9D7"
}
Outbound example: classic remove
{
"action": "remove",
"kind": "classic",
"target_message_guid": "1A562E89-FB3A-4F24-8CCE-054FDEC1A9D7",
"type": "like"
}
The CRM should pair this with the previous add request on
(target_message_guid, kind, type). Mac will ack
message_status: delivered once the requested end-state holds — see
Idempotency.
Outbound example: sticker add
{
"action": "add",
"kind": "sticker",
"target_message_guid": "1A562E89-FB3A-4F24-8CCE-054FDEC1A9D7",
"sticker": {
"bytes": "<base64-encoded image>",
"name": "Party Popper"
}
}
Sticker reactions are add-only — Apple's iMessage doesn't model
removing a sticker reaction, so the Mac will reject action: "remove"
with INVALID_KIND_FIELD_COMBO.
The sticker.bytes field carries the exact image to apply as the
reaction (HEIC / PNG / JPEG; the Mac re-encodes as needed). Same size
considerations as the standalone-sticker bytes field — see
Stickers → sticker block fields.
As an alternative, sticker.url accepts an HTTPS image URL. The Mac
fetches it on demand, then runs the same dispatch path as the inline
bytes form. Non-2xx HTTP responses ack STICKER_DOWNLOAD_FAILED;
URLs that return non-image bodies ack STICKER_ENCODE_FAILED. If both
bytes and url are provided, bytes wins and no network call is
made.
{
"action": "add",
"kind": "sticker",
"target_message_guid": "1A562E89-FB3A-4F24-8CCE-054FDEC1A9D7",
"sticker": {
"url": "https://cdn.example.com/stickers/party-popper.png",
"name": "Party Popper"
}
}
Pasting into a CRM message composer
If you're triggering reactions manually from the CRM's iMessage composer (or copy-pasting a test payload during integration), paste the literal multi-line body below — the CRM will forward it to BlueSender as a regular outbound message and the Mac will detect the metadata block and apply the requested Tapback.
Replace chat_id with the chat the target message belongs to (carry
it over from a recent inbound message's metadata) and
target_message_guid with the GUID of the message you want to react
to. Optionally prefix any display text before the
-- bluesendr-metadata:v1 -- line — that text shows up in the CRM
conversation log but is not included on the Tapback.
-- bluesendr-metadata:v1 --
{
"chat_id": 660,
"reaction": {
"action": "add",
"kind": "classic",
"target_message_guid": "7EE3FD68-C2D6-4066-9623-2598202C8A42",
"type": "like"
}
}
-- end bluesendr-metadata --
-- bluesendr-metadata:v1 --
{
"chat_id": 660,
"reaction": {
"action": "remove",
"kind": "classic",
"target_message_guid": "7EE3FD68-C2D6-4066-9623-2598202C8A42",
"type": "dislike"
}
}
-- end bluesendr-metadata --
-- bluesendr-metadata:v1 --
{
"chat_id": 660,
"reaction": {
"action": "add",
"kind": "emoji",
"emoji": "😂",
"target_message_guid": "7EE3FD68-C2D6-4066-9623-2598202C8A42"
}
}
-- end bluesendr-metadata --
The remaining classic type values (love, like, dislike,
laugh, emphasize, question) follow the same shape — swap the
type field. For emoji, swap the emoji field; the Mac uses the
inline tapback palette for recently-used glyphs and falls through
to the full system emoji picker for everything else, so any emoji
should land on macOS 26+. The rare edge cases ack
EMOJI_NOT_IN_RECENTS.
CRM responsibilities checklist
- Strip the metadata footer from displayed message bodies.
- Branch on
reaction.kindbefore reading kind-specific fields. - Pair add and remove by
(target_message_guid, sender, kind, type-or-glyph-or-sticker.guid). - Treat the structured
reactionobject as authoritative — never key off the body text (it's localized). - Display
metadata.senderas the reactor, not the original message author. - Ignore unknown fields and unknown
kindvalues rather than rejecting the payload — the contract may add new fields or kinds over time. - Fall back gracefully on dispatch-failure codes when sending
outbound reactions —
EMOJI_NOT_IN_RECENTS(neither the inline tapback palette nor the full system emoji picker could land the requested glyph; rare on macOS 26+),STICKER_DOWNLOAD_FAILED(URL fetch hit a network error or non-2xx status),STICKER_ENCODE_FAILED(supplied bytes or downloaded content wasn't a decodable image), andSTICKER_NOT_FEASIBLE(real dispatch failure or kill-switched build).
Related
- HTTP Webhooks — both webhook directions.
bluesendr-metadata:v1footer — the schema embedded inmessage.- Stickers — standalone sticker messages (sticker as its own message, not as a reaction).