Reactions
Choosing between the two reaction commands, and reading the outcome.
There are two reaction commands, and the first thing to settle is which one you need. They are not two versions of the same thing: they target different messages, work in different places, and one of them cannot remove a reaction at all.
Which command
reaction.tapback | reaction.apply | |
|---|---|---|
| Works in groups | Yes | No — direct messages only |
| Which message | Any message you name by externalId | Only the newest incoming message |
| Remove a reaction | Yes, with remove: true | No |
| Custom emoji | No — the classic six only | Yes, 1 to 32 characters |
| Throughput | Suitable for volume | One at a time per Mac |
Use reaction.tapback. It is the default for
every case except one.
reaction.apply earns its place for a single
reason: it is the only command that can react with an arbitrary emoji. If you
are applying one of the six classic tapbacks, there is nothing it does that
tapback does not do better.
The classic six
Both commands accept the same six reaction names, which are the tapbacks Messages itself offers:
love · like · dislike · laugh · emphasis · questionAnything outside that set fails, but not where you would expect: payloads are
not validated at the API boundary, so an unrecognised reaction still comes
back 202 and then settles failed on the stream. An arbitrary emoji goes
through reaction.apply instead, and needs a Mac that advertises custom-emoji
support — see Capabilities.
Reacting to a message
You react to a message by its externalId, which is the iMessage GUID that
arrives on the message event. Keep that
value alongside the conversationId when you process inbound messages; you
need both to react.
curl $MAPIER_BASE_URL/v1/commands/reaction.tapback \
-H "Authorization: Bearer $MAPIER_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: react-guid-1-love" \
-d '{
"target": {
"kind": "conversation",
"conversationId": "a0000000-0000-4000-8000-000000000001"
},
"payload": { "reaction": "love", "targetMessageExternalId": "guid-1" }
}'Reactions take the agent-turn routing path, so the Idempotency-Key above is
honoured: a retry adopts the original command rather than reacting twice. A key
built from the message id and the reaction, as here, is naturally unique per
intent — but the constraint is permanent, so if you later remove that reaction
and want to put it back, the same key adopts the finished command and nothing
happens. Add something that changes to the key whenever a repeat is a real
intent rather than a retry. See Idempotency.
The response is a 202 — the command is queued, not applied. The outcome
arrives on the response stream.
React promptly
stale_target is the failure you will actually hit. The executor checks that the target GUID is
still present in the conversation's bounded history before it fires, and fails closed if it is
not. React while the message is recent — a reaction issued hours later, or after a long backlog,
may find nothing to attach to.
Removing a reaction
Same command, same target message, plus remove:
{ "reaction": "love", "targetMessageExternalId": "guid-1", "remove": true }You must name the reaction you are removing, not only the message. Removal is
only available on reaction.tapback; reaction.apply has no equivalent, which
is one more reason to prefer tapback if there is any chance you will want to
undo.
Custom emoji
reaction.apply reacts to whichever message is currently newest and
incoming in a direct message. Because that target moves as new messages
arrive, you name the message you believe is newest, and the command refuses to
run if it has been overtaken — it settles stale_target instead, so you never
react to the wrong bubble.
curl $MAPIER_BASE_URL/v1/commands/reaction.apply \
-H "Authorization: Bearer $MAPIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"target": {
"kind": "conversation",
"conversationId": "a0000000-0000-4000-8000-000000000001"
},
"payload": {
"emoji": "🎉",
"expectedNewestIncomingMessageExternalId": "guid-1"
}
}'The payload has a second branch that takes reaction with one of the classic
six in place of emoji. It is there for completeness; there is no reason to
choose it over tapback.
reaction.apply does not scale
This command drives the Mac's user interface directly, and only one such reaction runs at a time
per host. A burst of them queues up behind itself and slows everything else on that Mac. If you
are reacting at any volume, use reaction.tapback.
Reading the outcome
Exactly one command event settles each reaction. There are four possible
outcomes, and two of them are not failures even though they are not
succeeded:
| Outcome | Settles as | What happened |
|---|---|---|
applied | succeeded | The reaction was added |
removed | succeeded | The reaction was removed |
already_reacted | no_effect | That reaction was already there |
not_reacted | no_effect | You asked to remove one that was not there |
no_effect means nothing changed because nothing needed to change. The end
state of the conversation is exactly what you asked for, so treat it as success
unless your application specifically cares whether it was the one that made the
change — a counter, say, or an audit trail.
reaction.apply has only two outcomes, applied and already_reacted, with
the same reading.
Genuine failures arrive as status: "failed" with an errorCode. Besides
stale_target, the ones worth handling are tapback_unverified and
reaction_unverified (the reaction may have been applied but could not be
confirmed) and unsupported_capability (the Mac does not offer this reaction
command, or not with the options you used). Treat any errorCode you do not
recognise as a generic failure — the list is not closed. See
Errors.