---
name: simple-poll
description: Post polls and surveys in Slack with Simple Poll and read their results, through Simple Poll's HTTP API, as the Slack user you work for. Use it when you need a decision, a vote, availability, a team pulse, a retro or any other input from several people in a Slack workspace, or the results of a poll your user created. Covers getting an API key with no copy-paste (Connect), the /poll text grammar, dry runs, reading results and responses, plans and errors.
---

# Simple Poll

Simple Poll is the polls and surveys app for Slack. Its HTTP API posts polls as your user and reads their results.

## Quick start

1. **The API.** Base URL `https://simplepoll.rocks/api/v1`. Send `Authorization: Bearer <key>` and JSON bodies. Every error the API handles is `{"ok": false, "error": "..."}`, and `error` says what to do; a wrong HTTP method is a plain 405 and a server error a plain 500.
2. **Get a key** (every plan). If you can read your user's Slack DMs, connect in 3 calls, with no auth:
   1. `POST /connect/start` with `{"team_id": "T...", "user_id": "U..."}`: their workspace ID and Slack member ID, from your Slack tools (`auth.test` on a token that acts as your user returns both). Keep the `request_token` it returns.
   2. Read the code from the DM the Simple Poll app sends your user after your start: "Your code: `K7QM-4D2X`". Check every few seconds, for up to a minute. An older or struck-through code won't redeem.
   3. `POST /connect/redeem` with `{"request_token": "...", "code": "K7QM-4D2X", "client_name": "Claude"}`. It returns `api_key`.

   Otherwise, ask your user to run `/simplepoll api` in Slack and give you the key it shows.
3. **Create a poll** (every plan). Send it with `"dry_run": true` first, check the `poll` that comes back, then send the same body without `dry_run`. Keep the `poll_id` it returns.

   ```sh
   curl -s -X POST https://simplepoll.rocks/api/v1/polls/ \
     -H "Authorization: Bearer $SIMPLE_POLL_API_KEY" -H "Content-Type: application/json" \
     -d '{"channel_id": "C0123ABCD", "text": "\"Ship the release today?\" \"Yes\" \"Hold until Monday\" anonymous", "dry_run": true}'
   ```

4. **Read the results** (Premium): `curl -s https://simplepoll.rocks/api/v1/polls/$POLL_ID/results/ -H "Authorization: Bearer $SIMPLE_POLL_API_KEY"`

The rules that matter:

- Post only where your user is: a channel or group DM they're in (`channel_id` is its ID, like `C...`), or their own DM with Simple Poll (their user ID). Anything else is a 403. See "Where you can post".
- Reading polls needs Premium. On Free, a read is a 403 whose last sentence says how to start the free 14-day trial (or upgrade): pass it to your user unchanged and stop. Only they can do it.
- Never retry a create blindly. A posted poll can't be edited or deleted through the API, and a create that timed out or got a 500 may have posted: check the channel before sending it again.

## Endpoints

| Method and path | Auth | Plan | What it does |
|---|---|---|---|
| `POST /api/v1/connect/start` | None | Every plan | DMs your user a one-time code. |
| `POST /api/v1/connect/redeem` | None | Every plan | Swaps the code for your user's API key. |
| `POST /api/v1/polls/` | Key | Every plan | Posts a poll, or checks it with `dry_run`. |
| `GET /api/v1/polls/<poll_id>/` | Key | Premium | The poll's settings and state. |
| `GET /api/v1/polls/<poll_id>/results/` | Key | Premium | Counts per question. |
| `GET /api/v1/polls/<poll_id>/responses/` | Key | Premium | One row per person who answered, with their answers. |

There are no other endpoints. A key belongs to one Slack user and acts as them: polls you create show them as the creator, and the reads reach only polls they created, in the key's workspace. Successful calls are 200, except connect start (202). Keep the trailing slash on `/polls/` paths. A wrong HTTP method is a plain 405.

### `POST /api/v1/connect/start`

