---
title: Polls and Quizzes API
updatedAt: 2026-07-13T15:20:43.000Z
---

Fetch the complete documentation index at: https://docs.watchers.io/llms.txt

# Polls and Quizzes API

You can create and publish polls, quizzes and rating scales via API — full parity with the **Widgets → Polls and Quizzes** admin panel, so external systems can publish engagement materials automatically.

A **poll cluster** is the unit you publish: it targets one or more rooms, carries the scheduling, and contains one question (a *standalone* poll) or several questions in a row (a *series*). Each **poll** inside a cluster is a single question with its **options**.

## Required headers for all endpoints

> ⚠️ **Regional endpoints:** Replace `chatbackend.watchers.io` with the endpoint matching your project region. See [Supported Regions](https://docs.watchers.io/docs/supported-regions).
> | Region | Endpoint |
> | :--- | :--- |
> | Europe (default) | `chatbackend.watchers.io` |
> | North America | `chatbackend.us.watchers.io` |
> | South America | `chatbackend.sa.watchers.io` |
> | Asia | `chatbackend.hk.watchers.io` |
> | Africa | `chatbackend.za.watchers.io` |

| Parameter | Description |
| --- | --- |
| `x-api-key` | Your public API key for the project |
| `Authorization` | `Bearer <token>` from the admin panel (*Settings → Bearer tokens*) for back-to-back integrations |

## Poll types

| Type | Description |
| --- | --- |
| `POLL` | A regular poll — no correct answer. Single or multiple choice (`isMultiple`). |
| `QUIZ` | A poll with correct answer(s). Mark correct options with `isRight: true`. |
| `RATING` | A rating scale. Set `ratingScale` (e.g. 3, 5, 10). |

To any type you can add a free-text **custom answer** option (`isCustom: true`).

## Endpoints

### POST `/external/poll-cluster` — create a poll/series

Creates a poll cluster together with its questions and targets it to rooms.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | No | Internal name of the cluster |
| `isStandalone` | boolean | No | `true` for a single question, `false` (default) for a series |
| `isActive` | boolean | No | Default `true` |
| `roomIds` | string[] | No | External room IDs to publish to (JSON array or CSV file) |
| `startTime` | ISO datetime | No | Scheduled publish time. If omitted — published immediately |
| `endTime` | ISO datetime | No | Auto-finish time |
| `dailyWindowStart` | `HH:MM` | No | Show only after this time each day |
| `dailyWindowEnd` | `HH:MM` | No | Hide after this time each day |
| `polls` | Poll[] | No | The questions (see **Poll object**) |

### GET `/external/poll-cluster` — list

Query parameters: `limit` (default 20), `offset` (default 0), `order` (default `id_desc`). Returns `{ pollClusters, pageCount }`.

### GET `/external/poll-cluster/:id` — get one

Returns the cluster with its `rooms` and `polls.options`.

### PATCH `/external/poll-cluster/:id` — update

Same body as create (all fields optional). Changing scheduling fields reschedules the cluster.

### DELETE `/external/poll-cluster/:id` — delete

### POST `/external/poll-cluster/:id/poll` — add a question to a cluster

Body = **Poll object**.

### PATCH `/external/poll/:pollId` — update a question

Partial **Poll object**. ⚠️ Sending `options` replaces the entire option set: all existing options are deleted and recreated from the array you send, so include every option you want to keep. Omit `options` to leave the current ones unchanged.

### DELETE `/external/poll/:pollId` — delete a question

## Poll object

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `text` | string | Yes | The question |
| `type` | `POLL` \| `QUIZ` \| `RATING` | Yes | See **Poll types** |
| `isMultiple` | boolean | No | Allow multiple selected options. Default `false` |
| `ratingScale` | integer ≥ 1 | For `RATING` | Number of points on the scale |
| `pic` | string | No | Image URL for the question (or upload, see **Media**) |
| `data` | LocalizedText[] | No | Translations of the question text (see **Localization**) |
| `options` | Option[] | Yes | At least 1 (see **Option object**) |

## Option object

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `text` | string | Yes | Answer text |
| `pic` | string | No | Image URL for the answer |
| `isRight` | boolean | No | Correct answer (for `QUIZ`). Default `false` |
| `isCustom` | boolean | No | Turns this option into a free-text input. Default `false` |
| `customPlaceholder` | string | No | Placeholder for the custom input |
| `customMinLength` | integer ≥ 0 | No | Minimum length of the custom text |
| `customMaxLength` | integer ≥ 1 | No | Maximum length of the custom text |
| `data` | LocalizedText[] | No | Translations of the answer text (see **Localization**) |

## Localization

You can create a poll with content in several languages in one request. Both the **Poll object** and the **Option object** accept a `data` field: an array of `{ "lang": "...", "text": "..." }` objects, one per language.

```json
"data": [
  { "lang": "en", "text": "Who will score first?" },
  { "lang": "es", "text": "¿Quién marcará primero?" },
  { "lang": "fr", "text": "Qui marquera en premier ?" }
]
```

The user sees the version matching their interface language, with a fallback to the project default language. The base `text` field is used when no translation matches.

Example of a multilingual poll:

```json
{
  "isStandalone": true,
  "roomIds": ["room-123"],
  "polls": [
    {
      "text": "Who will score first?",
      "type": "POLL",
      "isMultiple": false,
      "data": [
        { "lang": "en", "text": "Who will score first?" },
        { "lang": "es", "text": "¿Quién marcará primero?" }
      ],
      "options": [
        {
          "text": "Team A",
          "data": [
            { "lang": "en", "text": "Team A" },
            { "lang": "es", "text": "Equipo A" }
          ]
        },
        {
          "text": "Team B",
          "data": [
            { "lang": "en", "text": "Team B" },
            { "lang": "es", "text": "Equipo B" }
          ]
        }
      ]
    }
  ]
}
```

The same `data` field works in `PATCH /external/poll-cluster/:id`, `POST /external/poll-cluster/:id/poll` and `PATCH /external/poll/:pollId`.

## Media

Send requests as `multipart/form-data`. Either pass `pic` as a URL string, or upload a file:

- **Poll endpoints** (`/poll`, `/poll/:pollId`): attach the image in the `pic` field.
- **Cluster create/update**: give each poll a temporary `id`, then attach its image in a field named `pic_<id>`. Uploaded images are resized automatically (max 600 px).

## Example — a quiz published to two rooms

```json
{
  "name": "Match prediction",
  "isStandalone": true,
  "roomIds": ["room-123", "room-456"],
  "startTime": "2026-07-01T18:00:00Z",
  "endTime": "2026-07-01T20:00:00Z",
  "polls": [
    {
      "text": "Who scores first?",
      "type": "QUIZ",
      "options": [
        { "text": "Team A", "isRight": true },
        { "text": "Team B", "isRight": false },
        { "text": "Other", "isCustom": true, "customPlaceholder": "Your guess", "customMaxLength": 50 }
      ]
    }
  ]
}
```

## Example — a rating scale

```json
{
  "name": "Post-match rating",
  "isStandalone": true,
  "roomIds": ["room-123"],
  "polls": [
    {
      "text": "Rate the match",
      "type": "RATING",
      "ratingScale": 5,
      "options": [{ "text": "Rating" }]
    }
  ]
}
```
