# Управление опросом

## История публикаций и восстановление

Требуют право участника **`settings`**: история и возврат — это правка опроса.

---

### 61. История публикаций опроса

*Метод в API с 2026-09-08.*

Каждая публикация оставляет снимок. Черновик в историю не входит — это текущее
состояние, а не точка в ней.

**HTTP:** `GET /quiz/{id}/versions`

| Параметр | Тип | Описание |
|---|---|---|
| `page` | integer | Страница, по умолчанию 1. |
| `per_page` | integer | 1–50, по умолчанию 20. Снимок весит немало: это полная структура опроса. |

Список публикаций приходит полем `items`, рядом — `total`, `page`, `per_page`,
`last_page`.

---

### 62. Вернуть опрос к прежней публикации

*Метод в API с 2026-09-08.*

**HTTP:** `POST /quiz/{id}/versions/{versionId}/restore`

Возврат ложится **в черновик**: респонденты продолжают видеть опубликованную
версию, пока опрос не опубликуют заново. В ответе это отражено полем
`published: false`.

```json
{"status": true, "quiz_id": 18547, "version_id": 90412, "published": false, "quiz": {...}}
```

Неизвестная версия, версия другого опроса или попытка вернуть текущий черновик —
`400` с кодом `restore_failed` и пояснением в `message`. Опрос под оплаченной
кампанией за респондентов — `423`, см. «Заморозка опроса кампанией» ниже.

---

## Заморозка опроса кампанией

*С 2026-09-18.*

Пока по опросу идёт оплаченная кампания за респондентов, его состав менять нельзя:
респонденты проходят то, что оплачено и промодерировано. До оплаты правки открыты —
сумма пересчитывается перед платежом.

Под замком: `POST /quiz/{id}/save`, `POST /quiz/{id}/publish`,
`POST /quiz/{id}/conditions`, `POST /quiz/{id}/versions/{versionId}/restore`.
Настройки опроса (`POST /quiz/{id}/settings/info`), интеграции, темы и работа с
ответами замком не закрыты — так же, как в конструкторе.

Отказ — `423` с состоянием замка:

```json
{
    "status": false,
    "editing_lock": {
        "locked": true,
        "can_edit": false,
        "reason": "respondent_campaign",
        "campaign": {"id": 7, "title": "Кампания #1", "status": "in_progress"}
    }
}
```

То же состояние приходит в `GET /quiz/{id}` полем `editing_lock` (там `null`, когда
замка нет) — по нему видно заранее, что правка не пройдёт, и почему. Замок снимается
сам по завершении кампании; снять его досрочно может только поддержка.

---

### 63. Вернуть опросы из корзины

*Метод в API с 2026-09-08.*

**HTTP:** `POST /workspace/{workspace_id}/quiz/restore`

| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
| `ids` | array | Да | Идентификаторы опросов. |

Требует право **`delete_quiz`**: возврат отдаёт опрос обратно тому, кто его
удалил. Без права — `403` с `blocked_by_access: true`.

Возвращённый опрос снова занимает место в тарифе, поэтому лимит проверяется до
возврата: `403` с `blocked_by_tariff: true`. Это строже, чем в кабинете, где
возврат из корзины лимит не проверяет, — иначе лимит обходится через корзину.

---

## Пароли доступа, QR-код и печать

Всё в этом разделе требует право участника **`settings`**.

---

### 94. Пароли доступа к закрытому опросу

*Метод в API с 2026-09-08.*

| Действие | HTTP |
|---|---|
| Список | `GET /quiz/{id}/passwords` |
| Завести | `POST /quiz/{id}/passwords` с полем `password` или списком `passwords` |
| Сменить значение | `POST /quiz/{id}/passwords/{passwordId}` с полем `password` |
| Удалить | `DELETE /quiz/{id}/passwords/{passwordId}` |

Одинаковые значения внутри одной пачки записываются один раз: два одинаковых
пароля получили бы разные номера, и номер в ответах перестал бы однозначно
указывать на респондента. С уже выданными паролями значение не сверяется — они
хранятся хешами.

**Сами пароли не возвращаются ни в одном ответе** — они хранятся хешами. Виден
номер пароля: он оседает в визите и по нему автор узнаёт респондента в ответах.