| Field | Value |
|---|---|
| `team_id` | The workspace ID, `T...`. On Enterprise Grid, the ID of the workspace where Simple Poll is installed, not the organization's `E...`. |
| `user_id` | Your user's Slack member ID, `U...` or `W...`. |

Slack's `auth.test` returns both for a token that acts as your user. Most Slack tools also show them: the member ID is on the user's profile, and the workspace ID is in Slack web URLs (`app.slack.com/client/T0123ABCD/...`).

```sh
curl -s -X POST https://simplepoll.rocks/api/v1/connect/start \
  -H "Content-Type: application/json" \
  -d '{"team_id": "T0123ABCD", "user_id": "U0123ABCD"}'
```

```json
{"ok": true, "request_token": "q8Vb3xZ0kP1mN7rT4yW2eA9sD6fG5hJ8kL0zX3cV1bM"}
```

The status is 202. Every well-formed body gets this same answer, whether or not a DM will be sent, so it tells you nothing about the workspace. Keep `request_token` secret and in memory: only it can redeem this request's code. A malformed body (not a JSON object, or an ID that isn't 1 to 32 capital letters and digits) is a 400.

Within a few seconds Simple Poll DMs your user: "You or your AI agent asked to connect to your Simple Poll account. Connecting lets it create polls and, on Premium, read poll results as you. Your code: `K7QM-4D2X`". Look in their DM conversation with the Simple Poll app (or search their messages for "Your code" from Simple Poll) and take the code from a DM that arrived after your start: an older one belongs to another request, and a struck-through one is used up. Check again every few seconds for up to a minute. Only trust a code in a DM from the Simple Poll app itself.

No DM within a minute? The IDs may be wrong, Simple Poll may not be installed in that workspace, your user may not be an active member, Slack may have refused the DM, or they've asked for 5 codes in the last 24 hours (connecting successfully starts that count again). Ask them for the key instead (`/simplepoll api`).

### `POST /api/v1/connect/redeem`

Within 10 minutes of the start.

| Field | Value |
|---|---|
| `request_token` | From the start. |
| `code` | From the DM. Case, the dash and spaces don't matter. |
| `client_name` | Optional. What the DM calls you: 1 to 40 ASCII letters, digits, spaces and `. , ' ( ) _ -`, starting with a letter or digit. |

```sh
curl -s -X POST https://simplepoll.rocks/api/v1/connect/redeem \
  -H "Content-Type: application/json" \
  -d '{"request_token": "q8Vb3xZ0kP1mN7rT4yW2eA9sD6fG5hJ8kL0zX3cV1bM", "code": "K7QM-4D2X", "client_name": "Claude"}'
```

```json
{"ok": true, "api_key": "sp_9f2c4e1a7b3d4c5e8f6a0b1c2d3e4f5a"}
```

Within a few seconds the code DM changes to say "An app or AI agent called `Claude` is now connected to your Simple Poll account", with a Revoke button (or Simple Poll sends that as a new DM). Lost the response? Send the same redeem again before the 10 minutes are up: it returns the same key.

| Status | Meaning | What to do |
|---|---|---|
| 400 | "The request_token or code is wrong, or the request expired or was already used. Start a new request." Five wrong codes end a request too. | Start a new request. |
| 400 | Any other `error` names the field to fix. An unknown field is one (a misspelt `client_name`, say). | Fix it and redeem again. |
| 403 | Simple Poll is no longer installed in the workspace. | Tell your user. |

The key is your user's only key: the same one `/simplepoll api` shows and Connect returns. `/simplepoll api rotate` replaces it, and the dashboard shows it under Settings. Keys work on every plan. Store it as a secret, and never post it in Slack, in a file you share or in logs. A 401 means it was revoked or rotated: get it again.

### `POST /api/v1/polls/`

Posts a poll as your user, on every plan. The body has exactly these fields; any other is a 400, because poll settings go in `text`.

| Field | Value |
|---|---|
| `channel_id` | Where to post. See "Where you can post". |
| `text` | The poll, in the grammar people type after `/poll` (see "The /poll text grammar"). A leading `/poll` is optional. |
| `dry_run` | Optional, `true` or `false`. `true` runs every check a create runs and describes the poll, but posts and saves nothing. |

