{
  "openapi": "3.1.0",
  "jsonSchemaDialect": "https://spec.openapis.org/oas/3.1/dialect/base",
  "info": {
    "title": "Uniqore Partner API",
    "summary": "Партнёрские заявки, регистрация и первая подтверждённая оплата",
    "version": "1.2.0",
    "description": "Партнёрский API Uniqore: создание заявок, получение статусов и подписанные уведомления о регистрации и первой подтверждённой оплате. Для sandbox используются отдельные реквизиты. Регистрация и первая оплата в sandbox синтетические, без реальных денег и контактов. Настройки HTTPS-получателя и процент согласуются отдельно. Все значения в примерах условные и не задают коммерческих условий.\n\nЗаявка действует 30 дней; подтверждённая привязка аккаунта не истекает. В рабочей среде привязывается новый аккаунт либо существующий неоплаченный аккаунт без реферера. До привязки учитывается последний действительный переход. Тело запроса ограничено 8192 байтами. Общий лимит: 300 запросов на партнёра за скользящие 60 секунд; диагностические доставки дополнительно ограничены пятью попытками за 60 секунд. Денежные выплаты, промокоды и уведомления о продлениях в эту версию не входят."
  },
  "servers": [
    {
      "url": "https://api.uniqore.ai",
      "description": "Рабочая среда. Sandbox-only маршруты здесь отсутствуют."
    },
    {
      "url": "https://api-sandbox.uniqore.ai",
      "description": "Партнёрская API-песочница с отдельными ключами и синтетическими аккаунтами и платежами. Готовность адреса подтверждает команда Uniqore. Ссылка заявки ведёт на информационную страницу этой среды."
    }
  ],
  "tags": [
    {
      "name": "Sandbox",
      "description": "Синтетические регистрация и первая оплата в отдельной среде без реальных денег"
    },
    {
      "name": "Referrals",
      "description": "Заявки своего партнёра"
    },
    {
      "name": "Diagnostics",
      "description": "Проверка ключа и подписанной доставки без регистрации или оплаты"
    },
    {
      "name": "Webhooks",
      "description": "Уведомления на один настроенный HTTPS-адрес партнёра"
    }
  ],
  "security": [
    {
      "PartnerApiKey": []
    }
  ],
  "paths": {
    "/v1/partner/sandbox/referrals/{referral_id}/events": {
      "servers": [
        {
          "url": "https://api-sandbox.uniqore.ai"
        }
      ],
      "post": {
        "tags": [
          "Sandbox"
        ],
        "operationId": "simulatePartnerSandboxEvent",
        "summary": "Запустить тестовую регистрацию или первую оплату",
        "description": "Доступен только в sandbox с её Bearer-ключом. Сначала создайте заявку, затем отправьте registration.completed, затем payment.first_succeeded. Реальные контакты и деньги не участвуют. Дата задаётся сервером, процент берётся из настроек интеграции. Повтор возвращает прежний event_id и исходное тело события. Другой тариф при повторе первой оплаты возвращает 409; изменение процента не меняет прежнее событие. Повтор сценария не запускает новую доставку. Уведомления доставляются по правилам webhook. После привязки истечение срока заявки не блокирует первую оплату. При приостановке создания доступны повторы уже созданных событий. В production этот метод отсутствует.",
        "parameters": [
          {
            "$ref": "#/components/parameters/ReferralId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SandboxScenario"
              },
              "examples": {
                "registration": {
                  "value": {
                    "type": "registration.completed"
                  }
                },
                "firstPayment": {
                  "value": {
                    "type": "payment.first_succeeded",
                    "plan": {
                      "id": "founder",
                      "name": "Основатель"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Повтор: прежний ID и текущее состояние доставки без нового бизнес-события.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SandboxScenarioResult"
                }
              }
            }
          },
          "202": {
            "description": "Сценарий принят; событие создано и ожидает доставки. Успешную доставку webhook проверьте отдельно.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SandboxScenarioResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadJson"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PartnerDisabled"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "partner_sandbox_registration_required: сначала выполните тестовую регистрацию; partner_sandbox_plan_conflict: первая оплата уже имеет другой тариф.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/BodyTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/PartnerPaused"
          }
        }
      }
    },
    "/v1/partner/test": {
      "get": {
        "tags": [
          "Diagnostics"
        ],
        "operationId": "testPartnerAccess",
        "summary": "Проверить API-ключ и готовность настроек",
        "description": "Проверяет ключ и настройки без изменения данных. Доступен включённому партнёру, в том числе при приостановке создания новых заявок. status=ok подтверждает ключ. referrals_enabled показывает, доступно ли создание новых заявок. webhook_configured означает наличие настроек получателя и секрета подписи, но не подтверждает доступность получателя. Для проверки доставки используйте POST /v1/partner/test-webhook.",
        "responses": {
          "200": {
            "description": "Ключ действителен, партнёр включён.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerTestStatus"
                },
                "example": {
                  "status": "ok",
                  "referrals_enabled": false,
                  "webhook_configured": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PartnerDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/partner/test-webhook": {
      "post": {
        "tags": [
          "Diagnostics"
        ],
        "operationId": "sendPartnerTestWebhook",
        "summary": "Выполнить одну доставку подписанного тестового webhook",
        "description": "Отправляет integration.test с test=true на настроенный HTTPS-адрес. Тело запроса только {}: адрес и секрет в запросе не принимаются. Подпись HMAC и требования к получателю совпадают с правилами рабочих событий. Проверка доступна включённому партнёру даже при приостановке создания новых заявок. Лимит: пять попыток за скользящие 60 секунд на партнёра сверх общего лимита API.\n\nМетод ждёт одну попытку до 25 секунд. HTTP 200 означает выполненную проверку; для подтверждения доставки проверьте delivered. При сетевой ошибке или ответе вне 2xx возвращаются delivered=false, http_status при наличии и код error. Каждый вызов создаёт новый тестовый event_id. Автоматических повторов нет. Тест не создаёт заявок, регистраций и оплат и не влияет на партнёрскую статистику.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {}
              },
              "example": {}
            }
          }
        },
        "responses": {
          "200": {
            "description": "Диагностический результат одной попытки; успех определяется delivered=true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TestWebhookResult"
                },
                "examples": {
                  "delivered": {
                    "value": {
                      "event_id": "758ab31e-f875-4d3a-9d9b-2f1c99532658",
                      "delivered": true,
                      "http_status": 204,
                      "error": null
                    }
                  },
                  "receiverRejected": {
                    "value": {
                      "event_id": "758ab31e-f875-4d3a-9d9b-2f1c99532658",
                      "delivered": false,
                      "http_status": 400,
                      "error": "webhook_http_error"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadJson"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PartnerDisabled"
          },
          "413": {
            "$ref": "#/components/responses/BodyTooLarge"
          },
          "422": {
            "description": "Не настроен адрес webhook (partner_webhook_required) или секрет подписи (partner_secret_required). Согласуйте настройки с командой Uniqore.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "addressMissing": {
                    "value": {
                      "error": {
                        "code": "partner_webhook_required",
                        "message": "Сначала настройте адрес webhook"
                      }
                    }
                  },
                  "secretMissing": {
                    "value": {
                      "error": {
                        "code": "partner_secret_required",
                        "message": "Сначала получите секрет подписи webhook"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/partner/referrals": {
      "post": {
        "tags": [
          "Referrals"
        ],
        "operationId": "createPartnerReferral",
        "summary": "Создать заявку и персональную ссылку",
        "description": "Для одного привлечённого пользователя создайте одну логическую заявку. Первое создание возвращает 201. Повтор с тем же Idempotency-Key и теми же нормализованными данными возвращает 200 с исходными referral_id, landing_url и expires_at. Отсутствие locale эквивалентно ru, отсутствие external_user_id эквивалентно null. Пробелы в значениях не обрезаются. Изменённое тело с тем же ключом возвращает 409. После сетевого сбоя или временного отказа сохраняйте исходные тело и ключ. Новые заявки при общей паузе возвращают 503; повтор уже созданной заявки остаётся доступен, если партнёр включён. Нельзя передать partner_id, контактные данные или произвольный redirect URL.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReferralCreateRequest"
              },
              "examples": {
                "withUser": {
                  "summary": "Сопоставление с пользователем партнёра",
                  "value": {
                    "external_user_id": "user-1842",
                    "locale": "ru"
                  }
                },
                "minimal": {
                  "summary": "Без внешнего идентификатора, язык ru",
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Идемпотентный повтор исходной заявки. Ссылка и срок не меняются.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReferralCreated"
                },
                "examples": {
                  "referral": {
                    "$ref": "#/components/examples/ReferralCreated"
                  }
                }
              }
            },
            "links": {
              "status": {
                "$ref": "#/components/links/ReferralStatus"
              }
            }
          },
          "201": {
            "description": "Новая заявка создана.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReferralCreated"
                },
                "examples": {
                  "referral": {
                    "$ref": "#/components/examples/ReferralCreated"
                  }
                }
              }
            },
            "links": {
              "status": {
                "$ref": "#/components/links/ReferralStatus"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadJson"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PartnerDisabled"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/BodyTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/PartnerPaused"
          }
        }
      }
    },
    "/v1/partner/referrals/{referral_id}": {
      "get": {
        "tags": [
          "Referrals"
        ],
        "operationId": "getPartnerReferral",
        "summary": "Прочитать статус своей заявки",
        "description": "Возвращает только заявку партнёра, определённого по API-ключу. Чужая и отсутствующая заявки одинаково возвращают 404. Поле external_user_id отсутствует, если его не задавали. Остальные недостигнутые даты и сведения возвращаются как null. Для существующего неоплаченного аккаунта account_was_new=false и registered_at=null; отдельное событие регистрации не отправляется. После привязки истечение исходного срока не отменяет привязку. first_payment совпадает с data события первой оплаты. Контакты и данные аккаунта, не указанные в схеме ответа, не передаются.",
        "parameters": [
          {
            "$ref": "#/components/parameters/ReferralId"
          }
        ],
        "responses": {
          "200": {
            "description": "Текущее состояние заявки.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReferralStatus"
                },
                "examples": {
                  "firstPaid": {
                    "$ref": "#/components/examples/FirstPaidStatus"
                  },
                  "existingAccount": {
                    "$ref": "#/components/examples/ExistingAccountLinkedStatus"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Параметр пути не является UUID; штатный отказ извлечения параметра.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PartnerDisabled"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "webhooks": {
    "partnerEvents": {
      "description": "Один конечный HTTPS-адрес партнёра принимает оба рабочих типа события и диагностический integration.test по запросу партнёра. Адрес получателя настраивается отдельно. Нужны публичная маршрутизация, доверенный TLS и порт 443; логин, пароль и URL-фрагмент запрещены. HTTP-перенаправления не выполняются. Если адрес не настроен, рабочие события сохраняются и доступны по статусу заявки; поздняя настройка не отправляет историю автоматически.",
      "servers": [
        {
          "url": "https://receiver.example/webhooks/uniqore",
          "description": "Условный пример получателя, который нужно заменить своим настроенным HTTPS-адресом. Это не endpoint API Uniqore."
        }
      ],
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "receivePartnerEvent",
        "summary": "Получить рабочее событие или проверочное уведомление",
        "description": "Uniqore инициирует POST на настроенный адрес. Доставка рабочих событий может повторяться, порядок событий не гарантирован. Проверьте HMAC-SHA256 по исходным байтам тела до разбора JSON, свежесть X-Uniqore-Timestamp и дедупликацию по event_id. Затем надёжно сохраните событие и верните короткий 2xx, например 204 без тела. Повтор уже сохранённого корректно подписанного события также должен получать 2xx без повторного бизнес-действия. integration.test подтверждает только подключение, не создаёт бизнес-действий и автоматически не повторяется. Общий HTTP-таймаут 10 секунд, ответ не более 16 КиБ.\n\nПосле отказа рабочего события предусмотрены шесть повторов с паузами 60, 300, 900, 3600, 21600 и 86400 секунд после предыдущей неудачной попытки. После исчерпания повторов обратитесь в команду Uniqore. При каждом повторе event_id и исходные байты тела неизменны, timestamp и подпись формируются заново. Изменение процента после события не меняет его тело.",
        "security": [
          {
            "WebhookSignature": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookTimestamp"
          },
          {
            "$ref": "#/components/parameters/WebhookSignature"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerEvent"
              },
              "examples": {
                "integrationTest": {
                  "$ref": "#/components/examples/IntegrationTest"
                },
                "registration": {
                  "$ref": "#/components/examples/RegistrationCompleted"
                },
                "firstPayment": {
                  "$ref": "#/components/examples/FirstPaymentSucceeded"
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Событие проверено и надёжно сохранено, либо уже сохранено ранее. Тело ответа отсутствует."
          },
          "2XX": {
            "description": "Иной короткий успешный ответ после проверки и надёжного сохранения события. Ответ должен полностью завершиться за 10 секунд и содержать не более 16 КиБ."
          },
          "default": {
            "description": "Неуспешное подтверждение. Для рабочего события ответ вне 2xx, сетевой отказ или таймаут приводит к повтору по расписанию. Проверочный integration.test не повторяется автоматически. Формат отказа определяет получатель."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "PartnerApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "partner_api_key",
        "description": "Authorization: Bearer <partner_api_key>. Используйте предоставленный API-ключ выбранной среды. Храните его на сервере и не включайте в клиентский код. Production и sandbox используют разные ключи. Партнёр определяется по ключу и должен быть включён."
      },
      "WebhookSignature": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Uniqore-Signature",
        "description": "Вычисляемая подпись, а не статический API-ключ: v1=<64 lowercase hex symbols>. HMAC-SHA256 от UTF-8-байтов X-Uniqore-Timestamp, точки и исходных байтов тела. Ключом являются UTF-8-байты отдельного секрета webhook, без Base64-декодирования. Сам секрет не входит в уведомление и не используется для авторизации запросов API. Используйте сравнение с постоянным временем. Проверьте отклонение timestamp, например не более пяти минут. Подпись и timestamp обновляются при повторе."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "description": "Один ключ на логическую заявку, до 200 байт. Не может быть пустым или состоять только из пробелов. Повторяйте исходный ключ после временных сбоев. Ключ учитывается в пределах партнёра и не нормализуется.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 200,
          "pattern": "\\S",
          "x-max-utf8-bytes": 200
        },
        "example": "recommendation-2026-10-07-user-1842"
      },
      "ReferralId": {
        "name": "referral_id",
        "in": "path",
        "required": true,
        "description": "Идентификатор сохранённой заявки своего партнёра.",
        "schema": {
          "$ref": "#/components/schemas/Uuid"
        },
        "example": "92d3d9c1-9b04-4b35-9843-9e82c23a6ed3"
      },
      "WebhookTimestamp": {
        "name": "X-Uniqore-Timestamp",
        "in": "header",
        "required": true,
        "description": "Unix timestamp в секундах, сформированный для данной попытки доставки. Подписывается именно исходная строка заголовка. Не путать с occurred_at или paid_at события.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9]+$"
        }
      },
      "WebhookSignature": {
        "name": "X-Uniqore-Signature",
        "in": "header",
        "required": true,
        "description": "Подпись HMAC-SHA256: v1= и 64 шестнадцатеричных символа в нижнем регистре. Вычисление описано в схеме безопасности WebhookSignature.",
        "schema": {
          "type": "string",
          "pattern": "^v1=[0-9a-f]{64}$"
        }
      }
    },
    "schemas": {
      "SandboxScenario": {
        "oneOf": [
          {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "const": "registration.completed"
              }
            }
          },
          {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "type",
              "plan"
            ],
            "properties": {
              "type": {
                "const": "payment.first_succeeded"
              },
              "plan": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "id",
                  "name"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200,
                    "pattern": "\\S",
                    "x-max-utf8-bytes": 200
                  },
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200,
                    "pattern": "\\S"
                  }
                }
              }
            }
          }
        ]
      },
      "SandboxScenarioResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "referral_id",
          "event_id",
          "type",
          "delivery_status",
          "replayed"
        ],
        "properties": {
          "referral_id": {
            "$ref": "#/components/schemas/Uuid"
          },
          "event_id": {
            "$ref": "#/components/schemas/Uuid"
          },
          "type": {
            "enum": [
              "registration.completed",
              "payment.first_succeeded"
            ]
          },
          "delivery_status": {
            "enum": [
              "unconfigured",
              "pending",
              "delivering",
              "delivered",
              "failed"
            ]
          },
          "replayed": {
            "type": "boolean"
          }
        }
      },
      "PartnerTestStatus": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "status",
          "referrals_enabled",
          "webhook_configured"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "referrals_enabled": {
            "type": "boolean",
            "description": "Общий флаг создания новых заявок; false не мешает проверкам."
          },
          "webhook_configured": {
            "type": "boolean",
            "description": "У партнёра заданы адрес и секрет подписи. Доступность получателя этим флагом не проверяется."
          }
        }
      },
      "TestWebhookResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "event_id",
          "delivered",
          "http_status",
          "error"
        ],
        "properties": {
          "event_id": {
            "$ref": "#/components/schemas/Uuid"
          },
          "delivered": {
            "type": "boolean",
            "description": "true только после короткого полного ответа получателя со статусом 2xx."
          },
          "http_status": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 100,
            "maximum": 599,
            "description": "Статус получателя либо null, если HTTP-ответ не получен."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              null,
              "webhook_destination_denied",
              "webhook_transport_failed",
              "webhook_secret_unavailable",
              "webhook_response_failed",
              "webhook_response_too_large",
              "webhook_http_error",
              "webhook_timeout"
            ],
            "description": "Код отказа проверки либо null при успехе. Тело ответа получателя не возвращается."
          }
        }
      },
      "IntegrationTestEvent": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "event_id",
          "type",
          "occurred_at",
          "test"
        ],
        "description": "Проверка HTTPS-получателя и HMAC по запросу партнёра. Не относится к реальному клиенту, не содержит referral_id, не означает регистрацию или оплату.",
        "properties": {
          "event_id": {
            "$ref": "#/components/schemas/Uuid"
          },
          "type": {
            "type": "string",
            "enum": [
              "integration.test"
            ]
          },
          "occurred_at": {
            "$ref": "#/components/schemas/UtcDateTime"
          },
          "test": {
            "type": "boolean",
            "const": true
          }
        }
      },
      "Uuid": {
        "type": "string",
        "format": "uuid"
      },
      "UtcDateTime": {
        "type": "string",
        "format": "date-time",
        "description": "Дата и время по RFC 3339 в UTC."
      },
      "NullableUtcDateTime": {
        "type": [
          "string",
          "null"
        ],
        "format": "date-time",
        "description": "Дата и время по RFC 3339 в UTC либо null, если этап не достигнут."
      },
      "ExternalUserId": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200,
        "pattern": "\\S",
        "x-max-utf8-bytes": 200,
        "description": "Непрозрачный идентификатор пользователя в системе партнёра, до 200 байт UTF-8. Не может состоять только из пробелов. maxLength задаёт верхнюю границу по символам; ограничение байтов проверяет сервер. Значение не обрезается и не нормализуется. Не передавайте email или телефон.",
        "examples": [
          "user-1842"
        ]
      },
      "ReferralCreateRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "Оба поля необязательны; допустимо пустое JSON-тело {}. Неизвестные поля отклоняются.",
        "properties": {
          "external_user_id": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ExternalUserId"
              },
              {
                "type": "null"
              }
            ],
            "description": "Можно не передавать или передать null. Используется только для сопоставления на стороне партнёра."
          },
          "locale": {
            "type": "string",
            "enum": [
              "ru",
              "en",
              "kk"
            ],
            "default": "ru",
            "description": "Язык ссылки: en ведёт на корень сайта, ru на /ru/, kk на /kk/. null не допускается."
          }
        }
      },
      "ReferralCreated": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "referral_id",
          "landing_url",
          "expires_at"
        ],
        "properties": {
          "referral_id": {
            "$ref": "#/components/schemas/Uuid"
          },
          "landing_url": {
            "type": "string",
            "format": "uri",
            "description": "Готовая персональная HTTPS-ссылка с непрозрачным токеном. Передайте её одному привлечённому пользователю без изменения."
          },
          "expires_at": {
            "$ref": "#/components/schemas/UtcDateTime",
            "description": "Истечение через 30 дней с создания. Идемпотентный повтор не продлевает срок."
          }
        }
      },
      "ReferralStatus": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "referral_id",
          "status",
          "created_at",
          "expires_at",
          "visited_at",
          "linked_at",
          "registered_at",
          "first_paid_at",
          "account_was_new",
          "ineligible_reason",
          "first_payment"
        ],
        "properties": {
          "referral_id": {
            "$ref": "#/components/schemas/Uuid"
          },
          "external_user_id": {
            "$ref": "#/components/schemas/ExternalUserId",
            "description": "Поле отсутствует, если идентификатор не задавался в заявке; null не возвращается."
          },
          "status": {
            "type": "string",
            "enum": [
              "created",
              "visited",
              "linked",
              "first_paid",
              "ineligible",
              "expired"
            ],
            "description": "created: создана; visited: посещение зафиксировано; linked: аккаунт привязан; first_paid: первая оплата зафиксирована; ineligible: привязка отклонена; expired: срок непривязанной заявки истёк. После привязки статус не становится expired."
          },
          "created_at": {
            "$ref": "#/components/schemas/UtcDateTime"
          },
          "expires_at": {
            "$ref": "#/components/schemas/UtcDateTime"
          },
          "visited_at": {
            "$ref": "#/components/schemas/NullableUtcDateTime"
          },
          "linked_at": {
            "$ref": "#/components/schemas/NullableUtcDateTime"
          },
          "registered_at": {
            "$ref": "#/components/schemas/NullableUtcDateTime",
            "description": "Время новой регистрации. Для привязки существующего неоплаченного аккаунта остаётся null."
          },
          "first_paid_at": {
            "$ref": "#/components/schemas/NullableUtcDateTime",
            "description": "Дата первой оплаты, совпадает с first_payment.paid_at."
          },
          "account_was_new": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "true для нового аккаунта, false для существовавшего; null до привязки."
          },
          "ineligible_reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "already_referred",
              "already_paid",
              null
            ],
            "description": "already_referred: у аккаунта уже есть реферер; already_paid: ранее была успешная оплата; null при отсутствии причины."
          },
          "first_payment": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/FirstPaymentData"
              },
              {
                "type": "null"
              }
            ],
            "description": "Тот же объект, что data в payment.first_succeeded; null до первой подтверждённой оплаты."
          }
        }
      },
      "PaidPlan": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Идентификатор оплаченного тарифа."
          },
          "name": {
            "type": "string",
            "description": "Название оплаченного тарифа, сохранённое при подтверждении оплаты."
          }
        }
      },
      "RewardRatePercent": {
        "type": "string",
        "pattern": "^(?:100\\.00|(?:[1-9]?[0-9])\\.[0-9]{2})$",
        "description": "Процент от 0.00 до 100.00 включительно, десятичная строка с двумя знаками. Снимок явно настроенного процента на момент подтверждения оплаты. 12.50 в примерах является условным значением.",
        "examples": [
          "12.50",
          "17.25"
        ]
      },
      "FirstPaymentData": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "paid_at",
          "plan",
          "reward_rate_percent"
        ],
        "description": "Неизменяемые сведения о первой подтверждённой оплате. Сумма, валюта и контакты не передаются.",
        "properties": {
          "paid_at": {
            "$ref": "#/components/schemas/UtcDateTime",
            "description": "Дата самой оплаты, которая может отличаться от occurred_at события."
          },
          "plan": {
            "$ref": "#/components/schemas/PaidPlan"
          },
          "reward_rate_percent": {
            "$ref": "#/components/schemas/RewardRatePercent"
          }
        }
      },
      "RegistrationCompletedEvent": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "event_id",
          "type",
          "occurred_at",
          "referral_id"
        ],
        "description": "Одна настоящая регистрация нового подтверждённого аккаунта с партнёрской привязкой. Привязка существующего неоплаченного аккаунта и вход через Google в существующий email-аккаунт не создают событие.",
        "properties": {
          "event_id": {
            "$ref": "#/components/schemas/Uuid",
            "description": "Неизменяемый идентификатор события. Используйте для защиты от повторной обработки."
          },
          "occurred_at": {
            "$ref": "#/components/schemas/UtcDateTime",
            "description": "Время фиксации события. Для оплаты это момент подтверждения, который может отличаться от data.paid_at."
          },
          "referral_id": {
            "$ref": "#/components/schemas/Uuid"
          },
          "external_user_id": {
            "$ref": "#/components/schemas/ExternalUserId",
            "description": "Идентификатор из исходной заявки; поле отсутствует, если его не задавали."
          },
          "type": {
            "type": "string",
            "enum": [
              "registration.completed"
            ]
          }
        }
      },
      "FirstPaymentSucceededEvent": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "event_id",
          "type",
          "occurred_at",
          "referral_id",
          "data"
        ],
        "description": "Одна первая подтверждённая оплата привязанного аккаунта. Создание счёта, trial, выдача доступа без платежа, повторное подтверждение и последующие оплаты не создают нового события. Возврат не сбрасывает факт первой оплаты.",
        "properties": {
          "event_id": {
            "$ref": "#/components/schemas/Uuid",
            "description": "Неизменяемый идентификатор события. Используйте для защиты от повторной обработки."
          },
          "occurred_at": {
            "$ref": "#/components/schemas/UtcDateTime",
            "description": "Время фиксации события. Для оплаты это момент подтверждения, который может отличаться от data.paid_at."
          },
          "referral_id": {
            "$ref": "#/components/schemas/Uuid"
          },
          "external_user_id": {
            "$ref": "#/components/schemas/ExternalUserId",
            "description": "Идентификатор из исходной заявки; поле отсутствует, если его не задавали."
          },
          "type": {
            "type": "string",
            "enum": [
              "payment.first_succeeded"
            ]
          },
          "data": {
            "$ref": "#/components/schemas/FirstPaymentData"
          }
        }
      },
      "PartnerEvent": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/IntegrationTestEvent"
          },
          {
            "$ref": "#/components/schemas/RegistrationCompletedEvent"
          },
          {
            "$ref": "#/components/schemas/FirstPaymentSucceededEvent"
          }
        ],
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "integration.test": "#/components/schemas/IntegrationTestEvent",
            "registration.completed": "#/components/schemas/RegistrationCompletedEvent",
            "payment.first_succeeded": "#/components/schemas/FirstPaymentSucceededEvent"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Машинный код ошибки. Используйте его вместе с HTTP-статусом для обработки."
              },
              "message": {
                "type": "string",
                "description": "Пояснение. Текст не является стабильным идентификатором ошибки."
              }
            }
          }
        }
      }
    },
    "responses": {
      "BadJson": {
        "description": "Тело не удалось разобрать: неверный JSON, тип, неизвестные поля или Content-Type.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "bad_json",
                "message": "Тело запроса не разобрано"
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Ключ отсутствует, недействителен или отозван.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "unauthorized",
                "message": "Недействительный или отозванный токен"
              }
            }
          }
        }
      },
      "PartnerDisabled": {
        "description": "Партнёр отключён; уточните включение у команды Uniqore.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "partner_disabled",
                "message": "Партнёр отключён"
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Заявка не найдена либо принадлежит другому партнёру.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "partner_not_found",
                "message": "Заявка или партнёр не найден"
              }
            }
          }
        }
      },
      "IdempotencyConflict": {
        "description": "Idempotency-Key уже использован с другими нормализованными данными. Не заменяйте ключ автоматически: сверяйте исходную заявку.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "partner_idempotency_conflict",
                "message": "Этот ключ уже использован с другими данными"
              }
            }
          }
        }
      },
      "BodyTooLarge": {
        "description": "Тело запроса превышает 8192 байта.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "body_too_large",
                "message": "Запрос слишком большой — вероятно, длинная запись звонка"
              }
            }
          }
        }
      },
      "InvalidInput": {
        "description": "Некорректные значения external_user_id, locale или отсутствующий/некорректный Idempotency-Key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "partner_invalid_input",
                "message": "Проверьте данные партнёрской интеграции"
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Лимит API превышен. Снизьте частоту; повторяйте позже с увеличивающейся паузой и исходным Idempotency-Key при создании.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "partner_rate_limited",
                "message": "Слишком много запросов. Повторите позже"
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Временная ошибка сервера. Повторите позже, сохраняя тело и Idempotency-Key при создании заявки.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "internal",
                "message": "Внутренняя ошибка сервера"
              }
            }
          }
        }
      },
      "PartnerPaused": {
        "description": "Создание новых заявок приостановлено общим флагом. Идемпотентный повтор существующей заявки доступен для включённого партнёра.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "partner_paused",
                "message": "Создание партнёрских заявок приостановлено"
              }
            }
          }
        }
      }
    },
    "links": {
      "ReferralStatus": {
        "operationId": "getPartnerReferral",
        "parameters": {
          "referral_id": "$response.body#/referral_id"
        },
        "description": "Прочитать результат созданной либо повторно возвращённой заявки с тем же API-ключом."
      }
    },
    "examples": {
      "IntegrationTest": {
        "summary": "Проверочная доставка без бизнес-действий",
        "value": {
          "event_id": "758ab31e-f875-4d3a-9d9b-2f1c99532658",
          "type": "integration.test",
          "occurred_at": "2026-10-08T12:00:00Z",
          "test": true
        }
      },
      "ReferralCreated": {
        "summary": "Условная заявка, действующая 30 дней",
        "value": {
          "referral_id": "92d3d9c1-9b04-4b35-9843-9e82c23a6ed3",
          "landing_url": "https://uniqore.ai/ru/?ref=uqr_example_token",
          "expires_at": "2026-11-06T12:00:00Z"
        }
      },
      "FirstPaidStatus": {
        "summary": "Новый аккаунт зарегистрирован и впервые оплатил",
        "value": {
          "referral_id": "92d3d9c1-9b04-4b35-9843-9e82c23a6ed3",
          "external_user_id": "user-1842",
          "status": "first_paid",
          "created_at": "2026-10-07T12:00:00Z",
          "expires_at": "2026-11-06T12:00:00Z",
          "visited_at": "2026-10-07T12:01:00Z",
          "linked_at": "2026-10-07T12:05:00Z",
          "registered_at": "2026-10-07T12:05:00Z",
          "first_paid_at": "2026-10-08T09:00:00Z",
          "account_was_new": true,
          "ineligible_reason": null,
          "first_payment": {
            "paid_at": "2026-10-08T09:00:00Z",
            "plan": {
              "id": "example-plan",
              "name": "Пример тарифа"
            },
            "reward_rate_percent": "12.50"
          }
        }
      },
      "ExistingAccountLinkedStatus": {
        "summary": "Существующий неоплаченный аккаунт привязан без события регистрации",
        "value": {
          "referral_id": "92d3d9c1-9b04-4b35-9843-9e82c23a6ed3",
          "external_user_id": "user-1842",
          "status": "linked",
          "created_at": "2026-10-07T12:00:00Z",
          "expires_at": "2026-11-06T12:00:00Z",
          "visited_at": "2026-10-07T12:01:00Z",
          "linked_at": "2026-10-07T12:05:00Z",
          "registered_at": null,
          "first_paid_at": null,
          "account_was_new": false,
          "ineligible_reason": null,
          "first_payment": null
        }
      },
      "RegistrationCompleted": {
        "summary": "Новая регистрация",
        "value": {
          "event_id": "2ca4a517-4045-4425-96df-e2d7d0ac0d11",
          "type": "registration.completed",
          "occurred_at": "2026-10-07T12:05:00Z",
          "referral_id": "92d3d9c1-9b04-4b35-9843-9e82c23a6ed3",
          "external_user_id": "user-1842"
        }
      },
      "FirstPaymentSucceeded": {
        "summary": "Первая оплата, без суммы и валюты",
        "value": {
          "event_id": "c36092d3-5bfb-4f54-9bf1-4dbeb6f7b635",
          "type": "payment.first_succeeded",
          "occurred_at": "2026-10-08T09:10:00Z",
          "referral_id": "92d3d9c1-9b04-4b35-9843-9e82c23a6ed3",
          "external_user_id": "user-1842",
          "data": {
            "paid_at": "2026-10-08T09:00:00Z",
            "plan": {
              "id": "example-plan",
              "name": "Пример тарифа"
            },
            "reward_rate_percent": "12.50"
          }
        }
      }
    }
  }
}

