{"openapi":"3.1.0","info":{"title":"Bounty Captcha","description":"\nСобственная капча сервиса: ползунок-пазл во всех формах (Р-481), мозаика 3×3 —\nзапасной вид. Задание показывается только по подозрению (Р-277, Р-466).\n\n## Как подключить\n\n1. **Задание.** Форма с подозрением просит `GET /v1/new` — по умолчанию ползунок.\n   В ответе кадр, фрагмент и его высота; место паза по горизонтали не отдаётся.\n2. **Человек решает.** Ответ — доля хода ручки от 0 до 1000 целым числом.\n   Браузер отдаёт его `POST /v1/solve`: верно — «Капча пройдена», промах —\n   задание удалено, сразу просите новое.\n3. **Сервер сверяет.** Форма уходит в ваш API вместе с `captcha_id` и\n   `captcha_answer`; API зовёт `POST /v1/verify` с заголовком\n   `X-Captcha-Secret`. Засчитывается задание один раз — второй `verify` вернёт\n   `ok: false`.\n\n## Правила\n\n- Задание живёт 10 минут и засчитывается один раз; неверный ответ в `/v1/solve`\n  удаляет его сразу.\n- Допуск ползунка — 40 из 1000 (около 4 % хода ручки) в обе стороны.\n- Выдача — 120 заданий в час на адрес; проверка — 600 в час.\n- Сервис капчи недоступен — форма отклоняется с понятной ошибкой: пропускать\n  всех на время сбоя значит открыть регистрацию ботам ровно на этот срок.\n- Демонстрация — `/visual`, только вне релиза.\n","version":"26.10.18"},"paths":{"/v1/new":{"get":{"tags":["Капча"],"summary":"Выдаёт новое задание: ползунок-пазл, по запросу — мозаику 3×3","description":"Кадр пазла (768×432) клиент вписывает в сцену той же пропорции 16:9:\nположение фрагмента задаётся долями, поэтому кадр не растягивается и\nфрагмент совпадает с пазом на любой ширине. Место паза по горизонтали не\nотдаётся — его и надо найти.\n\nНечитаемое задание клиент меняет повторным вызовом этого же эндпоинта, а\nне перезагрузкой формы: перезагрузка стирает уже введённые почту и\nпароль. Язык задания мозаики — `lang`, иначе `Accept-Language`, иначе\nанглийский (Р-062).","operationId":"new_captcha_v1_new_get","parameters":[{"name":"kind","in":"query","required":false,"schema":{"enum":["slider","mosaic"],"type":"string","description":"Вид задания: ползунок (по умолчанию) или мозаика","default":"slider","title":"Kind"},"description":"Вид задания: ползунок (по умолчанию) или мозаика"},{"name":"lang","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Язык задания мозаики","title":"Lang"},"description":"Язык задания мозаики"}],"responses":{"200":{"description":"Успешный ответ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewResponse"}}}},"400":{"description":"Неверные параметры или тело запроса — `validation_error`; в `details.fields` — какие поля","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"error":{"code":"validation_error","message":"Проверьте заполненные поля","request_id":"0f1c2b7e9a4d4c1e"}}}}},"429":{"description":"Слишком часто — `rate_limited`. Подождите столько секунд, сколько в заголовке `Retry-After`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"error":{"code":"rate_limited","message":"Слишком много запросов","request_id":"0f1c2b7e9a4d4c1e"}}}}},"500":{"description":"Сбой на стороне сервиса — `internal_error`; `request_id` — для поддержки","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"error":{"code":"internal_error","message":"Внутренняя ошибка","request_id":"0f1c2b7e9a4d4c1e"}}}}}}}},"/v1/solve":{"post":{"tags":["Капча"],"summary":"Проверяет решение для браузера: верно ли решено задание","description":"Ответ нужен, чтобы человек сразу увидел «Капча пройдена» или новое\nзадание, а не узнал о промахе, отправив всю форму. **Попытка одна:**\nневерный ответ удаляет задание, верный помечает его решённым — и\nзасчитать его по-прежнему может только сервер формы (`/v1/verify`) с тем\nже ответом. Сказать «пройдено» за сервер браузер так не может.","operationId":"solve_captcha_v1_solve_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnswerRequest"}}},"required":true},"responses":{"200":{"description":"Успешный ответ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyResponse"}}}},"400":{"description":"Неверные параметры или тело запроса — `validation_error`; в `details.fields` — какие поля","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"error":{"code":"validation_error","message":"Проверьте заполненные поля","request_id":"0f1c2b7e9a4d4c1e"}}}}},"429":{"description":"Слишком часто — `rate_limited`. Подождите столько секунд, сколько в заголовке `Retry-After`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"error":{"code":"rate_limited","message":"Слишком много запросов","request_id":"0f1c2b7e9a4d4c1e"}}}}},"500":{"description":"Сбой на стороне сервиса — `internal_error`; `request_id` — для поддержки","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"error":{"code":"internal_error","message":"Внутренняя ошибка","request_id":"0f1c2b7e9a4d4c1e"}}}}}}}},"/v1/verify":{"post":{"tags":["Капча"],"summary":"Засчитывает решение — только сервер формы, по общему секрету","description":"Задание удаляется при первом вызове, каким бы ни был ответ. Решённое в\nбраузере (`/v1/solve`) засчитывается только с тем же ответом; не решённое\nтам — проверяется здесь. Без секрета — `403`: иначе браузер мог бы сам\nсказать «капча пройдена».","operationId":"verify_captcha_v1_verify_post","parameters":[{"name":"x-captcha-secret","in":"header","required":false,"schema":{"type":"string","description":"Общий секрет из `.env` (CAPTCHA_SHARED_SECRET)","default":"","title":"X-Captcha-Secret"},"description":"Общий секрет из `.env` (CAPTCHA_SHARED_SECRET)"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnswerRequest"}}}},"responses":{"200":{"description":"Успешный ответ","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyResponse"}}}},"400":{"description":"Неверные параметры или тело запроса — `validation_error`; в `details.fields` — какие поля","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"error":{"code":"validation_error","message":"Проверьте заполненные поля","request_id":"0f1c2b7e9a4d4c1e"}}}}},"429":{"description":"Слишком часто — `rate_limited`. Подождите столько секунд, сколько в заголовке `Retry-After`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"error":{"code":"rate_limited","message":"Слишком много запросов","request_id":"0f1c2b7e9a4d4c1e"}}}}},"500":{"description":"Сбой на стороне сервиса — `internal_error`; `request_id` — для поддержки","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"error":{"code":"internal_error","message":"Внутренняя ошибка","request_id":"0f1c2b7e9a4d4c1e"}}}}}}}}},"components":{"schemas":{"AnswerRequest":{"properties":{"id":{"type":"string","title":"Id","description":"Идентификатор задания из `/v1/new`"},"answer":{"type":"string","title":"Answer","description":"Ползунок — доля хода ручки 0…1000 целым числом; мозаика — номера плиток через запятую"}},"type":"object","required":["id","answer"],"title":"AnswerRequest","description":"Ответ на задание."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"NewResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Идентификатор задания — уходит вместе с ответом"},"kind":{"type":"string","title":"Kind","description":"Вид задания: slider или mosaic"},"ttl":{"type":"integer","title":"Ttl","description":"Сколько секунд задание действительно"},"image":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image","description":"Кадр пазла в виде data-URI (768 × 432)"},"piece":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Piece","description":"Фрагмент пазла в виде data-URI"},"piece_top":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Piece Top","description":"Y фрагмента; X паза не отдаётся"},"piece_width":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Piece Width","description":"Сторона фрагмента в пикселях"},"canvas_width":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Canvas Width","description":"Ширина кадра пазла"},"canvas_height":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Canvas Height","description":"Высота кадра пазла"},"images":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Images","description":"Девять плиток мозаики"},"prompt":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prompt","description":"Задание мозаики на языке человека"}},"type":"object","required":["id","kind","ttl"],"title":"NewResponse","description":"Задание капчи."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyResponse":{"properties":{"ok":{"type":"boolean","title":"Ok","description":"Решено ли задание"}},"type":"object","required":["ok"],"title":"VerifyResponse"},"ErrorEnvelope":{"title":"ErrorEnvelope","description":"Ошибка — всегда один конверт (docs/api.md §2). Ветвиться — по `code`; `message` — для человека и может меняться; `request_id` — номер запроса для поддержки (он же в заголовке `X-Request-Id`).","type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Машинный код ошибки"},"message":{"type":"string","description":"Текст для человека"},"details":{"type":"object","description":"Подробности: поле с ошибкой (`field`), список полей (`fields`), пределы","additionalProperties":true},"request_id":{"type":"string","description":"Номер запроса в журнале"}}}}}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Access-токен из `POST /v1/auth/login` (живёт 15 минут, обновляется `POST /v1/auth/refresh`). Заголовок `Authorization: Bearer <access>`."}}},"tags":[{"name":"Капча","description":"Выдать задание (`GET /v1/new`), проверить решение в браузере (`POST /v1/solve`) и засчитать его на сервере формы (`POST /v1/verify`, общий секрет). Вход не нужен."}]}