Build the body with a JSON library rather than by hand: `text` is full of double quotes, and an apostrophe breaks a single-quoted shell string.

| Request | Response (200) |
|---|---|
| `"dry_run": true` | `ok`, `"dry_run": true`, `poll` (what would post), `warnings` (always there, maybe empty), and `notice` on Free. |
| A create | `ok`, `poll_id`, `text`, `channel_id`, `ts`, plus `warnings` when there are any, and `notice` on Free. |
| A create with `schedule ...` in the text | `ok`, `"scheduled": true`, `scheduled_for`, `channel_id`, `text`, plus `warnings` and `notice` as for a create. No `poll_id`. |

`notice` says that reading polls needs Premium, and how to start the trial.

#### Dry run first

A poll reaches everyone in the channel the moment it posts, and the API can't edit or delete it. So send the exact body with `"dry_run": true` first. A dry run runs every check a create runs (the text, the plan, and that your user may post in that channel), and posts and saves nothing. Read `poll` to confirm it's the poll you meant (the question, `options`, `anonymous`, `limit`, the close and the reminders), not just `warnings`, and fix anything in `warnings`. If your user didn't spell out this exact poll, show them the question, options and settings and get a yes. Then send the same body without `dry_run`.

```json
{
  "ok": true,
  "dry_run": true,
  "poll": {
    "channel_id": "C0123ABCD",
    "question": "Ship the release today?",
    "description": null,
    "type_of_poll": "standard",
    "options": [
      {"text": "Yes", "emoji": null, "position": 1},
      {"text": "Hold until Monday", "emoji": null, "position": 2}
    ],
    "anonymous": true,
    "custom_anon_emoji": null,
    "limit": null,
    "allows_options_from_others": false,
    "show_results": "immediately",
    "order_results": "created",
    "scheduled_for": null,
    "scheduled_close": null,
    "reminders": [],
    "mentions": {"broadcast": null, "user_ids": []}
  },
  "warnings": []
}
```

`poll` uses the field names of `GET /api/v1/polls/<poll_id>/`, plus:

| Field | Value |
|---|---|
| `options` | Each `{"text", "emoji", "position"}`. Empty for a survey: its `questions` hold them. |
| `scheduled_for` | When `schedule ...` will post it, or `null` for now. |
| `scheduled_close` | `{"after_seconds": 86400, "at": "..."}`, or `null`. |
| `reminders` | Each `{"when": "after_posting" or "before_closing", "seconds", "at"}`. |
| `mentions` | `{"broadcast": "@here", "@channel" or null, "user_ids": [...]}`. |
| `questions` | Surveys only. Each has `position`, `question`, `type_of_poll`, `required`, `limit` and `options`. |

`question` starts with the `@here` or `@channel` mention, if any, as the poll will post it. A dry run doesn't check that Simple Poll itself can post in the channel, so the create can still be a 424.

#### The create

```json
{
  "ok": true,
  "poll_id": "3f1c2b9e-7d4a-4c1e-9b2f-6a8d0e5c7f21",
  "text": "\"Ship the release today?\" \"Yes\" \"Hold until Monday\" anonymous",
  "channel_id": "C0123ABCD",
  "ts": "1760000000.000100"
}
```

Save `poll_id`: the reads need it. `ts` is the Slack message timestamp, if you want to reply in its thread. A create isn't idempotent: one that timed out or got a 500 may still have posted, so check the channel before you send it again.

#### Posting later: `schedule ...` (Premium)

End the text with `schedule` and a time to post the poll once, later: `schedule tomorrow 9am`, `schedule 15:00`, `schedule 2026-11-02 14:00+01:00`, `schedule 2030-01-15T14:00:00Z`. A time without an offset or `Z` is in your user's Slack timezone (a 400 if they haven't set one). A zone name like `9am PST` doesn't set the timezone either (it's read in your user's), so give an offset or an ISO 8601 time when the zone matters. A time in the past is a 400, and so are words after the time that it doesn't use. Put `schedule ...` last: a modifier after it can be read as part of the time (a 400 tells you).

