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, and it carries a status and
an errorCode — nothing more specific than that.
A reaction that was added, one that was removed, and one that was already in the
state you asked for all settle the same way: succeeded, with
errorCode: null. The connector does distinguish those cases internally, but
the distinction is not on the settlement event and there is no endpoint that
reads a message's reaction state, so the stream cannot tell you whether your
command was the one that changed anything.
For most integrations that is the right answer anyway — you asked for the
message to carry a love tapback, and it does. It matters only if you are
counting your own changes or keeping an audit trail, and there the API gives you
nothing to count with.
Genuine failures arrive as status: "failed" with an errorCode. The two worth
handling are stale_target (a newer message arrived, or the target aged out)
and unsupported_capability (the Mac does not offer this reaction command, or
not with the options you used).
tapback_unverified and reaction_unverified are a different case: they arrive
on status: "ambiguous", and mean the reaction may have been applied but could
not be confirmed. Reapplying is harmless for an add — the end state is the
same either way — but do not treat them as failures in a counter or an audit
trail. Treat any errorCode you do not recognise as a generic failure; the list
is not closed. See Errors.