# Подключение партнёра к Uniqore

Описание API и доступа для технической команды партнёра.
Контракт API и примеры запросов актуальны на 12 октября 2026 года.
Для API-проверки регистрации и первой оплаты используйте отдельное
[руководство sandbox](/guide).
Для запросов используется действующий API-ключ включённого партнёра;
для тестовой доставки дополнительно нужен настроенный получатель webhook.
Подключение к рабочей среде и начало привлечения пользователей
согласуются отдельно после приёмки интеграции.

Машиночитаемый контракт: [partner-api.openapi.json](/openapi.json).
Формат [OpenAPI 3.1.0](https://spec.openapis.org/oas/v3.1.0.html), JSON.
В этом руководстве описаны четыре метода основной интеграции и HTTPS-получатель
рабочих и тестовых уведомлений. Дополнительный метод API-песочницы описан
в отдельном руководстве sandbox.

## Что вы получаете

Для каждого привлечённого пользователя ваша система создаёт заявку и получает
персональную ссылку на Uniqore. Через API можно посмотреть результат этой заявки,
а через webhook получить уведомления о новой регистрации и первой подтверждённой
оплате. Webhook означает HTTP-уведомление, которое Uniqore отправляет на ваш сервер.

Партнёрский учёт происходит в фоне. Пользователь скачивает приложение и проходит
обычный вход через email или Google. Он не вводит партнёрский код и не разбирается
в привязке ссылки. Эти способы входа относятся к пользователю приложения.

## Доступ и кабинет

Отдельного партнёрского кабинета в этой версии нет.
Ваша система получает статусы заявок через API и уведомления webhook.

## Порядок подключения

1. Выберите идентификатор пользователя вашей системы для `external_user_id`.
   Можно сопоставлять пользователей только по `referral_id`.
2. Подготовьте конечный HTTPS-адрес для `registration.completed`,
   `payment.first_succeeded` и проверочного `integration.test`.
3. Используйте предоставленные реквизиты выбранной среды. API-ключ применяется
   в запросах к Uniqore, отдельный секрет используется для проверки подписи webhook.
   Храните оба на сервере. Production и sandbox используют разные реквизиты.
4. Настройте запросы, проверку подписи и защиту от повторной обработки по `event_id`.
5. Совместно проверьте заявку, идемпотентность, статус, оба события
   и повтор доставки после временного отказа.
6. Начало работы в основной среде согласуется после приёмки интеграции.

## Общие правила API

Базовый адрес рабочей среды: `https://api.uniqore.ai`.
Адрес sandbox: `https://api-sandbox.uniqore.ai`. Используйте её отдельные
реквизиты до согласованного запуска в основной среде.

Формат тела и ответа: JSON. Даты: RFC 3339 в UTC.
Авторизация каждого запроса:

```http
Authorization: Bearer <partner_api_key>
```

Партнёр определяется по ключу. Поле `partner_id` в запросах не передаётся.
Контракт содержит следующие методы:

| Метод | Путь | Результат |
|---|---|---|
| `GET` | `/v1/partner/test` | Проверить ключ и готовность настроек, дополнение 1.1.0 |
| `POST` | `/v1/partner/test-webhook` | Выполнить тестовую доставку, дополнение 1.1.0 |
| `POST` | `/v1/partner/referrals` | Создать заявку и получить ссылку |
| `GET` | `/v1/partner/referrals/{referral_id}` | Прочитать состояние своей заявки |

Метода получения общего списка заявок, изменения процента, настройки webhook
или управления сотрудниками через партнёрский API сейчас нет. Храните выданные
`referral_id` в своей системе. Заявка относится к одному привлечённому пользователю;
одну ссылку не следует распространять как общую ссылку на всю аудиторию.

Тело запроса ограничено 8192 байтами. Текущий серверный ограничитель допускает
300 запросов за скользящие 60 секунд суммарно для всех методов одного партнёра.
Тестовые доставки дополнительно ограничены пятью попытками за 60 секунд.
Обрабатывайте ответ `429` и уменьшайте частоту запросов; предпочтительно получать
изменения через webhook, а статус читать для сверки или восстановления.

## Проверка подключения

Проверки используют выданный API-ключ и доступны для включённого партнёра
даже при общей паузе создания заявок. Создавать проверочный аккаунт или проводить
платёж для этих методов не требуется.

### Проверить API-ключ

```bash
curl https://api.uniqore.ai/v1/partner/test \
  -H "Authorization: Bearer <API_KEY>"
```

Пример ответа `200`:

```json
{
  "status": "ok",
  "referrals_enabled": false,
  "webhook_configured": true
}
```

`status: "ok"` подтверждает доступ по ключу. `referrals_enabled` показывает,
разрешено ли создание новых заявок общим флагом; `false` не мешает тестированию.
`webhook_configured` означает, что у партнёра настроены адрес и секрет подписи.
Следующий метод проверяет доставку; ваш получатель должен проверить подпись
до подтверждения запроса.
Неверный или отозванный ключ возвращает `401`, отключённый партнёр — `403`.

### Отправить тестовый webhook

```bash
curl https://api.uniqore.ai/v1/partner/test-webhook \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{}'
```

Тело запроса должно быть `{}`. Используется согласованный HTTPS-адрес
получателя. На него придёт подписанное уведомление:

```json
{
  "event_id": "758ab31e-f875-4d3a-9d9b-2f1c99532658",
  "type": "integration.test",
  "occurred_at": "2026-10-08T12:00:00Z",
  "test": true
}
```

Проверьте HMAC по исходным байтам тем же способом, что и для рабочих событий,
и верните короткий `2xx`. Тип `integration.test` служит только проверке:
он не означает регистрацию или оплату и не должен создавать бизнес-действия.

Пример успешного результата API:

```json
{
  "event_id": "758ab31e-f875-4d3a-9d9b-2f1c99532658",
  "delivered": true,
  "http_status": 204,
  "error": null
}
```

Метод ждёт результат одной попытки, не более 25 секунд вместе с проверкой адреса.
HTTP `200` означает, что проверка выполнена. Успешную доставку подтверждает
`delivered: true`; отказ получателя или сети возвращает `delivered: false`.
`http_status` содержит статус получателя либо `null`, если ответ не получен.
`error` содержит код причины: `webhook_destination_denied`,
`webhook_transport_failed`, `webhook_secret_unavailable`, `webhook_response_failed`,
`webhook_response_too_large`, `webhook_http_error` или `webhook_timeout`.

Если адрес не настроен, API возвращает `422 partner_webhook_required`;
если нет секрета подписи, `422 partner_secret_required`. Уточните настройки
у команды Uniqore. Слишком частые проверки возвращают `429`.

Каждый вызов создаёт новый тестовый `event_id`. Автоматических повторов у теста нет;
заявки, регистрации, оплаты и статистика партнёра остаются прежними.
Для получателя действуют те же HTTPS-ограничения, HTTP-таймаут 10 секунд
и предел ответа 16 КиБ, что и для рабочих событий.

## Создать заявку

```http
POST /v1/partner/referrals
Authorization: Bearer <partner_api_key>
Content-Type: application/json
Idempotency-Key: recommendation-2026-10-07-user-1842
```

```json
{
  "external_user_id": "user-1842",
  "locale": "ru"
}
```

| Поле или заголовок | Обязательность | Правило |
|---|---|---|
| `Idempotency-Key` | Обязателен | Непустой ключ длиной до 200 байт; один на логическую заявку |
| `external_user_id` | Необязателен | Строка до 200 байт; если задана, не может быть пустой или состоять только из пробелов |
| `locale` | Необязателен | `ru`, `en` или `kk`; при отсутствии используется `ru` |

`external_user_id` используется только для сопоставления в вашей системе.
Передавайте непрозрачный идентификатор, а не email или телефон. Дополнительные
поля не принимаются. Адрес перенаправления партнёр не задаёт.

Пример запроса с `curl`:

```bash
curl --request POST 'https://api.uniqore.ai/v1/partner/referrals' \
  --header 'Authorization: Bearer <partner_api_key>' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: recommendation-2026-10-07-user-1842' \
  --data '{"external_user_id":"user-1842","locale":"ru"}'
```

Ответ на первую успешную запись: HTTP `201`.
Идентификаторы, даты, токен ссылки и тарифы во всех примерах вымышленные.

```json
{
  "referral_id": "92d3d9c1-9b04-4b35-9843-9e82c23a6ed3",
  "landing_url": "https://uniqore.ai/ru/?ref=uqr_example_token",
  "expires_at": "2026-11-06T12:00:00Z"
}
```

Сохраните `referral_id`, связь с вашим пользователем, `Idempotency-Key` и ссылку.
Покажите пользователю именно полученный `landing_url`. Для `en` ссылка ведёт
на корень сайта, для `ru` на `/ru/`, для `kk` на `/kk/`.

Заявка действует 30 дней с создания. Повтор с тем же ключом и теми же данными
возвращает HTTP `200` и исходные идентификатор, ссылку и срок, без новой заявки.
Пропущенный `locale` и явный `"ru"` эквивалентны. Пропущенный `external_user_id`
и `null` эквивалентны. Пробелы внутри значений строк автоматически не удаляются.

При сетевом сбое повторите исходный запрос с тем же ключом. Тот же ключ с другими
данными возвращает HTTP `409`. Такой конфликт требует проверки вашей записи;
автоматическая замена ключа может создать вторую заявку.

## Прочитать статус заявки

```bash
curl 'https://api.uniqore.ai/v1/partner/referrals/92d3d9c1-9b04-4b35-9843-9e82c23a6ed3' \
  --header 'Authorization: Bearer <partner_api_key>'
```

Успех: HTTP `200`. Пример после первой оплаты:

```json
{
  "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"
  }
}
```

| `status` | Значение |
|---|---|
| `created` | Заявка создана, посещение ссылки ещё не зафиксировано |
| `visited` | Посещение ссылки зафиксировано, аккаунт ещё не привязан |
| `linked` | Аккаунт привязан; это может быть новый или существующий аккаунт |
| `first_paid` | Первая подтверждённая оплата привязанного аккаунта зафиксирована |
| `ineligible` | Привязка отклонена по правилам учёта |
| `expired` | Срок непривязанной заявки истёк |

`external_user_id` отсутствует в ответе, если его не передавали.
Остальные недостигнутые даты и сведения возвращаются как `null`.
`account_was_new` равен `true` для нового аккаунта, `false` для существовавшего,
а до привязки равен `null`.

`registered_at` заполняется только для события новой регистрации.
`first_paid_at` и `first_payment.paid_at` обозначают дату оплаты;
`first_payment` совпадает с объектом `data` в уведомлении об оплате.
Причины `ineligible_reason`: `already_referred` для уже привязанного аккаунта
и `already_paid` для аккаунта с предыдущей успешной оплатой.

Привязка не истекает через 30 дней: после неё статус остаётся `linked` или
`first_paid`. Чужая и отсутствующая заявка одинаково возвращают HTTP `404`.
Контакты и данные аккаунта, не указанные в схеме ответа, не передаются.

## Уведомления webhook

Получатель должен быть доступен из публичного интернета по HTTPS на порту 443
с доверенным сертификатом. В адресе не допускаются логин, пароль или фрагмент
после `#`. Локальные адреса и закрытые сети не поддерживаются.
Uniqore не следует HTTP-перенаправлениям: укажите конечный адрес обработчика.

Доставка выполняется методом `POST`, тип содержимого `application/json`.
Поддерживаются два типа рабочего события и диагностическое `integration.test`
по запросу партнёра, описанное в разделе «Проверка подключения».
Если `external_user_id` не задавали, поле отсутствует в обоих рабочих уведомлениях.

### Новая регистрация

```json
{
  "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"
}
```

Событие означает создание нового подтверждённого аккаунта с этой привязкой.
Открытие сайта, создание заявки и скачивание приложения его не создают.
Вход через Google в существующий email-аккаунт не считается новой регистрацией.
Для привязки существующего неоплаченного аккаунта это событие не отправляется;
результат виден в API как `linked` с `account_was_new: false`.

### Первая подтверждённая оплата

```json
{
  "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"
  }
}
```

`occurred_at` обозначает момент фиксации события после подтверждения оплаты,
`data.paid_at` обозначает дату самой оплаты. Эти даты могут различаться.
`data.plan` содержит идентификатор и название оплаченного тарифа, сохранённые
при подтверждении. `reward_rate_percent` является строкой с двумя знаками после
точки и содержит явно настроенный процент на момент подтверждения оплаты.
Значение `"12.50"` в примере не является коммерческим предложением.

Сумма, валюта и контакты в уведомление не входят.
Создание счёта, пробный период и выдача доступа без платежа событие не создают.
Повторное подтверждение, последующие оплаты и возврат не создают нового события
первой оплаты. Изменение тарифа или процента после события не меняет его тело.
API не рассчитывает денежную выплату и не запускает перечисление вознаграждения.

### Проверка подписи

В каждом запросе передаются заголовки:

```http
X-Uniqore-Timestamp: <unix_seconds>
X-Uniqore-Signature: v1=<hex_digest>
```

Алгоритм подписи: HMAC-SHA256, код аутентификации сообщения с общим секретом.
Секрет используется как строка в UTF-8, без дополнительного декодирования.
Подписываются байты строки timestamp, затем точка, затем исходные байты тела:

```text
HMAC-SHA256(webhook_secret, timestamp + "." + raw_body)
```

Сохраните исходное тело до разбора JSON. Повторная сериализация JSON, изменение
пробелов или порядка полей даст другую подпись. Сравнивайте подписи с постоянным
временем выполнения и проверяйте отклонение timestamp от текущего времени,
например, не более пяти минут. Часы получателя должны быть синхронизированы.

Пример проверки на Node.js, без сторонних библиотек:

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyWebhook(rawBody, timestamp, signature, secret,
  nowSeconds = Math.floor(Date.now() / 1000)) {
  if (!Buffer.isBuffer(rawBody) || typeof timestamp !== 'string' ||
      typeof signature !== 'string' || typeof secret !== 'string' || !secret) {
    return false;
  }
  if (!/^[0-9]+$/.test(timestamp)) return false;
  const seconds = Number(timestamp);
  if (!Number.isSafeInteger(seconds) || Math.abs(nowSeconds - seconds) > 300) {
    return false;
  }
  const match = /^v1=([0-9a-f]{64})$/.exec(signature);
  if (!match) return false;
  const expected = createHmac('sha256', Buffer.from(secret, 'utf8'))
    .update(timestamp + '.', 'utf8')
    .update(rawBody)
    .digest();
  return timingSafeEqual(expected, Buffer.from(match[1], 'hex'));
}
```

Эта функция проверяет подпись и время доставки. Разбор схемы события, сохранение
и защита по `event_id` выполняются обработчиком отдельно, после успешной проверки.

### Подтверждение и повтор доставки

1. Проверьте подпись и timestamp, затем разберите JSON и проверьте тип события.
2. Надёжно сохраните событие с уникальным `event_id`. Повтор уже сохранённого
   события не должен повторять бизнес-действие.
3. Верните короткий успешный ответ `2xx`, например `204` без тела.
   Долгую обработку выполняйте после сохранения в своей очереди.

Для сетевого запроса установлен общий таймаут 10 секунд. Тело ответа получателя
не должно превышать 16 КиБ. Ошибка, таймаут или ответ вне `2xx` означают отказ
доставки. Повторный корректно подписанный запрос с известным `event_id` также
должен получать `2xx`.

Uniqore может доставить одно событие несколько раз; порядок получения событий
не гарантируется. После отказа запланированы шесть повторов с паузами 1 минута,
5 минут, 15 минут, 1 час, 6 часов и 24 часа после предыдущей неудачной попытки.
После исчерпания повторов обратитесь в команду Uniqore.

Во всех повторах сохраняются исходные `event_id` и байты тела, но timestamp
и подпись формируются заново. Это относится и к ручному повтору.

Если адрес webhook пока не настроен, события сохраняются, а результат можно
прочитать через API. Настройка адреса позднее не отправляет историю автоматически;
повтор нужных событий согласуйте с командой Uniqore.

## Ошибки API

Обычный ответ ошибки имеет форму:

```json
{
  "error": {
    "code": "partner_idempotency_conflict",
    "message": "Этот ключ уже использован с другими данными"
  }
}
```

Ориентируйтесь на HTTP-статус и `error.code`, а не на текст `message`.

| HTTP | `error.code` | Что делать |
|---|---|---|
| `400` | `bad_json` | Проверить JSON, типы и разрешённые поля, заголовок `Content-Type` |
| `401` | `unauthorized` | Проверить заголовок и актуальность API-ключа |
| `403` | `partner_disabled` | Уточнить статус включения у команды Uniqore |
| `404` | `partner_not_found` | Проверить сохранённый `referral_id` и принадлежность заявки |
| `409` | `partner_idempotency_conflict` | Сверить исходное тело для этого ключа |
| `413` | `body_too_large` | Уменьшить тело до допустимого размера |
| `422` | `partner_invalid_input` | Проверить значения полей и `Idempotency-Key` |
| `422` | `partner_webhook_required` | Попросить Uniqore настроить адрес webhook |
| `422` | `partner_secret_required` | Согласовать настройки подписи с командой Uniqore |
| `429` | `partner_rate_limited` | Снизить частоту и повторить с увеличивающейся паузой |
| `500` | `internal` | Повторить позже; при сохранении ошибки обратиться в Uniqore |
| `503` | `partner_paused` | Создание новых заявок приостановлено; согласовать возобновление |

При временной ошибке создания сохраняйте исходные тело и `Idempotency-Key`.
Повтор уже созданной заявки с теми же данными доступен при общей паузе создания,
если сам партнёр включён. `403`, `409` и ошибки входных данных не следует
повторять бесконечно без устранения причины.

## Правила учёта и границы версии

- Привязывается новый аккаунт или существующий аккаунт без успешных оплат
  и без прежнего реферера. Уже оплаченный или привязанный аккаунт не перепривязывается.
- До привязки учитывается последний действительный партнёрский переход.
  После привязки реферер аккаунта сохраняется при повторном входе и перезапуске.
- Пользователь должен продолжить установку и вход после перехода по своей
  партнёрской ссылке в том же браузере и на том же компьютере.
  Смена браузера, приватный режим или установка на другом компьютере могут
  оставить пользователя без подтверждённой привязки. Обычный вход при этом доступен.
- Отключение партнёра запрещает новые заявки и привязки; существующие привязки
  и учёт первой оплаты сохраняются.
- Партнёрский кабинет, промокоды, денежные выплаты и уведомления о продлениях
  в первую версию не входят.

## Что проверить вместе перед запуском

- Проверка ключа возвращает `status: "ok"`; тестовый `integration.test` проходит
  проверку подписи и получает `delivered: true` без регистрации или оплаты.
- Новая заявка возвращает `201`, повтор возвращает `200` с тем же результатом,
  изменённое тело с тем же ключом возвращает `409`.
- Статус и события содержат согласованный `external_user_id` и `referral_id`.
- На принятой Windows- и macOS-версии новый пользователь проходит установку,
  обычную регистрацию и первую подтверждённую оплату с ожидаемой привязкой.
- Получатель проверяет подпись обоих типов события по исходным байтам и сверяет
  их с API. Оплата содержит дату, тариф и согласованный процент.
- После временного отказа и ручного повтора событие сохраняет тело и идентификатор,
  а ваша система выполняет бизнес-действие один раз.
- Команда Uniqore подтверждает приёмку и отдельно разрешает рабочее включение.