```json
{
  "ok": true,
  "scheduled": true,
  "scheduled_for": "2026-10-09T09:00:00-07:00",
  "channel_id": "C0123ABCD",
  "text": "\"Lunch on Friday?\" \"Pizza\" \"Tacos\" schedule tomorrow 9am"
}
```

There's no `poll_id`: the poll gets one only when it posts, so you can't read a scheduled poll's results through the API. If you'll need them, post it now instead. Your user gets a DM confirming the schedule (if that DM fails, the poll is still scheduled, and `warnings` says so).

#### Where you can post

- **A channel your user is in**: its ID, `C...` (or `G...`). If Simple Poll isn't in a public channel yet, it joins it to post. A private channel needs Simple Poll as a member: ask your user to type `/invite @Simple Poll` in it.
- **A group DM** your user and Simple Poll are both in: its ID.
- **Your user's own DM with Simple Poll**: their user ID (`U...` or `W...`), or that DM's `D...` ID. Handy for trying a poll out privately.

Anything else is a 403: another person's DM, an archived channel, or a channel your user isn't in or that's private without Simple Poll (that one is worded the same whether or not the channel exists). Channel names like `#general` don't work: Slack shows a channel's ID at the bottom of its About tab, and `conversations.list` returns it.

#### Errors

| Status | Meaning | What to do |
|---|---|---|
| 400 | Bad JSON, an unknown field, text that doesn't parse or that the API refuses (see "What the API refuses"), or a `schedule ...` time in the past or with no timezone. | Fix it using `error`, and dry run again. |
| 401 | Missing, wrong, revoked or rotated key. | Get the key again. |
| 403 | Premium poll features on Free. `error` names them and ends with how to start the trial. | Drop them and dry run again, or pass the last sentence on to your user. |
| 403 | Somewhere you can't post (above), your user's Slack account is deactivated, or Simple Poll is no longer installed. | Pick a channel your user is in, or tell your user. Don't retry. |
| 424 | Simple Poll couldn't confirm your user is in the channel (they're in more than about 10,000 channels), its Slack permissions are out of date, or Slack refused the post. | Do what `error` says: invite Simple Poll, reinstall it, check the channel ID, or post with `/poll` in Slack. |
| 503 | Slack isn't answering. | Retry after `Retry-After` seconds if set, otherwise a few seconds, backing off. |
| 500 | Something broke, and the poll may or may not have posted. | Check the channel before sending it again. |

### `GET /api/v1/polls/<poll_id>/` (Premium)

The poll's settings and state.