Пачка ограничена **50 значениями** за вызов. Ограничение намеренное: bcrypt стоит
около 50 мс на пароль, и пятьсот значений упирались бы в таймаут, откатывая всю
пачку. На большее число делайте несколько вызовов.

Номера **не переиспользуются** после удаления: повторно выданный номер слил бы
прежнего респондента с новым.

```json
{"status": true, "quiz_id": 18547, "added": 3,
 "passwords": [{"password_id": 91, "number": 1}, {"password_id": 92, "number": 2}]}
```

Пароль другого опроса — `400` с кодом `password_not_found`. Если пароль найден, но
запись не удалась, приходит `password_save_failed` (смена) или
`password_delete_failed` (удаление): такой вызов имеет смысл повторить, а заводить
пароль заново не нужно.

---

### 95. QR-код опроса

*Метод в API с 2026-09-08.*

| Действие | HTTP |
|---|---|
| Состояние | `GET /quiz/{id}/qr-code` |
| Собрать | `POST /quiz/{id}/qr-code` |

| Поле | Тип | Описание |
|---|---|---|
| `format` | string | `png`, `svg`, `eps`. |
| `size` | integer | 100–1000 пикселей. |
| `color`, `background` | string | `#RRGGBB`. |
| `margin` | integer | 0–10. |
| `force` | boolean | Пересобрать с теми же настройками. |

Непереданное берётся **из прошлой сборки**, а не из умолчаний: иначе запрос
«сделай код побольше» вернул бы чёрный код вместо фирменного.

Повторный запрос с теми же настройками отдаёт уже готовый код и `regenerated:
false` — каждая сборка пишет новый файл и списывает место с тарифа.

Код ведёт на опубликованный опрос: у неопубликованного кодировать нечего, и это
видно по полю `is_published`.

---

### 96. Печатная версия опроса

*Метод в API с 2026-09-08.*

**HTTP:** `POST /quiz/{id}/print`

| Поле | Тип | Описание |
|---|---|---|
| `format` | string | `pdf` (по умолчанию), `word`, `txt`, `html`. |
| `font` | string | `little`, `medium` (по умолчанию), `large`. |

**В ответе приходит сам файл** (`Content-Disposition: attachment`), а не JSON —
как и у остальных выгрузок v3. Собрать не удалось — `400` с кодом `print_failed`.

PDF собирает headless-браузер, поэтому он заметно медленнее остальных форматов;
`txt` и `html` готовятся из данных опроса напрямую.

---

## Папки аккаунта и избранные вопросы

---

### 97. Папки

*Метод в API с 2026-09-08.*

| Действие | HTTP | Право |
|---|---|---|
| Создать | `POST /workspace/{workspace_id}/folders` с полем `name` | `create_folder` |
| Переименовать | `POST /workspace/{workspace_id}/folders/{folderId}/rename` с полем `name` | `rename_folder` |
| Удалить | `DELETE /workspace/{workspace_id}/folders/{folderId}` | `delete_folder` |
| Порядок в списке | `POST /workspace/{workspace_id}/folders/reorder` с полем `folder_ids` | — |

Идентификатор созданной папки приходит полем `folder_id` — он нужен для
переименования, доступа участников и переноса опросов.

Одноимённые папки не заводятся — `400` с кодом `duplicate_name`: список станет
неразличимым, а «положи в папку N» неоднозначным.

Удаление папки уносит лежащие в ней опросы, а с ними ответы респондентов.
Поэтому при непустой папке приходит `422` с кодом `confirm_required` и числом
опросов в поле `quizzes`; повторите запрос с `confirm_delete_quizzes: true`.
Папку по умолчанию удалить нельзя — `400` с кодом `default_folder`: это
единственное место, куда падают новые опросы.

Порядок задаётся только своими папками: чужой идентификатор в списке — `400`,
иначе он сдвинул бы папки другого аккаунта.

---

### 98. Библиотека избранных вопросов

*Метод в API с 2026-09-08.*

| Действие | HTTP |
|---|---|
| Список | `GET /workspace/{workspace_id}/favorite-questions` |
| Сохранить или изменить | `POST /workspace/{workspace_id}/favorite-questions` |
| Удалить | `DELETE /workspace/{workspace_id}/favorite-questions/{questionId}` |

