openapi: 3.0.3
info:
  title: HumanGate API
  version: 0.1.0
  description: |
    CAPTCHA-as-a-service для Telegram-ботов и приложений.

    Клиент создаёт **challenge**, отправляет пользователю одноразовую ссылку,
    пользователь проходит CAPTCHA на хостированной странице, результат
    возвращается тремя путями: webhook, redirect с verification-кодом
    и polling через API.

    ## Авторизация
    Все `/v1/*` endpoints требуют API-ключ: `Authorization: Bearer hg_live_...`
    (или `hg_test_...` для test-режима). Live- и test-данные изолированы.

    ## Webhooks
    События `challenge.passed|failed|blocked|expired` доставляются POST-запросом
    с заголовками `HumanGate-Event-ID`, `HumanGate-Timestamp` и подписью
    `HumanGate-Signature: v1=HMAC_SHA256(secret, timestamp + "." + body)`.
    Повторные попытки: 10с, 1м, 5м, 30м, 2ч, 12ч.
servers:
  - url: https://humangate.hikka.host
    description: Текущий деплой

tags:
  - name: Account
    description: Self-service — выпуск своего токена и доп. ключей через API
  - name: Provider Configs
    description: Ваши ключи CAPTCHA-провайдеров — создаются один раз, дальше challenges используют их
  - name: Challenges
    description: Жизненный цикл проверок
  - name: Verifications
    description: Проверка verification-кодов после redirect
  - name: Webhooks
    description: Управление доставкой событий
  - name: Public
    description: Endpoints страницы CAPTCHA — без API-ключа

security:
  - bearerAuth: []