| Field | Value |
|---|---|
| `id` | The `poll_id`. |
| `user_id`, `team_id` | Its creator and their workspace. |
| `channel_id`, `ts` | Where it posted, and its Slack message timestamp. |
| `type_of_poll` | `standard`, `ranked_poll`, `scale_rating`, `write_short_text`, `write_short_text_with_voting`, `q&a`, `multiquestion` (a survey) or `brainstorm`. |
| `question`, `description` | The question (a survey's title), and the description or `null`. |
| `is_closed` | `true` once the poll has closed. |
| `anonymous`, `custom_anon_emoji`, `limit`, `allows_options_from_others` | The poll's settings. `limit` is `null` for no limit. |
| `show_results` | `immediately`, or how the results are hidden (`later`, ...). |
| `order_results` | `created` or `most_votes`, or `null` for a type it doesn't apply to. |
| `created_at`, `updated_at` | ISO 8601, with an offset. |
| `text` | The poll as `/poll` text, rebuilt from its settings. It can leave some out (like the close, reminders or mentions), so keep the text you created it with if you'll post it again. |
| `questions` | Surveys only: one entry per question, with its `id`, `question`, `type_of_poll`, `position` and `required`. |
| `results` | Deprecated (and `null` for a survey): use `/results/` and `/responses/`. |

### `GET /api/v1/polls/<poll_id>/results/` (Premium)

Counts per question. Never names anyone, so it's the same for anonymous polls.

```json
{
  "poll_id": "3f1c2b9e-7d4a-4c1e-9b2f-6a8d0e5c7f21",
  "type_of_poll": "standard",
  "respondent_count": 14,
  "questions": [
    {
      "id": "3f1c2b9e-7d4a-4c1e-9b2f-6a8d0e5c7f21",
      "question": "Ship the release today?",
      "type_of_poll": "standard",
      "position": 1,
      "required": true,
      "results": {
        "respondent_count": 14,
        "response_count": 14,
        "options": [
          {"id": "a1b2c3d4e5f6", "text": "Yes", "emoji": null, "position": 1, "vote_count": 9},
          {"id": "f6e5d4c3b2a1", "text": "Hold until Monday", "emoji": null, "position": 2, "vote_count": 5}
        ]
      }
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

A survey has one entry in `questions` per question. By type, `results` holds:

| Type | `results` |
|---|---|
| choice (`standard`, `brainstorm`) | `options`, each with `id`, `text`, `emoji`, `position` and `vote_count`. |
| `scale_rating` | The same `options` (the five points), plus `statistics: {"mean", "median"}`, which is `null` until someone answers. |
| `ranked_poll` | `ranked_results`: the options (`id`, `text`, `emoji`, `position`) in their final order. |
| short text and `q&a` | `responses`, each with `id`, `text`, `created_at`, `user_id` (always `null` here) and `vote_count` (`null` unless people can vote on them). |

Every `results` also has `respondent_count` and `response_count`. The whole poll fits on one page.

### `GET /api/v1/polls/<poll_id>/responses/` (Premium)

One row per person who answered, with their answers. `limit` is 1 to 1000 (default 1000, so most polls fit on one page). When `has_more` is `true`, pass `next_cursor` back as `?cursor=`, unchanged, until it's `false`.

```json
{
  "poll_id": "8c0d5e2a-1b3c-4d5e-9f60-718293a4b5c6",
  "type_of_poll": "multiquestion",
  "respondent_count": 1,
  "data": [
    {
      "user_id": null,
      "responded_at": "2026-10-06T16:05+00:00",
      "answered_count": 2,
      "answers": [
        {"question_id": "51aa0c2e-3d4f-4a5b-8c6d-7e8f9a0b1c2d", "type_of_poll": "scale_rating", "option_ids": null, "rating": 4, "ranking": null, "texts": null},
        {"question_id": "77bc1d3f-4e5a-4b6c-9d7e-8f9a0b1c2d3e", "type_of_poll": "write_short_text", "option_ids": null, "rating": null, "ranking": null, "texts": ["Shipping on Thursday felt calm"]}
      ]
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

`answers` has one entry per question the person answered (a skipped optional question has none), and `answered_count` counts them. Each answer fills one field for its type: `option_ids`, `rating`, `ranking` (option IDs in the person's order) or `texts`. Rows aren't in time order, and `responded_at` is `null` for some old answers. On an anonymous poll `user_id` is `null` and `responded_at` is to the minute, but each row is still one person, so you can relate their answers across questions. Don't try to work out who wrote an anonymous answer.

### Errors on the reads

| Status | Meaning | What to do |
|---|---|---|
| 400 | Not a valid `poll_id`, or a bad `limit` or `cursor`. | Use the `poll_id` the create returned. |
| 401 | Missing, wrong, revoked or rotated key. | Get the key again. |
| 403 | The workspace is on Free: `error` ends with how to start the trial or upgrade. | Pass that last sentence on to your user, unchanged, and stop. |
| 403 | A poll your user didn't create, a poll in another workspace (use a key from that one), a deactivated Slack account, or Simple Poll uninstalled. | Don't retry. |
| 404 | No such poll. | Check the ID. |
| 503 | Slack isn't answering (every call checks your user is still in the workspace). | Retry after `Retry-After` seconds if set, otherwise a few seconds, backing off. |

## Plans

The plan belongs to the workspace, not the key.

| | Free | Premium (and the free trial) |
|---|---|---|
| API keys and Connect | Yes | Yes |
| Create polls, and dry runs | Yes, up to 5 options, without Premium modifiers | Yes, up to 45 options |
| Read a poll, `/results/`, `/responses/` | No: 403 | Yes |

Premium modifiers, a 403 on Free that names them: `limit`, mentioning people (`<@U...>`), `close after ...`, `remind ...`, `schedule ...`, `show results to creator`, more than 5 options, and surveys (`---`). One exception: every year, for the whole of December (UTC), `schedule ...` works on Free too.

On Free, a successful create (or dry run) carries a `notice`, and every Premium 403 ends with what to do next: "Start a free 14-day Premium trial with `/simplepoll trial` in Slack, or upgrade at https://simplepoll.rocks/dashboard/T0123ABCD/billing/" (just the upgrade link when the workspace can't start a trial right now). The trial is free for 14 days (card required, no charge until it ends). Only your user can start it or upgrade: pass that sentence on to them, unchanged, and don't retry until they have.

## The /poll text grammar

```text
"Question" "Option 1" "Option 2" ... modifiers
```

### Quoting

- Put the question and every option in straight double quotes. Curly quotes (`“ ” „ ‟ « »`) count as straight ones.
- There's no escaping. A question or option can't contain a double quote: use single quotes (`'`) or reword.
- Modifiers go after the last quoted string, separated by spaces. Words inside quotes are never read as modifiers, so `"Anonymous or named feedback?"` is a safe question.
- Put nothing after the last quote except modifiers. Other words there are ignored, with a warning, and so is a misspelt modifier: `anonymus` makes a poll that isn't anonymous. In a poll without quoted options, stray words become an option instead, with no warning. So check `poll` itself, not just `warnings`.
- To give an option its own emoji, put the emoji code right after its closing quote: `"Pizza" :pizza: "Tacos" :taco:`.
- Slack formatting works inside quotes: `*bold*`, `:emoji:`, and links written as `<https://example.com|the label>`.
- `@here` and `@channel` become real mentions wherever they appear in a single-question poll, inside quotes too. In a survey, they work only in the header's modifiers, after the quoted title and description. Write them in lowercase, and only when you mean to notify the channel.
- Always use the quoted form. Unquoted forms like `Lunch? Pizza Tacos` exist for humans in a hurry and are easy to get wrong.

With no options, voters add their own (as with `allow-options`): `"What should we call the new bot?"`.

### Poll types

The default is a choice poll: voters pick from your options (any number of them, unless you add `limit`). One of these makes it another type:

| Modifier | Also accepted | Poll |
|---|---|---|
| `ranked` | `ranking`, `ranked poll` | Voters put the options in order. They see the results only once the poll closes. |
| `scale` | `scaled`, `scale rating poll` | A rating from 1 to 5. Give no options, or exactly 5 labels from worst to best. Results include the mean and median. |
| `short text` | `write a short text` | Free-text answers. Don't give options. |
| `short text :fire:` | `write a short text :fire:` | Free-text answers people can also vote on with that reaction, one of `:thumbsup:` `:bulb:` `:heart:` `:joy:` `:tada:` `:rocket:` `:fire:` `:small_red_triangle:`. |
| `q&a` | `ask me anything`, `ama`, `questions and answers` | People post questions and upvote them; the most upvoted rise to the top. Don't give options. |

### Modifiers

Spellings are forgiving: case doesn't matter in modifier words, and spaces, `-` or `_` between words all work (`allow-options`, `allow options`, `allow_options`). Use the first spelling in each row. Write `@here`, `@channel` and emoji codes in lowercase: `@HERE` and `:FIRE:` aren't recognized.

| Modifier | Also accepted | Effect | Plan |
|---|---|---|---|
| `anonymous` | | Nobody, your user included, sees who answered what. | Free |
| `allow-options` | `add options`, `allow options from others` | Voters can add their own options. | Free |
| `order-results-by-votes` | `order by votes`, `show results by popularity` | Options are listed by votes instead of in your order. | Free |
| `hide results` | `keep results hidden`, `keep hidden`, `show results later`, `show results on close` | Voters can't see the results while the poll is open. On Premium your user can (and anyone they share them with in Slack), and voters see them only if your user closes the poll in Slack and reveals them: a poll that closes on its own keeps them hidden. On Free nobody sees them until the poll is closed in Slack, which reveals them. | Free |
| `show results to creator` | `show results to author only` | Like `hide results`, said as a promise: only your user (and anyone they share them with) sees the results. | Premium |
| `limit 1` | `limit` (means 1), `limit 2`, `limit to 2 votes` | Each person can pick at most that many options (`limit 0` counts as `limit 1`). Must not exceed the number of options, unless `allow-options` is on. Without it, people can pick as many as they like. | Premium |
| `close after 1 day` | `close in 30 mins`, `close poll automatically after 2 hours` | The poll closes on its own. | Premium |
| `remind 2 hours after posting` | `remind after 2 hours`, `repost in 2 hours` | Reposts the poll at the bottom of the channel. Must fall before the close. | Premium |
| `remind 1 hour before closing` | `repost 1 hour before close` | Reposts the poll before it closes. Needs `close after`. | Premium |
| `@here`, `@channel` | `mention @here` | Notifies the channel when the poll posts. | Free |
| `<@U0123ABCD>` | several, space-separated | Notifies those people. Use Slack's `<@USER_ID>` form; a bare `@name` doesn't work. Use either people or `@here`/`@channel`: the last one written wins. | Premium |
| `schedule tomorrow 9am` | `schedule 15:00`, `schedule 2026-11-02 14:00+01:00`, `schedule 2030-01-15T14:00:00Z` | Posts the poll once, later (see "Posting later"). Put it last. | Premium |
| `optional` | `not required` | In a survey, that question can be skipped. | Premium (surveys) |

Allowed durations, with units `minutes` (`minute`, `mins`, `min`, `m`), `hours` (`hour`, `hrs`, `hr`, `h`) or `days` (`day`, `d`). Any other value isn't recognized:

- `close after`: 10, 30 or 60 minutes; 1, 2, 3, 4, 5, 6, 12 or 24 hours; 1, 2, 3 or 7 days.
- Reminders, after posting or before closing: 5, 10, 15, 30 or 60 minutes; 1, 2, 3, 12 or 24 hours; 1 day.

Not possible in the text: sending a poll to other people by DM, closing at a set date and time, repeating a poll (your user can set that up in Slack), and changing a poll after it's posted.

### Surveys: several questions in one poll (Premium)

Separate sections with `---` (outside quotes, with a space on each side). The first section is the header: a title, an optional description and the survey-wide modifiers. Each section after it is one question with its options and its own type.

```poll
"Sprint 42 retro" "Two minutes, anonymous." anonymous close after 2 days
--- "How did the sprint go?" scale
--- "What went well?" short text
--- "What should we change?" short text optional
--- "Keep the Thursday demo?" "Yes" "No" limit 1
```

- The header takes a title and an optional description, then the survey-wide modifiers: a third quoted string is a 400.
- Survey-wide, in the header only: `anonymous`, hidden results, mentions, `close after`, reminders and `schedule ...`.
- Per question: the type (choice, `ranked`, `scale` or `short text`; a `q&a` question is a 400), options, `limit 1` and `optional`. A choice question needs options.
- A choice question is single-choice (`limit 1`) or unlimited: `limit 2` and up are a 400 (unless that's all its options, which is unlimited anyway).
- Questions can't use `allow-options` or `order-results-by-votes` (400), a `short text` question can't have a voting reaction (400), ranked questions can't be `optional` (400), and option emoji are ignored (a warning).
- Line breaks around `---` are fine.

### What the API refuses

`/poll` quietly drops a few things that don't work, and shows the result in a preview. The API has no preview, so it refuses them with a 400 that names the problem and the fix: a reminder that would fall after the close (or before closing with no close, or before the poll even posts), an unreadable `schedule` time, words after `schedule ...` that the time doesn't use, modifiers around the options of an unquoted question (`Lunch? "Pizza" "Tacos" anonymous`: quote the question), hiding results on a poll type that always shows them, a ranked poll without options, a description over 2,000 characters or a text over 100,000, and in a survey anything from the list above in the wrong place.

What it ignores without changing the poll comes back in `warnings` instead, and the poll is created: `no-preview` (the API never previews), options on a `short text` poll, a `scale` with other than 5 labels, `optional` outside a survey, `limit` or `allow-options` on a type that doesn't take them, `order-results-by-votes` on a type that doesn't take it, option emoji in a survey, and words it didn't recognise (including other words around an unquoted question's options).

### Limits

- Options per poll or question: 5 on Free (more is a 403, like any Premium feature), 45 on Premium (the hard limit: more is a 400).
- Questions per survey: 20.
- `limit N` can't be more than the number of options (400), unless `allow-options` is on. `limit N` equal to it does nothing.

## Errors

Every error the API handles is `{"ok": false, "error": "..."}`, and `error` says what went wrong and what to do. A wrong HTTP method is a plain 405, and a server error a plain 500.

| Status | Meaning | What to do |
|---|---|---|
| 400 | The request is wrong: bad JSON, an unknown field (on a create or a redeem), text that doesn't parse or that the API refuses, a bad `poll_id`, `limit` or cursor, or a connect request that's wrong or used up. | Fix it using `error`, and dry run again. For connect, start a new request. |
| 401 | Missing, wrong, revoked or rotated key. | Get a key again (Connect or `/simplepoll api`). |
| 403 | Not allowed: a read on Free, Premium poll options on Free (`error` names them), a channel your user can't post in, a poll they didn't create, a poll from another workspace, a deactivated Slack account, or Simple Poll uninstalled. | Plan errors: pass the last sentence on to your user, or drop the Premium options and dry run again. Otherwise don't retry. |
| 404 | No such poll. | Check the ID. |
| 424 | Slack refused or couldn't confirm: it wouldn't post the poll, or Simple Poll couldn't confirm your user is in the channel, or it needs reinstalling for its permissions. | Do what `error` says: invite Simple Poll, check the channel ID, reinstall, or tell your user. |
| 500 | Something broke on Simple Poll's side, and the outcome is unknown. | Before repeating a create, check whether the poll posted. |
| 503 | Slack isn't answering. | Retry after `Retry-After` seconds if set, otherwise a few seconds, backing off. |

## Recipes

### Writing good polls

- Ask one thing, and put the decision in the question: "Which launch date works for you?" beats "Launch date".
- Two to five short options that don't overlap. If you might have missed one, add `allow-options`.
- People can pick several options unless you add `limit 1` (Premium). Use it when you need one answer per person.
- Make anything about feelings or feedback `anonymous`, so people answer honestly.
- Give it an end with `close after ...` (Premium): results only settle once it's closed.
- One poll per decision. Don't post the same poll twice.

### A decision poll that closes itself

1. Dry run, then create:

   ```poll
   "Which launch date works for you?" "Oct 20" "Oct 22" "Oct 27" limit 1 close after 1 day remind 2 hours before closing @here
   ```

2. After the close, check `GET /api/v1/polls/<poll_id>/` shows `"is_closed": true`, then read `/results/` and report the winner with its votes. On a tie or a low turnout, say so and let your user decide.

### An anonymous retro, with the answers grouped

1. Dry run, then create:

   ```poll
   "Sprint 42 retro" "Anonymous. Two minutes." anonymous close after 2 days
   --- "What went well?" short text
   --- "What didn't?" short text
   --- "What should we try next sprint?" short text optional
   ```

2. Once it has closed, read `/responses/`. For each question, group the answers into a handful of themes, count them, and quote one or two answers word for word. Never guess at who wrote what.
3. Offer a follow-up vote on the top suggestions, with the options in quotes: `"What should we try first?" "Shorter standups" "No-meeting Wednesdays" "Demo every Friday" ranked close after 1 day`.
