Polls API
The official Polls plugin has an API. Use it to run a poll from an automation tool such as n8n, with no clicks in Broadcast.
Before you start
- Install the Polls plugin and turn it on in the channel.
- Open the API tokens page and make a token for that channel.
- Give the token the permissions below.
- Write
broadcast.polls/*in the token’s Plugin endpoints field.
| Endpoint | Method | Token permission |
|---|---|---|
create |
POST | Broadcasts write |
list |
GET | Broadcasts read |
results |
GET | Broadcasts read |
close |
POST | Broadcasts write |
delete |
POST | Broadcasts write |
All addresses start with https://your-broadcast-server/api/plugins/broadcast.polls/. Send the token in the Authorization header. A poll belongs to the channel of the token.
Create a poll
curl -X POST https://your-broadcast-server/api/plugins/broadcast.polls/create \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "question": "Should we ship it?", "answers": ["Yes", "Not yet"], "idempotency_key": "launch-poll-2026-11" }'
| Field | Required | Rule |
|---|---|---|
question |
Yes | Text, 1 to 200 characters |
answers |
Yes | A list of 2 to 6 different texts, 1 to 200 characters each |
idempotency_key |
No | 1 to 100 letters, digits or ., _, :, - |
The answer is 201:
{ "poll_id": "should-we-ship-it", "question": "Should we ship it?", "answers": [ { "id": "yes", "label": "Yes" }, { "id": "not-yet", "label": "Not yet" } ], "closed": false, "created_at": "2026-11-01T18:51:15.829Z", "html": "<p><strong>Should we ship it?</strong></p>\n<p><a href=\"{{ plugins.broadcast_polls.actions.vote }}?value=should-we-ship-it:yes\">Yes</a></p>\n<p><a href=\"{{ plugins.broadcast_polls.actions.vote }}?value=should-we-ship-it:not-yet\">Not yet</a></p>" }
html is the block for the email. Leave the merge tag as it is. At send time, Broadcast turns it into a signed link for each person.
Retries
Use an idempotency_key, so that a retry makes no second poll. A repeat with the same key does not create anything. It answers 200 with the same poll and "replayed": true.
| Status | Code | Meaning |
|---|---|---|
| 200 | A retry. You get the first poll, with "replayed": true |
|
| 409 | in_progress |
The first call with this key is still running. Try again |
| 409 | idempotency_key_reused |
The key was used with a different question or different answers |
| 409 | poll_deleted |
The poll of this key was deleted. Use a new key |
| 422 | invalid |
A field is wrong. The message says which |
An error has this form:
{ "error": { "code": "invalid", "message": "answers must be a list of 2 to 6 texts of 1 to 200 characters." } }
Put the poll in a broadcast
Send html as the body of a broadcast. This call needs the Broadcasts write permission too.
curl -X POST https://your-broadcast-server/api/v1/broadcasts \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "broadcast": { "subject": "A quick question", "body": "<p><strong>Should we ship it?</strong></p>...", "content_type": "html" } }'
The links work in broadcasts and sequence emails. They do not work in transactional emails or test sends.
List polls
curl https://your-broadcast-server/api/plugins/broadcast.polls/list \ -H "Authorization: Bearer YOUR_TOKEN"
The answer has polls, a list of up to 100 polls, newest first. Each poll has the same fields as results.
Read the results
Put the poll ID in the query.
curl "https://your-broadcast-server/api/plugins/broadcast.polls/results?poll_id=should-we-ship-it" \ -H "Authorization: Bearer YOUR_TOKEN"
{ "poll_id": "should-we-ship-it", "question": "Should we ship it?", "answers": [ { "id": "yes", "label": "Yes", "count": 29 }, { "id": "not-yet", "label": "Not yet", "count": 11 } ], "closed": false, "created_at": "2026-11-01T18:51:15.829Z", "total": 40, "html": "..." }
An unknown poll answers 404 with the code not_found. A missing or malformed poll_id answers 422.
Close or delete a poll
Send the poll ID in the body. A closed poll keeps its results and takes no more answers.
curl -X POST https://your-broadcast-server/api/plugins/broadcast.polls/close \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "poll_id": "should-we-ship-it" }'
{ "poll_id": "should-we-ship-it", "closed": true }
delete takes the same body and removes the poll and its counts. It answers:
{ "deleted": true, "poll_id": "should-we-ship-it" }
An n8n flow
This flow asks a question each month and posts the result to your team.
- Schedule Trigger. Run on the first day of the month.
- HTTP Request, create. POST to
create. Foridempotency_key, use an expression with the month, such aspoll-{{ $now.format('yyyy-MM') }}. A retry of this step then finds the same poll. - HTTP Request, broadcast. POST to
/api/v1/broadcasts. Setbodyto{{ $json.html }}from the last step. - Webhook. Add a separate workflow with a Webhook trigger, and put its address in a Broadcast webhook endpoint. Tick
plugin.broadcast.polls.poll.answeredunder Plugin events. Now each answer reaches n8n. See Plugin Events and Segment Rules. - Wait. Wait for 7 days in the first workflow.
- HTTP Request, results. GET
resultswithpoll_idset to thepoll_idof step 2. - HTTP Request, close. POST to
close. - Slack. Post
totaland the counts.
In the HTTP Request nodes, use a Header Auth credential with the name Authorization and the value Bearer YOUR_TOKEN, so that the token is not in the workflow.
Check the signature of each webhook in n8n. Webhooks shows how.
Tag the people who answer
Turn on the Tag voters setting of the plugin. A vote then adds the tag poll:<poll id>:<answer id>, such as poll:should-we-ship-it:yes. A changed answer removes the earlier tag. The setting is off by default.
Use the tag to start a sequence or to build a segment. A tag that is a topic is refused, because a topic is consent. The plugin log shows the refusal, and the vote still counts.
Common mistakes
You get 403. The token has the permission, but no broadcast.polls/* in Plugin endpoints. It needs both.
You get 404 on every call. The plugin is off in the channel of the token.
You remove the merge tag from html. Then the link goes nowhere. Keep it as it is, and let the send fill it in.
You make the poll in one channel and send in another. A poll belongs to one channel. Use a token for the channel that sends the email.