paths:
  /v1/signup:
    post:
      tags: [Account]
      summary: Выпустить свой токен (без авторизации)
      description: |
        Self-service: создаёт новый tenant + project и возвращает **live и test
        API-ключи**. Дальше этими ключами можно привязывать ключи провайдеров
        (`POST /v1/provider-configs`) и создавать challenges. Ключи показываются
        один раз. Можно отключить на сервере через `OPEN_SIGNUP=false`.
      security: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, description: Название проекта, example: Acme Bot }
      responses:
        "201":
          description: Tenant и ключи созданы
          content:
            application/json:
              schema:
                type: object
                properties:
                  project: { type: string, example: t_g82x7r4tvvd8/default }
                  live_key: { type: string, example: hg_live_... }
                  test_key: { type: string, example: hg_test_... }
                  scopes:
                    type: array
                    items: { type: string }
        "403":
          description: Signup отключён (`OPEN_SIGNUP=false`)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /v1/api-keys:
    post:
      tags: [Account]
      summary: Выпустить дополнительный ключ
      description: |
        Создаёт ещё один API-ключ в проекте вызывающего. Требует scope
        `api_keys:manage`. Ключ возвращается один раз.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, example: ci-key }
                mode: { type: string, enum: [live, test], description: По умолчанию режим вызывающего ключа }
                scopes:
                  type: array
                  items: { type: string }
                  description: По умолчанию полный набор
      responses:
        "201":
          description: Ключ создан
          content:
            application/json:
              schema:
                type: object
                properties:
                  key: { type: string, example: hg_test_... }
                  prefix: { type: string }
                  livemode: { type: boolean }
                  scopes:
                    type: array
                    items: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Unauthorized" }

  /v1/provider-configs:
    post:
      tags: [Provider Configs]
      summary: Сохранить ключи вашей CAPTCHA
      description: |
        **Сюда передаются параметры капчи.** Возьмите site key и secret key
        из консоли провайдера (Cloudflare / Google / Yandex Cloud) и сохраните
        их один раз. `secret_key` шифруется AES-GCM и **никогда не возвращается**.

        Первая конфигурация для пары (provider, mode) автоматически становится
        default — её не нужно указывать при создании challenge. Требует scope
        `provider_configs:manage`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateProviderConfigRequest" }
            examples:
              turnstile:
                summary: Cloudflare Turnstile
                value:
                  name: Main Turnstile
                  provider: cloudflare_turnstile
                  config:
                    site_key: 0x4AAAAAAA…
                    secret_key: 0x4AAAAAAA…
              recaptcha_v3:
                summary: Google reCAPTCHA v3 с порогом score
                value:
                  name: Main reCAPTCHA
                  provider: google_recaptcha_v3
                  config:
                    site_key: 6Lc…
                    secret_key: 6Lc…
                    min_score: 0.7
              yandex_invisible:
                summary: Yandex SmartCaptcha (невидимая)
                value:
                  name: Yandex invisible
                  provider: yandex_smartcaptcha
                  config:
                    site_key: ysc1_…
                    secret_key: ysc2_…
                    invisible: true
      responses:
        "201":
          description: Конфигурация создана (secret_key не возвращается)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProviderConfig" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409":
          description: Конфигурация с таким name уже существует
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "422": { $ref: "#/components/responses/ValidationError" }
    get:
      tags: [Provider Configs]
      summary: Список конфигураций
      responses:
        "200":
          description: Активные конфигурации текущего режима (live/test)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProviderConfig" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/challenges:
    post:
      tags: [Challenges]
      summary: Создать challenge
      description: |
        Требует scope `challenges:write`. Поддерживает идемпотентность через
        заголовок `Idempotency-Key` (повтор с тем же ключом и телом вернёт
        сохранённый ответ; с другим телом — `409 CONFLICT`).
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateChallengeRequest" }
            examples:
              minimal:
                summary: Минимальный запрос
                value:
                  provider: cloudflare_turnstile
                  action: registration
                  subject: { type: telegram_user, id: "123456789" }
                  expires_in: 600
              full:
                summary: Полный запрос (Telegram WebApp)
                value:
                  provider: cloudflare_turnstile
                  provider_config_id: pcfg_01K2A1VXRM3J
                  action: telegram_registration
                  subject: { type: telegram_user, id: "123456789" }
                  channel:
                    type: telegram_webapp
                    telegram:
                      require_init_data: true
                      expected_user_id: "123456789"
                      max_auth_age_seconds: 300
                  expires_in: 600
                  max_attempts: 3
                  webhook:
                    endpoint_id: wh_01K2B7P0XXXXXXXXXXXXXXXXXX
                    events: [challenge.passed, challenge.failed, challenge.blocked, challenge.expired]
                  redirect:
                    success_url: "https://t.me/example_bot?start=verify_{code}"
                    failure_url: "https://t.me/example_bot?start=captcha_failed"
                  appearance:
                    theme: dark
                    language: ru
                    title: Подтвердите, что вы человек
                    description: Проверка займёт несколько секунд
                  risk_policy: { policy_id: default, max_score: 69 }
                  metadata: { chat_id: "-1001234567890" }
      responses:
        "201":
          description: Challenge создан
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Challenge" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/ValidationError" }
    get:
      tags: [Challenges]
      summary: Список challenges
      description: Cursor-pagination. Требует scope `challenges:read`.
      parameters:
        - { name: status, in: query, schema: { $ref: "#/components/schemas/ChallengeStatus" } }
        - { name: provider, in: query, schema: { $ref: "#/components/schemas/ProviderType" } }
        - { name: action, in: query, schema: { type: string } }
        - { name: subject_type, in: query, schema: { type: string } }
        - { name: subject_id, in: query, schema: { type: string } }
        - { name: created_from, in: query, schema: { type: string, format: date-time } }
        - { name: created_to, in: query, schema: { type: string, format: date-time } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        "200":
          description: Страница списка
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ChallengeList" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/challenges/{id}:
    get:
      tags: [Challenges]
      summary: Получить challenge
      parameters:
        - { name: id, in: path, required: true, schema: { type: string }, example: ch_01K2AFZT8F3F4XW6D2CBFBRZ1E }
      responses:
        "200":
          description: Текущее состояние challenge (просроченные лениво переводятся в `expired`)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Challenge" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/challenges/{id}/cancel:
    post:
      tags: [Challenges]
      summary: Отменить challenge
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Отменён
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  status: { type: string, enum: [cancelled] }
                  cancelled_at: { type: string, format: date-time }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: Уже завершён (`CHALLENGE_ALREADY_COMPLETED`)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /v1/verifications/verify:
    post:
      tags: [Verifications]
      summary: Проверить verification-код
      description: |
        Код приходит пользовательским redirect'ом (`{code}` в шаблоне URL).
        Принимается с префиксом `hgv_` и без него. Одноразовые коды
        потребляются атомарно: повторная проверка вернёт
        `valid: false, reason: TOKEN_ALREADY_CONSUMED`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/VerifyRequest" }
      responses:
        "200":
          description: Результат (и валидный, и невалидный код — это 200)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VerifyResult" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/ValidationError" }

  /v1/webhook-events/{id}/retry:
    post:
      tags: [Webhooks]
      summary: Повторить доставку события
      description: Требует scope `webhooks:manage`.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string }, example: evt_01K2AG7YKPN26WHZQNRD5MTRBR }
      responses:
        "200":
          description: Доставка перепланирована
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  delivery_status: { type: string, enum: [pending] }
                  next_attempt_at: { type: string, format: date-time }
        "404": { $ref: "#/components/responses/NotFound" }

  /public/v1/stats:
    get:
      tags: [Public]
      summary: Открытая статистика (для главной)
      description: Сколько challenge'ей создано и пройдено. Кэшируется на 15с.
      security: []
      responses:
        "200":
          description: Агрегаты
          content:
            application/json:
              schema:
                type: object
                properties:
                  challenges_total: { type: integer, example: 1280 }
                  passed_total: { type: integer, example: 1042 }

  /public/v1/demo:
    post:
      tags: [Public]
      summary: Создать демо-challenge (страница /example)
      description: |
        Работает только если на сервере задан `DEMO_PROJECT`. Создаёт
        короткоживущий test-mode challenge в демо-проекте и возвращает ссылку.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [provider]
              properties:
                provider: { $ref: "#/components/schemas/ProviderType" }
      responses:
        "201":
          description: Демо-challenge создан
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  url: { type: string }
                  expires_at: { type: string, format: date-time }
        "404":
          description: Демо отключено (DEMO_PROJECT не задан)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "422":
          description: Для провайдера нет тестовой конфигурации
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /public/v1/challenges/{token}:
    get:
      tags: [Public]
      summary: Публичная конфигурация challenge
      description: |
        Вызывается страницей CAPTCHA. Первый вызов фиксирует `opened_at`.
        Не возвращает subject, webhook, metadata, секреты и risk-настройки.
      security: []
      parameters:
        - { name: token, in: path, required: true, schema: { type: string }, description: "Одноразовый public token из URL вида /c/{token}" }
      responses:
        "200":
          description: Безопасная для клиента конфигурация
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PublicChallenge" }
        "404": { $ref: "#/components/responses/NotFound" }

  /public/v1/challenges/{token}/start:
    post:
      tags: [Public]
      summary: Начать browser-сессию
      description: |
        Создаёт сессию (предыдущая активная ревокается — у challenge одна
        primary-сессия). Здесь же валидируется Telegram `initData`, если
        канал этого требует.
      security: []
      parameters:
        - { name: token, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/StartRequest" }
      responses:
        "200":
          description: Сессия создана
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StartResult" }
        "400":
          description: INVALID_TELEGRAM_INIT_DATA / SUBJECT_MISMATCH
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "410":
          description: CHALLENGE_EXPIRED / CHALLENGE_CANCELLED
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /public/v1/challenges/{token}/complete:
    post:
      tags: [Public]
      summary: Завершить попытку CAPTCHA
      description: |
        Сервер проверяет ответ у провайдера, считает risk score и завершает
        challenge. Клиенту намеренно не возвращаются risk-детали — blocked
        неотличим от failed.
      security: []
      parameters:
        - { name: token, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CompleteRequest" }
      responses:
        "200":
          description: Итог попытки
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CompleteResult" }
              examples:
                passed:
                  value:
                    status: passed
                    verification_code: hgv_A7F2K9XW4M
                    redirect: { url: "https://t.me/example_bot?start=verify_A7F2K9XW4M", delay_ms: 1200 }
                failed_retry:
                  value: { status: failed, attempts_remaining: 2, retry_allowed: true }
        "400":
          description: INVALID_SESSION
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "409":
          description: MAX_ATTEMPTS_EXCEEDED / CHALLENGE_ALREADY_COMPLETED
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "502":
          description: PROVIDER_UNAVAILABLE (попытка не списана)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "API-ключ: hg_live_… или hg_test_…"

  responses:
    Unauthorized:
      description: Невалидный API-ключ
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    NotFound:
      description: Ресурс не найден
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    Conflict:
      description: Конфликт (идемпотентность, состояние)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    ValidationError:
      description: Ошибка валидации
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }

  schemas:
    ProviderType:
      type: string
      description: |
        Внешние провайдеры требуют provider config с ключами.
        HumanGate-native (text_image, proof_of_work, browser_fingerprint)
        не требуют ключей: задание выдаётся в ответе /start (поле task) и
        проверяется на сервере по состоянию сессии.
      enum:
        - cloudflare_turnstile
        - google_recaptcha_v2
        - google_recaptcha_v3
        - yandex_smartcaptcha
        - text_image
        - proof_of_work
        - browser_fingerprint

    ChallengeStatus:
      type: string
      enum: [pending, opened, in_progress, passed, failed, blocked, expired, cancelled]

    CreateProviderConfigRequest:
      type: object
      required: [name, provider, config]
      properties:
        name:
          type: string
          description: Уникальное имя в рамках проекта
          example: Main Turnstile
        provider: { $ref: "#/components/schemas/ProviderType" }
        config:
          type: object
          required: [site_key, secret_key]
          description: Ключи из консоли провайдера + опции виджета
          properties:
            site_key:
              type: string
              description: Публичный ключ (показывается виджету)
            secret_key:
              type: string
              description: Секретный ключ — шифруется, никогда не возвращается
            min_score:
              type: number
              minimum: 0
              maximum: 1
              description: "google_recaptcha_v3: минимальный проходной score (default 0.5)"
            invisible:
              type: boolean
              description: "yandex_smartcaptcha: невидимый виджет"
            test:
              type: boolean
              description: "yandex_smartcaptcha: тест-режим виджета"
        is_default:
          type: boolean
          description: |
            Сделать default для (provider, mode). Если не указан — первая
            конфигурация становится default автоматически.

    ProviderConfig:
      type: object
      properties:
        id: { type: string, example: pcfg_01K2A1VXRM3J }
        name: { type: string }
        provider: { $ref: "#/components/schemas/ProviderType" }
        livemode: { type: boolean }
        is_default: { type: boolean }
        public_config:
          type: object
          description: Только публичная часть — секрет не возвращается
        created_at: { type: string, format: date-time }

    Subject:
      type: object
      required: [type, id]
      properties:
        type: { type: string, example: telegram_user }
        id: { type: string, example: "123456789" }

    CreateChallengeRequest:
      type: object
      required: [provider, action, subject]
      properties:
        provider: { $ref: "#/components/schemas/ProviderType" }
        provider_config_id:
          type: string
          description: Если не указан — берётся default-конфигурация проекта для (provider, mode)
          example: pcfg_01K2A1VXRM3J
        action: { type: string, example: telegram_registration }
        subject: { $ref: "#/components/schemas/Subject" }
        channel:
          type: object
          description: "type: browser | telegram_webapp; telegram.require_init_data включает проверку initData"
        expires_in:
          type: integer
          minimum: 60
          maximum: 3600
          default: 600
          description: Секунды жизни challenge
        max_attempts: { type: integer, minimum: 1, maximum: 20, default: 3 }
        webhook:
          type: object
          properties:
            endpoint_id: { type: string, description: Заранее созданный webhook endpoint }
            events:
              type: array
              items: { type: string }
        redirect:
          type: object
          description: Шаблоны URL; "{code}" заменяется на verification-код (без префикса hgv_)
          properties:
            success_url: { type: string }
            failure_url: { type: string }
        appearance:
          type: object
          properties:
            theme: { type: string, enum: [light, dark, system] }
            language: { type: string }
            title: { type: string }
            description: { type: string }
        collection: { type: object }
        risk_policy:
          type: object
          properties:
            policy_id: { type: string }
            max_score: { type: integer, minimum: 0, maximum: 100 }
        metadata: { type: object }

    Challenge:
      type: object
      properties:
        id: { type: string, example: ch_01K2AFZT8F3F4XW6D2CBFBRZ1E }
        livemode: { type: boolean }
        status: { $ref: "#/components/schemas/ChallengeStatus" }
        provider: { $ref: "#/components/schemas/ProviderType" }
        provider_config_id: { type: string }
        action: { type: string }
        subject: { $ref: "#/components/schemas/Subject" }
        url:
          type: string
          description: Одноразовая ссылка для пользователя (только в ответе создания)
        attempts:
          type: object
          properties:
            used: { type: integer }
            maximum: { type: integer }
        timestamps:
          type: object
          properties:
            created_at: { type: string, format: date-time }
            opened_at: { type: string, format: date-time, nullable: true }
            started_at: { type: string, format: date-time, nullable: true }
            completed_at: { type: string, format: date-time, nullable: true }
            expires_at: { type: string, format: date-time }
        result:
          type: object
          nullable: true
          properties:
            success: { type: boolean }
            risk_score: { type: integer, minimum: 0, maximum: 100 }
            risk_level: { type: string, enum: [low, medium, high] }
            duration_ms: { type: integer }
        metadata: { type: object }
        created_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }

    ChallengeList:
      type: object
      properties:
        data:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              livemode: { type: boolean }
              status: { $ref: "#/components/schemas/ChallengeStatus" }
              provider: { $ref: "#/components/schemas/ProviderType" }
              action: { type: string }
              subject: { $ref: "#/components/schemas/Subject" }
              risk_score: { type: integer }
              duration_ms: { type: integer }
              created_at: { type: string, format: date-time }
              completed_at: { type: string, format: date-time, nullable: true }
        pagination:
          type: object
          properties:
            next_cursor: { type: string, nullable: true }
            has_more: { type: boolean }

    VerifyRequest:
      type: object
      required: [code]
      properties:
        code: { type: string, example: A7F2K9XW4M }
        expected_action: { type: string }
        expected_subject: { $ref: "#/components/schemas/Subject" }
        consume:
          type: boolean
          default: true
          description: Потребить одноразовый код при успешной проверке

    VerifyResult:
      type: object
      properties:
        valid: { type: boolean }
        reason:
          type: string
          description: Заполнен при valid=false
          enum: [TOKEN_NOT_FOUND, TOKEN_EXPIRED, TOKEN_ALREADY_CONSUMED, ACTION_MISMATCH, SUBJECT_MISMATCH]
        consumed: { type: boolean }
        challenge_id: { type: string }
        action: { type: string }
        subject: { $ref: "#/components/schemas/Subject" }
        risk_score: { type: integer }
        risk_level: { type: string }
        client:
          type: object
          description: Данные того, кто прошёл капчу (IP, user-agent, сигналы из window)
          properties:
            ip: { type: string, example: 203.0.113.7 }
            country: { type: string, example: KZ }
            user_agent: { type: string }
            device:
              type: object
              description: |
                Сигналы браузера из window: screen_width/height, pixel_ratio,
                viewport, hardware_concurrency, device_memory, timezone, languages,
                connection {effective_type, downlink_mbps, rtt_ms, save_data},
                timing {ttfb_ms, dom_content_loaded_ms, load_ms}.
            telegram:
              type: object
              description: Telegram Mini App данные прошедшего (если были при /start)
              properties:
                valid: { type: boolean, description: прошла ли серверная проверка initData }
                user_id: { type: string }
                auth_date: { type: string, format: date-time }
                init_data: { type: string, description: сырой initData (валидирован сервером) }
        completed_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }

    PublicChallenge:
      type: object
      properties:
        status: { $ref: "#/components/schemas/ChallengeStatus" }
        provider: { $ref: "#/components/schemas/ProviderType" }
        action: { type: string }
        appearance: { type: object }
        provider_config:
          type: object
          properties:
            site_key: { type: string }
            invisible: { type: boolean, description: yandex_smartcaptcha }
            test: { type: boolean, description: "yandex_smartcaptcha: тест-режим виджета" }
        channel_type: { type: string }
        expires_at: { type: string, format: date-time }
        redirect_url:
          type: string
          description: |
            Возвращается только при повторном открытии уже завершённого
            challenge (passed → success_url с новым кодом, failed/blocked →
            failure_url), чтобы страница сразу показала ссылку возврата.

    StartRequest:
      type: object
      properties:
        telegram_init_data: { type: string }
        client:
          type: object
          properties:
            timezone: { type: string }
            language: { type: string }
            screen_width: { type: integer }
            screen_height: { type: integer }
            color_depth: { type: integer }
            touch_support: { type: boolean }

    StartResult:
      type: object
      properties:
        session_id: { type: string, example: ses_01K2AH0J9WJNFQ5B }
        session_token: { type: string, example: hgs_Rl8X… }
        status: { type: string, enum: [in_progress] }
        expires_at: { type: string, format: date-time }
        task:
          type: object
          description: |
            Задание HumanGate-native капчи (отсутствует у внешних провайдеров):
            proof_of_work → {algorithm, prefix, difficulty};
            text_image → {image (data URL), length};
            browser_fingerprint → {nonce}.

    CompleteRequest:
      type: object
      required: [session_token, provider_response]
      properties:
        session_token: { type: string }
        provider_response:
          type: object
          description: |
            Внешние провайдеры → {token}. HumanGate-native:
            proof_of_work → {nonce}; text_image → {answer};
            browser_fingerprint → {nonce, fingerprint}.
          properties:
            token: { type: string, description: Токен внешнего провайдера CAPTCHA }
            nonce: { type: string, description: "proof_of_work / browser_fingerprint" }
            answer: { type: string, description: "text_image: введённые символы" }
            fingerprint: { type: object, description: "browser_fingerprint: собранные сигналы" }
        telemetry:
          type: object
          properties:
            elapsed_ms: { type: integer }
            pointer_events: { type: integer }
            keyboard_events: { type: integer }
            focus_changes: { type: integer }

    CompleteResult:
      type: object
      properties:
        status: { type: string, enum: [passed, failed] }
        verification_code: { type: string, description: При passed }
        attempts_remaining: { type: integer, description: При failed }
        retry_allowed: { type: boolean, description: При failed }
        redirect:
          type: object
          properties:
            url: { type: string }
            delay_ms: { type: integer }

    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              example: VALIDATION_ERROR
              description: |
                UNAUTHORIZED, FORBIDDEN, VALIDATION_ERROR, RATE_LIMITED, NOT_FOUND,
                CONFLICT, CHALLENGE_EXPIRED, CHALLENGE_CANCELLED,
                CHALLENGE_ALREADY_COMPLETED, MAX_ATTEMPTS_EXCEEDED, INVALID_SESSION,
                INVALID_TELEGRAM_INIT_DATA, SUBJECT_MISMATCH, PROVIDER_UNAVAILABLE,
                PROVIDER_REJECTED, PROVIDER_CONFIGURATION_ERROR
            message: { type: string }
            request_id: { type: string, example: req_01K2AHTMX… }
            fields:
              type: array
              items:
                type: object
                properties:
                  field: { type: string }
                  code: { type: string }
                  message: { type: string }
