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.tapbackreaction.apply
Works in groupsYesNo — direct messages only
Which messageAny message you name by externalIdOnly the newest incoming message
Remove a reactionYes, with remove: trueNo
Custom emojiNo — the classic six onlyYes, 1 to 32 characters
ThroughputSuitable for volumeOne 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 · question

Anything 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.

On this page