# Polls API

> Create a poll, put it in a broadcast and read the results with an API token. Request and response examples, and a complete n8n flow.

Source: https://sendbroadcast.net/docs/plugins-polls-api

> Coming in Broadcast 3.0. Until then, this page applies only to the Upcoming (edge) build.

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

1. Install the Polls plugin and turn it on in the channel.
2. Open the API tokens page and make a token for that channel.
3. Give the token the permissions below.
4. 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

```bash
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`:

```json
{
  "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:

```json
{ "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.

```bash
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

```bash
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.

```bash
curl "https://your-broadcast-server/api/plugins/broadcast.polls/results?poll_id=should-we-ship-it" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

```json
{
  "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.

```bash
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" }'
```

```json
{ "poll_id": "should-we-ship-it", "closed": true }
```

`delete` takes the same body and removes the poll and its counts. It answers:

```json
{ "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.

1. **Schedule Trigger.** Run on the first day of the month.
2. **HTTP Request, create.** POST to `create`. For `idempotency_key`, use an expression with the month, such as `poll-{{ $now.format('yyyy-MM') }}`. A retry of this step then finds the same poll.
3. **HTTP Request, broadcast.** POST to `/api/v1/broadcasts`. Set `body` to `{{ $json.html }}` from the last step.
4. **Webhook.** Add a separate workflow with a Webhook trigger, and put its address in a Broadcast webhook endpoint. Tick `plugin.broadcast.polls.poll.answered` under **Plugin events**. Now each answer reaches n8n. See [Plugin Events and Segment Rules](https://sendbroadcast.net/docs/plugins-segment-rules).
5. **Wait.** Wait for 7 days in the first workflow.
6. **HTTP Request, results.** GET `results` with `poll_id` set to the `poll_id` of step 2.
7. **HTTP Request, close.** POST to `close`.
8. **Slack.** Post `total` and 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](https://sendbroadcast.net/docs/webhooks) shows how.

## Tag the people who answer

Turn on the **Tag voters** setting of the plugin. A vote then adds the tag `poll::`, 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.