Поля записи: `name`, `widget_type`, `data`, необязательные `choice_type`,
`choice_multiple`, `source_quiz_id`, `source_widget_id`. С полем
`favorite_question_id` правится существующая запись, без него заводится новая.

---

## Шаблоны и сборка опроса по описанию

---

### 111. Свои шаблоны аккаунта

*Метод в API с 2026-09-08.*

**HTTP:** `GET /workspace/{workspace_id}/templates`

| Параметр | Тип | Описание |
|---|---|---|
| `limit` | integer | 1–100, по умолчанию 20. |
| `offset` | integer | Сдвиг по списку. |

Шаблоны приходят полем `items`; `has_more` говорит, набралась ли страница
целиком, то есть стоит ли запрашивать следующую.

---

### 112. Создать опрос из шаблона

*Метод в API с 2026-09-08.*

Требует право **`create_quiz`** и не обходит тарифный лимит на число опросов.

**HTTP:** `POST /quiz/from-template`

| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
| `template_id` | integer | Да | Шаблон общего каталога или свой шаблон аккаунта. |
| `folder_id` | integer | Да | Куда положить копию. |
| `name` | string | Нет | По умолчанию берётся название шаблона. |

Годятся ровно две вещи: каталожный шаблон и свой шаблон аккаунта. Обычный опрос —
`400` с кодом `template_not_found`, иначе по идентификатору копировался бы любой
чужой опрос.

Тема шаблона копируется себе, а не остаётся ссылкой на чужую запись: иначе правка
у автора каталога меняла бы вид ваших опросов.

Опрос создаётся черновиком — в ответе `needs_publish: true`.

---

### 113. Собрать опрос по описанию

*Метод в API с 2026-09-08.*

Требует право **`create_quiz`**.

**HTTP:** `POST /quiz/ai`

| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
| `folder_id` | integer | Да | Куда положить опрос. |
| `prompt` | string | Да | Описание: о чём опрос. |
| `project` | string | Нет | Название проекта или бренда. |
| `ai_type` | string | Нет | `quiz` или `test` с подсчётом баллов. |
| `with_logic` | boolean | Нет | Сразу расставить переходы между вопросами. |

**Сборка идёт очередью.** Ответ приходит сразу с `quiz_id` и `ai_status:
"pending"`: опрос уже создан заготовкой, а вопросы появятся, когда сборка
закончится. Готовность видна в самом опросе — поле `ai_quiz_job` в
`GET /quiz/{id}`.

Тариф, папку, членство и суточный предел проверяет сама сборка; отказ приходит
как `400` с кодом `generation_failed` и пояснением.

---

## Медиа опроса

Требуют право участника **`files_upload`** и платный тариф; при исчерпанном
дисковом месте загрузка отклоняется сразу — `403` с кодом `disk_limit_reached` и
признаком `blocked_by_tariff: true`.

Файл передаётся обычной загрузкой формы (`multipart/form-data`), полем `file`.
Ни ссылки, ни base64 здесь нет — в отличие от ассистента, которому иначе файл не
передать.

---

### 115. Картинка, видео или звук для вопроса

*Метод в API с 2026-09-08.*

**HTTP:** `POST /quiz/{id}/media`

| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
| `type` | string | Да | `image`, `video`, `audio`. |
| `file` | file | Да | Сам файл. |

**Формат проверяется содержимым файла, а не полем `type`**: тип задаёт лишь
подпись. Иначе под видом картинки принимался бы любой файл, включая html и svg,
которые отдаются с исполняемым типом. Неподходящий формат — `422`.

```json
{"status": true, "quiz_id": 18547, "type": "image",
 "url": "https://storage.webask.io/...", "multimedia_field": "imgUrl"}
```

`multimedia_field` подсказывает, куда подставить ссылку в структуре вопроса:
`imgUrl`, `videoUrl` или `audioUrl`.

---

### 116. Картинка варианта ответа

*Метод в API с 2026-09-08.*

**HTTP:** `POST /quiz/{id}/media/widget-image`

Поле `file`, форматы jpeg, jpg, png, webp, gif. Ссылку подставляют в
`choiceImageEntities[].link`.

---