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.sendandkind: "poll". - Read it with poll events —
poll.created,poll.options_changed,poll.votedandpoll.vote_cleared— on a stream opened with?events=pollsor 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:
{
"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.
removeon apoll.voteis422 validation_failedatpayload.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.