Messages

Polls

Send a poll into a conversation, vote on a poll — yours or a contact's — and read every vote as an event you opt into.

An iMessage line can send a poll, vote on one, and hear every vote cast in it. Three pieces do it:

  • Send a poll with message.send and kind: "poll".
  • Read it with poll events — poll.created, poll.options_changed, poll.voted and poll.vote_cleared — on a stream opened with ?events=polls or at a webhook endpoint subscribed to them by name. They carry the ids a vote needs, and arrive for a contact's poll as well as yours.
  • Vote with poll.vote, naming the poll and the choice by those ids.

Sending a poll

{
  "kind": "poll",
  "question": "Lunch?",
  "options": ["Tacos", "Ramen", "Pho"]
}

Prop

Type

Nothing else goes beside them: any other key, a text included, is 422 validation_failed at that key before anything is written. So is a question or a choice outside its bounds — at payload.question, payload.options or the choice, payload.options[i] — and a choice given twice. A question or options beside any other kind is a 422 at that key too.

The 128-character cap is deliberate: a longer poll would send, but would never come back with the ids its choices were given, and without them nobody can vote on it through the API.

Where a poll can go

A poll travels on the line's conversation path alone: into a one-to-one conversation the transport has confirmed is with that one person, which it does the first time they write into it, or into a group chat it reported. A conversation not confirmed yet is 422 validation_failed with reason: conversation_not_confirmed — send the person a text first. A poll to a line on any other channel is 404 unsupported_command with reason: not_on_channel.

A reply is target.replyToMessageId, as on any send.

A worked send

curl https://api.mapier.ai/v1/commands/message.send \
  -H "Authorization: Bearer $MAPIER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: team-lunch-poll-1001" \
  -d '{
    "target": {
      "kind": "conversation",
      "conversationId": "a0000000-0000-4000-8000-000000000001"
    },
    "payload": {
      "kind": "poll",
      "question": "Lunch?",
      "options": ["Tacos", "Ramen", "Pho"]
    }
  }'

The 202 is followed by a command frame for the send. succeeded means the poll was sent. When the frame carries an externalId, it is the poll's id — the same pollExternalId its events carry.

What the line's device can send is not known before a poll is sent. A device that has not announced polls is sent nothing: the command is accepted with a 202 and then settles failed with capability_constraint_violation (or capability_not_negotiated), failureClass: capability.

Getting the ids

A sent poll comes back as a poll.created with actor: "line", carrying the question and each choice with the id it was given:

event: poll
data: {"type":"poll.created","conversationId":"a0000000-0000-4000-8000-000000000001","actor":"line","contactId":null,"pollExternalId":"2F6B1C3D-8E4A-4B5C-9D7E-0A1B2C3D4E5F","question":"Lunch?","options":[{"optionExternalId":"A1B6D8F0-9C2E-4D5F-8A4B-6C8D0E1F2A3B","text":"Tacos"},{"optionExternalId":"B2C7E9A1-0D3F-4E6A-8B5C-7D9E1F2A3B4C","text":"Ramen"},{"optionExternalId":"C3D8F0B2-1E4A-4F7B-9C6D-8E0F2A3B4C5D","text":"Pho"}],"occurredAt":"2026-10-01T18:11:40.000Z","simulated":false}

Poll events are the only place the choices' ids arrive, so ask for them before you send: open the stream with ?events=polls, or subscribe a webhook endpoint to poll.created and the other three by name — they are not in a new endpoint's default set. A poll a contact creates, in a one-to-one conversation or a group's, arrives the same way, with actor: "contact" and their contactId. Option ids are opaque: store them and pass them back exactly as given. See Poll events.

Voting

Vote with poll.vote, naming the conversation, the poll and one choice:

curl https://api.mapier.ai/v1/commands/poll.vote \
  -H "Authorization: Bearer $MAPIER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: lunch-poll-vote-ramen" \
  -d '{
    "target": { "conversationId": "a0000000-0000-4000-8000-000000000001" },
    "payload": {
      "pollExternalId": "2F6B1C3D-8E4A-4B5C-9D7E-0A1B2C3D4E5F",
      "optionExternalId": "B2C7E9A1-0D3F-4E6A-8B5C-7D9E1F2A3B4C"
    }
  }'

The line can vote on its own poll or on a contact's, in a one-to-one conversation or a group's. Once it has voted, a poll.voted with actor: "line" carries its whole selection.

A vote adds its choice. An iMessage poll lets each person pick several, so voting for another choice does not take the first one back. Vote for Ramen, then for Pho, and the second poll.voted reads both:

poll.voted, after the second vote
{
  "type": "poll.voted",
  "conversationId": "a0000000-0000-4000-8000-000000000001",
  "actor": "line",
  "contactId": null,
  "pollExternalId": "2F6B1C3D-8E4A-4B5C-9D7E-0A1B2C3D4E5F",
  "selectedOptionExternalIds": [
    "B2C7E9A1-0D3F-4E6A-8B5C-7D9E1F2A3B4C",
    "C3D8F0B2-1E4A-4F7B-9C6D-8E0F2A3B4C5D"
  ],
  "occurredAt": "2026-10-01T18:12:09.000Z",
  "simulated": false
}

To change the line's answer you would have to take the first vote back, which is not available yet, so vote only for the choices you want the line to hold. The settlements and refusals are on the poll.vote reference.

Reading the votes

Every vote in the poll — the contact's and the line's — arrives as a poll.voted carrying that voter's whole current selection, and a vote taken back as poll.vote_cleared. Keep one selection per voter, keyed by actor and contactId, and replace it on each poll.voted. A group member nobody has written to one to one has contactId: null, so two such members' votes cannot be told apart. A choice someone adds arrives as poll.options_changed with the whole list of choices. The rules are on Poll events.

A contact's question also arrives as a message

When a contact creates a poll, Messages sends its question as ordinary text beside it, so the question also reaches you as a message frame and a message.received webhook from them — whether or not you asked for poll events. It is the same question, not a second one: answer it once. The poll itself and each vote are not delivered as messages, though one can occasionally arrive as a message with no text. See Poll events.

Not available yet

  • Taking back the line's own vote. remove on a poll.vote is 422 validation_failed at payload.remove.
  • Adding a choice to a poll that exists. A contact can, and you hear it as poll.options_changed; the line cannot.
  • Sending a poll on any channel but iMessage.

In test

A test line rehearses the line's side of a poll. A poll it sends reaches the sandbox phone and comes back as poll.created with actor: "line", and a poll.vote on it comes back as poll.voted, each with "simulated": true. Nobody on the sandbox phone creates polls or votes, so a contact's poll and their votes are not rehearsed.

On this page