{
  "openapi": "3.1.0",
  "info": {
    "title": "Confirmator REST API",
    "version": "1.0.0",
    "description": "Server-to-server API подтверждения владения номером телефона через мессенджеры."
  },
  "servers": [{ "url": "https://confirmator.ru" }],
  "tags": [
    { "name": "Challenges" },
    { "name": "Public presentation" }
  ],
  "paths": {
    "/v1/challenges": {
      "post": {
        "tags": ["Challenges"],
        "summary": "Создать challenge",
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "schema": { "$ref": "#/components/schemas/CreateChallengeRequest" } }
          }
        },
        "responses": {
          "201": {
            "description": "Challenge создан",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateChallengeResponse" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/challenges/{challengeId}/status": {
      "get": {
        "tags": ["Challenges"],
        "summary": "Получить безопасный статус",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/ChallengeId" }],
        "responses": {
          "200": {
            "description": "Текущее состояние",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChallengeStatus" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/challenges/{challengeId}/consume": {
      "post": {
        "tags": ["Challenges"],
        "summary": "Одноразово получить proof",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/ChallengeId" },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": { "type": "string", "minLength": 1, "maxLength": 128 }
          }
        ],
        "responses": {
          "200": {
            "description": "Proof получен",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConsumeResponse" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/public/challenges/{token}": {
      "get": {
        "tags": ["Public presentation"],
        "summary": "Получить данные страницы подтверждения",
        "security": [],
        "parameters": [{ "$ref": "#/components/parameters/PublicToken" }],
        "responses": {
          "200": {
            "description": "Короткоживущие данные без полного номера и proof",
            "headers": { "Cache-Control": { "schema": { "type": "string", "const": "private, no-store" } } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicPresentation" } } }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/public/messengers": {
      "get": {
        "tags": ["Public presentation"],
        "summary": "Доступность настроенных мессенджеров",
        "security": [],
        "responses": {
          "200": {
            "description": "Публичный каталог каналов с текущим runtime-статусом",
            "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/PublicMessenger" } } } }
          }
        }
      }
    },
    "/public/qr/challenge/{token}": {
      "get": {
        "tags": ["Public presentation"],
        "summary": "QR-код authorization URL",
        "security": [],
        "parameters": [{ "$ref": "#/components/parameters/PublicToken" }],
        "responses": {
          "200": { "description": "PNG", "content": { "image/png": { "schema": { "type": "string", "contentEncoding": "binary" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/public/qr/messengers/{messenger}/{token}": {
      "get": {
        "tags": ["Public presentation"],
        "summary": "QR-код messenger deep link",
        "security": [],
        "parameters": [
          { "name": "messenger", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/Messenger" } },
          { "$ref": "#/components/parameters/PublicToken" }
        ],
        "responses": {
          "200": { "description": "PNG", "content": { "image/png": { "schema": { "type": "string", "contentEncoding": "binary" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    }
  },
  "webhooks": {
    "challengeCompleted": {
      "post": {
        "summary": "Подписанный результат challenge",
        "parameters": [
          { "name": "x-confirmator-event-id", "in": "header", "required": true, "schema": { "type": "string" } },
          { "name": "x-confirmator-timestamp", "in": "header", "required": true, "schema": { "type": "string", "pattern": "^[0-9]+$" } },
          { "name": "x-confirmator-signature", "in": "header", "required": true, "schema": { "type": "string", "pattern": "^v1=[a-f0-9]{64}$" } }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEvent" } } }
        },
        "responses": { "204": { "description": "Событие принято" } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Ключ проекта confirmator_test_<prefix>.<secret>"
      }
    },
    "parameters": {
      "ChallengeId": {
        "name": "challengeId",
        "in": "path",
        "required": true,
        "schema": { "type": "string", "pattern": "^ch_[A-Za-z0-9_-]+$" }
      },
      "PublicToken": {
        "name": "token",
        "in": "path",
        "required": true,
        "schema": { "type": "string", "minLength": 32, "maxLength": 128 }
      }
    },
    "responses": {
      "BadRequest": { "description": "Ошибка валидации", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
      "Unauthorized": { "description": "Неверный API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
      "InsufficientBalance": { "description": "Недостаточно средств", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
      "Forbidden": { "description": "Операция запрещена", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
      "NotFound": { "description": "Challenge не найден", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
      "Conflict": { "description": "Конфликт состояния", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
      "PayloadTooLarge": { "description": "Тело больше 16 КБ", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
      "InternalError": { "description": "Внутренняя ошибка", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
    },
    "schemas": {
      "Messenger": { "type": "string", "enum": ["telegram", "max", "whatsapp"] },
      "PublicMessenger": {
        "type": "object",
        "required": ["messenger", "title", "bot_id", "image", "color", "available", "status"],
        "properties": {
          "messenger": { "$ref": "#/components/schemas/Messenger" },
          "title": { "type": "string" },
          "bot_id": { "type": "string" },
          "image": { "type": "string", "format": "uri" },
          "color": { "type": "string" },
          "available": { "type": "boolean" },
          "status": { "type": "string", "enum": ["starting", "up", "down"] }
        }
      },
      "ChallengeState": { "type": "string", "enum": ["created", "opened", "confirmed", "rejected", "expired", "consumed"] },
      "CreateChallengeRequest": {
        "type": "object",
        "required": ["phone"],
        "properties": {
          "phone": { "type": "string", "examples": ["+79991234567"] },
          "externalUserRef": { "type": "string", "maxLength": 128 },
          "purpose": { "type": "string", "maxLength": 64, "default": "login" },
          "expiresIn": { "type": "integer", "minimum": 30, "maximum": 180 },
          "deliveryMode": { "type": "string", "enum": ["polling", "webhook"] },
          "webhookUrl": { "type": "string", "format": "uri", "maxLength": 2048 }
        }
      },
      "MessengerLink": {
        "type": "object",
        "required": ["messenger", "title", "bot_id", "link", "image", "color", "qr_code"],
        "properties": {
          "messenger": { "$ref": "#/components/schemas/Messenger" },
          "title": { "type": "string" },
          "bot_id": { "type": "string", "pattern": "^@" },
          "link": { "type": "string" },
          "image": { "type": "string", "format": "uri" },
          "color": { "type": "string", "pattern": "^#[A-Fa-f0-9]{6}$" },
          "qr_code": { "type": "string", "format": "uri" }
        }
      },
      "CreateChallengeResponse": {
        "type": "object",
        "required": ["id", "challengeId", "projectId", "status", "purpose", "maskedPhone", "createdAt", "expiresAt", "expiresIn", "fallbackCode", "authorization_url", "qr_code", "links", "deliveryMode"],
        "properties": {
          "id": { "type": "string" },
          "challengeId": { "type": "string" },
          "projectId": { "type": "string" },
          "status": { "const": "created" },
          "purpose": { "type": "string" },
          "externalUserRef": { "type": "string" },
          "maskedPhone": { "type": "string" },
          "createdAt": { "type": "string", "format": "date-time" },
          "expiresAt": { "type": "string", "format": "date-time" },
          "expiresIn": { "type": "integer", "minimum": 30, "maximum": 180 },
          "fallbackCode": { "type": "string", "pattern": "^[0-9]{6}$" },
          "authorization_url": { "type": "string", "format": "uri" },
          "qr_code": { "type": "string", "format": "uri" },
          "links": { "type": "array", "minItems": 1, "description": "Только включённые в проекте мессенджеры со статусом up на момент ответа.", "items": { "$ref": "#/components/schemas/MessengerLink" } },
          "deliveryMode": { "type": "string", "enum": ["polling", "webhook"] }
        }
      },
      "ChallengeStatus": {
        "type": "object",
        "required": ["id", "challengeId", "status", "purpose", "deliveryMode", "messenger", "createdAt", "resolvedAt", "expiresAt"],
        "properties": {
          "id": { "type": "string" },
          "challengeId": { "type": "string" },
          "status": { "$ref": "#/components/schemas/ChallengeState" },
          "purpose": { "type": "string" },
          "deliveryMode": { "type": "string", "enum": ["polling", "webhook"] },
          "externalUserRef": { "type": "string" },
          "messenger": { "oneOf": [{ "$ref": "#/components/schemas/Messenger" }, { "type": "null" }] },
          "reason": { "type": "string", "enum": ["phone_mismatch", "foreign_contact", "invalid_phone"] },
          "createdAt": { "type": "string", "format": "date-time" },
          "resolvedAt": { "type": ["string", "null"], "format": "date-time" },
          "expiresAt": { "type": "string", "format": "date-time" }
        }
      },
      "PhoneProof": {
        "type": "object",
        "required": ["type", "messenger", "method", "expectedPhone", "reportedPhone", "phonesMatch", "phoneVerified"],
        "properties": {
          "type": { "const": "phone" },
          "messenger": { "oneOf": [{ "$ref": "#/components/schemas/Messenger" }, { "type": "null" }] },
          "method": { "type": ["string", "null"], "enum": ["shared_contact", "account_phone", null] },
          "expectedPhone": { "type": "string" },
          "reportedPhone": { "type": ["string", "null"] },
          "phonesMatch": { "type": "boolean" },
          "phoneVerified": { "type": "boolean" }
        }
      },
      "ConsumeResponse": {
        "type": "object",
        "required": ["challengeId", "projectId", "purpose", "proof", "resolvedAt"],
        "properties": {
          "challengeId": { "type": "string" },
          "projectId": { "type": "string" },
          "externalUserRef": { "type": "string" },
          "purpose": { "type": "string" },
          "proof": { "$ref": "#/components/schemas/PhoneProof" },
          "resolvedAt": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "PublicPresentation": {
        "type": "object",
        "required": ["id", "status", "outcome", "purpose", "resolvedAt", "expiresAt", "fallbackCode", "authorization_url", "links"],
        "properties": {
          "id": { "type": "string" },
          "status": { "$ref": "#/components/schemas/ChallengeState" },
          "outcome": { "type": ["string", "null"], "enum": ["confirmed", "rejected", null] },
          "purpose": { "type": "string" },
          "resolvedAt": { "type": ["string", "null"], "format": "date-time" },
          "expiresAt": { "type": "string", "format": "date-time" },
          "fallbackCode": { "type": ["string", "null"], "pattern": "^[0-9]{6}$" },
          "authorization_url": { "type": "string", "format": "uri" },
          "links": { "type": "array", "description": "Доступные сейчас каналы; список обновляется при падении или восстановлении адаптера.", "items": { "$ref": "#/components/schemas/MessengerLink" } }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "required": ["id", "type", "createdAt", "data"],
        "properties": {
          "id": { "type": "string" },
          "type": { "const": "challenge.completed" },
          "createdAt": { "type": "string", "format": "date-time" },
          "data": {
            "type": "object",
            "required": ["challengeId", "projectId", "status", "externalUserRef", "purpose", "proof", "resolvedAt"],
            "properties": {
              "challengeId": { "type": "string" },
              "projectId": { "type": "string" },
              "status": { "type": "string", "enum": ["confirmed", "rejected"] },
              "externalUserRef": { "type": ["string", "null"] },
              "purpose": { "type": "string" },
              "proof": { "$ref": "#/components/schemas/PhoneProof" },
              "resolvedAt": { "type": ["string", "null"], "format": "date-time" }
            }
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message", "requestId"],
            "properties": {
              "code": { "type": "string" },
              "message": { "type": "string" },
              "requestId": { "type": "string", "format": "uuid" }
            }
          }
        }
      }
    }
  }
}
