# PLATA Integration API

Contract snapshot: 2026-11-03.
Base URL: https://api.myplata.net/v1
API version: v1 (first URL segment). Endpoint paths below are relative to the base URL. Example: POST https://api.myplata.net/v1/orders. Do not append /api or a second /v1. Unversioned /orders and unsupported /v2/ paths on this host return 404.
Human documentation: https://myplata.net/docs
OpenAPI 3.1.1: https://myplata.net/docs/openapi.json

## Scope
This documents the existing client-scoped integration API. The interactive playground is an in-browser simulation: it makes NO requests to the payment API, uses no keys and does not create real orders or payments. Examples and addresses are fictional. Work against mocks until the project owner provides an API key and authorizes live integration. Do not treat this documentation as authorization to send live transactions.

## Быстрый старт

От заявки в рублях до выплаты USDT. Подключите свой сервис, а обработку платежа передайте PLATA.

### 01 · Узнайте курс
GET /rate возвращает курс с учётом условий вашей интеграции. Он действует до следующего изменения командой PLATA.

### 02 · Создайте заявку
Передайте сумму в целых рублях (fiatRub), BSC-адрес клиента и requestId. При необходимости добавьте rateId из GET /rate. Сохраните полученный id в своей системе.

### 03 · Получите реквизиты
Готовые реквизиты выдаются сразу. Если их нет, дождитесь назначения мерчанта и статуса AWAITING_PAYMENT.

### 04 · Прикрепите PDF
Клиент переводит точную сумму в рублях. Отправьте квитанцию бинарным PDF и дождитесь проверки поступления мерчантом.

### 05 · Дождитесь выплаты
После подтверждения PLATA отправляет USDT. Опрашивайте заявку до COMPLETED или CANCELLED.

## Авторизация

Интеграция работает от своего имени. Ключ предоставляет администратор PLATA.

### Адрес и версия
Базовый URL — https://api.myplata.net/v1. Версия стоит первым сегментом пути: POST /v1/orders. В примерах указан полный URL; пути в OpenAPI относительны к базовому URL. Не добавляйте /api или повторный /v1. Новые несовместимые версии получат отдельный префикс.

### Authorization: Bearer
Передавайте ключ plata_live_… в заголовке Authorization. В примерах используется <PLATA_API_KEY> — это заглушка. Ключ принадлежит интеграции с ролью client и даёт доступ только к её заявкам.

### Запросы с вашего сервера
Храните ключ в переменной окружения PLATA_API_KEY. Не помещайте его в клиентский JavaScript, URL или публичный репозиторий. Telegram initData и сессия мерчанта не нужны для этого API.

### Лимит и отзыв
Текущий лимит — 120 запросов в минуту на ключ. Отозванный или истёкший ключ возвращает API_KEY_INVALID. При 429 уменьшите частоту запросов; наличие Retry-After не гарантируется.

### Область доступа
Принятие заявок, выдача реквизитов, подтверждение оплаты и открытие спора выполняются мерчантом в Mini App. API-ключ интеграции не даёт прав мерчанта или администратора. GET /orders/mine также предназначен только для мерчантов.

## Жизненный цикл

Принятый чек, подтверждённая оплата и завершённая выплата — разные этапы.

### Автоматическая выдача
При создании проверяются заранее сохранённые реквизиты: диапазон суммы, свободный лимит и доступность мерчанта. Активная заявка резервирует сумму. Если реквизит не подошёл, заявка остаётся CREATED для ручного принятия.

### Срок оплаты
После выдачи реквизитов paymentExpiresAt задаётся на 30 минут вперёд. Неоплаченные заявки без квитанции отменяются обработчиком истечения срока. Показывайте клиенту срок из ответа, а не собственный новый таймер при каждом обновлении.

### Проверка и спор
После загрузки PDF начинается проверка поступления. При споре выплата приостановлена до решения администратора. Решение в пользу клиента ставит выплату в очередь; решение в пользу мерчанта отменяет заявку.

### Обновления через вебхуки
Подключите POST /webhook для получения обновлений. Для сверки используйте GET /orders/{id}. Если вебхуки не подключены, опрашивайте раз в 3–5 секунд с backoff при ошибках и останавливайтесь на COMPLETED или CANCELLED.

## Повторы запросов

Сеть может оборвать ответ после выполнения операции. requestId помогает безопасно повторить команду.

### Создавайте UUID до отправки
Для создания заявки и отмены requestId передаётся в JSON-теле. Сохраните его вместе с телом запроса до первого вызова. Отдельного заголовка Idempotency-Key нет.

### Повторяйте без изменений
Тот же requestId и тот же нормализованный запрос возвращают сохранённый результат. Изменённое тело с прежним requestId возвращает 409 IDEMPOTENCY_CONFLICT. Ключ относится к интеграции и конкретной операции.

### Сохранённый ответ — не свежий статус
Повтор создания может вернуть исходный статус, даже если заявка уже перешла дальше. Получайте текущее состояние отдельным GET /orders/{id}.

### У PDF другой механизм
При загрузке квитанции requestId не передаётся. Одинаковый PDF для той же заявки дедуплицируется по SHA-256. Использование этого же файла в другой заявке запрещено.

## Суммы и сеть

Сумма заявки — целое число рублей. Курс и выплата USDT сохраняют необходимую точность.

### Рубли · целое число
Передавайте fiatRub: 10000 для заявки на 10 000 ₽. Это JSON-число, без умножения на 100. Дробные рубли и строки не принимаются. В ответе используется то же поле fiatRub.

### USDT в API · 6 знаков
clientUnits — сумма выплаты USDT в масштабе 10⁶. «100000000» = 100 USDT. Значение передаётся строкой для точных вычислений.

### BSC · BEP-20
Рабочая сеть — BSC_USDT, chainId 56. Поле network при создании не передаётся. У токена в сети 18 знаков, но в Order единицы учёта имеют 6: не подменяйте clientUnits raw-суммой блокчейна.

### Курс фиксируется в заявке
GET /rate возвращает текущие условия вашей интеграции: rubPerUsdt — рублей за 1 USDT. Курс может изменяться в течение дня и действовать несколько дней. Индивидуальные условия имеют приоритет над общими. В заявке rate и clientUnits фиксируются при создании и больше не пересчитываются. Выплата округляется вниз до 6 знаков. Если передан rateId и курс успел измениться, заявка не создаётся: ответ 409 RATE_CHANGED. Без rateId применяется актуальный курс на момент создания.

## Вебхуки

PLATA отправляет обновления заявок на ваш сервер. В каждом событии — состояние заявки на момент изменения.

### Подключение
Создайте отдельный случайный секрет и отправьте POST /webhook с url, secret, enabled: true и requestId. Используйте публичный HTTPS-адрес на порту 443. Один адрес относится к одному API-ключу. GET /webhook возвращает настройки без секрета.

### Событие order.updated
Уведомления приходят при создании, назначении мерчанта, выдаче реквизитов, проверке оплаты, споре, постановке выплаты в очередь, завершении или отмене. Поле data содержит публичный Order. id — постоянный ID события, createdAt — время изменения, data.version — версия заявки. При автоматической выдаче возможны два события: CREATED и AWAITING_PAYMENT.

### Подпись и время
X-Plata-Signature имеет вид v1=<hex>. Вычислите HMAC-SHA256 от X-Plata-Timestamp + точка + X-Plata-Event-Id + точка + исходные байты тела, используя свой secret целиком как UTF-8 строку. Сравнивайте подписи за постоянное время. Проверяйте отклонение timestamp не более 300 секунд и совпадение ID заголовка с id в JSON. Сначала проверьте подпись, затем разбирайте JSON.

### Подтверждение и повторы
Сохраните событие и задачу обработки в одной транзакции, затем верните любой 2xx. Уникальный индекс по id исключает повторную обработку; на дубликат тоже отвечайте 2xx. На ошибку или таймаут PLATA повторяет запрос: задержка начинается с минуты, удваивается до часа. Не более 24 попыток за 24 часа с постановки в очередь. В каждой попытке свежая подпись и timestamp; ID и тело остаются прежними. Перенаправления не выполняются.

### Порядок и восстановление
Доставка может повторяться и приходить не по порядку. Обновляйте локальную заявку только при увеличении data.version. Дубликаты храните не меньше 30 дней. После длительного простоя сверяйте актуальные статусы через GET /orders/{id}. COMPLETED — единственный успешный финальный статус; PAYOUT_PENDING ещё не означает выплату.

### Смена настроек
Новый POST /webhook заменяет конфигурацию и отменяет её старую очередь. Ротация секрета выполняется этим же методом. После изменения сверяйте активные заявки через GET /orders/{id}. enabled: false останавливает доставку; отозванный или истёкший API-ключ также прекращает её. Запрос, который уже отправляется, может завершиться. События до подключения не воспроизводятся.

## Ошибки

Проверяйте HTTP-статус и поле error. При сетевом сбое результат команды может быть неизвестен.

### Структура ответа
Приложение возвращает { error, requestId }. Здесь requestId — ID трассировки HTTP-запроса, а не ключ идемпотентности из тела. Прокси или сетевой сбой могут вернуть не-JSON: обрабатывайте Content-Type и статус.

### Когда повторять
Для 429 и временных 5xx используйте увеличивающуюся задержку. Повторяйте команды с сохранённым requestId. При 409 сначала проверьте актуальную заявку. Валидационные 400 нужно исправить, а не повторять бесконечно.

## Order states
- CREATED: Поиск мерчанта. Заявка создана. Готовые реквизиты не найдены, ожидается принятие мерчантом.
- AWAITING_REQUISITES: Подготовка реквизитов. Мерчант принял заявку и готовит реквизиты.
- AWAITING_PAYMENT: Ожидание оплаты. Реквизиты доступны. Покажите их клиенту вместе с суммой и paymentExpiresAt.
- PAYMENT_REVIEW: Проверка оплаты. PDF принят. Мерчант проверяет фактическое поступление рублей.
- DISPUTED: Открыт спор. Выплата приостановлена. Разбирательство проходит с администратором в Telegram.
- PAYOUT_PENDING: Ожидание выплаты. Поступление подтверждено, выплата USDT поставлена в очередь. Это ещё не завершённая сделка.
- COMPLETED: Завершена. Выплата подтверждена. Финальный успешный статус.
- CANCELLED: Отменена. Заявка отменена. Финальный статус без выплаты.

## Order response fields
- id (UUID, required): ID заявки. Сохраните у себя: клиентского метода списка заявок сейчас нет.
- status (OrderStatus, required): Текущее состояние. Только COMPLETED означает успешное завершение выплаты.
- fiatRub (integer, required): Сумма в целых рублях. Например, 10000 — это 10 000 ₽.
- clientUnits (string, required): Сумма выплаты: 1 USDT = 1 000 000 units. «100000000» = 100 USDT.
- clientId / merchantId (UUID / UUID | null, required): Идентификатор интеграции и назначенного мерчанта. merchantId = null до назначения.
- requisites (object | null, required): account, bank, recipient. До выдачи — null. Банк и получатель могут быть пустыми строками.
- payoutAddress (string, required): Адрес выплаты, переданный при создании. Изменение адреса отдельным методом не поддерживается.
- rate (Rate, required): Курс, зафиксированный при создании: id, rubPerUsdt и updatedAt. Не меняется при обновлении условий.
- network (string, optional): В рабочем контуре — BSC_USDT. TEST_USDT предназначен только для внутреннего тестового контура.
- version (integer, required): Версия заявки увеличивается при изменении состояния.
- paymentExpiresAt (date-time | null, optional): Срок оплаты в ISO 8601. Назначается на 30 минут при выдаче реквизитов.
- savedRequisiteId (UUID | null, optional): ID реквизита при автоматической выдаче. Может отсутствовать в ответе создания.
- dispute (object | null, optional): Причина, комментарий, состояние и решение спора. Может отсутствовать в ответе создания.

## Endpoints
### GET /rate
Действующий курс RUB за 1 USDT для вашей интеграции.

Клиент определяется по API-ключу. Если согласованы индивидуальные условия, вернётся персональный курс, иначе — общий. Команда PLATA может менять курс в любое время; он действует до следующего изменения. Ответ не кэшируется. Передайте его id как rateId при создании, чтобы защититься от изменения курса между запросами.

Success: HTTP 200.


```sh
curl --request GET 'https://api.myplata.net/v1/rate' \
  --header 'Authorization: Bearer <PLATA_API_KEY>'
```

Response example:
```json
{
  "id": "a61a93f0-c4ec-4bad-9587-98b4749e9349",
  "pair": "USDT/RUB",
  "rubPerUsdt": "100.00",
  "updatedAt": "2026-11-03T09:00:00.000Z"
}
```

### POST /orders
Передайте сумму в рублях и адрес, на который клиент получит USDT.

Сначала платформа ищет готовые реквизиты с доступным лимитом. Если они найдены, ответ сразу содержит AWAITING_PAYMENT и реквизиты. Иначе вернётся CREATED: заявка ожидает мерчанта. Не отправляйте bank, currency, network или callbackUrl — этих полей в запросе нет.

Success: HTTP 201.
- fiatRub (integer · body, required): Сумма в рублях: 10000 = 10 000 ₽. Целое JSON-число от 1 до 9007199254740991; фактический лимит выплаты зависит от условий.
- payoutAddress (string · body, required): Ненулевой 0x-адрес из 40 hex-символов. Только BNB Smart Chain / BEP-20.
- requestId (UUID · body, required): UUID команды. Повторяйте тот же UUID и тело после таймаута. Для новой команды создайте новый UUID.
- rateId (UUID · body): Необязательно. Передайте id из GET /rate, чтобы создать заявку только по этому курсу. Если курс изменился, вернётся 409 RATE_CHANGED.

```sh
curl --request POST 'https://api.myplata.net/v1/orders' \
  --header 'Authorization: Bearer <PLATA_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "fiatRub": 10000,
  "payoutAddress": "0x1111111111111111111111111111111111111111",
  "requestId": "917c0421-6123-4e68-b6b3-1f8d92f0b470"
}'
```

Response example:
```json
{
  "network": "BSC_USDT",
  "id": "018f6fa2-7b18-4d5f-9d02-5c76a0e2b931",
  "clientId": "99517341-5c92-43b7-a44d-8c531e2ed090",
  "merchantId": "4bd22f46-88ea-4af8-8185-7298362e2839",
  "fiatRub": 10000,
  "clientUnits": "100000000",
  "rate": {
    "id": "a61a93f0-c4ec-4bad-9587-98b4749e9349",
    "pair": "USDT/RUB",
    "rubPerUsdt": "100.00",
    "updatedAt": "2026-11-03T09:00:00.000Z"
  },
  "payoutAddress": "0x1111111111111111111111111111111111111111",
  "status": "AWAITING_PAYMENT",
  "version": 2,
  "requisites": {
    "account": "DEMO-REQUISITES-NOT-FOR-PAYMENT",
    "bank": "Демонстрационный банк",
    "recipient": "Демо-получатель"
  },
  "savedRequisiteId": "f5d66215-0be9-452d-b27b-d97160586f88",
  "paymentExpiresAt": "2026-11-03T09:30:00.000Z",
  "dispute": null
}
```

### GET /orders/{id}
Актуальное состояние, реквизиты и сумма выплаты по вашей заявке.

Сохраните id из ответа создания и опрашивайте этот метод. Рекомендуемый интервал — 3–5 секунд с увеличением задержки при ошибках. Внешние webhook сейчас не реализованы. Завершайте опрос на COMPLETED или CANCELLED. transactionHash не входит в объект Order.

Success: HTTP 200.
- id (UUID · path, required): ID собственной заявки, полученный при создании.

```sh
curl --request GET 'https://api.myplata.net/v1/orders/018f6fa2-7b18-4d5f-9d02-5c76a0e2b931' \
  --header 'Authorization: Bearer <PLATA_API_KEY>'
```

Response example:
```json
{
  "network": "BSC_USDT",
  "id": "018f6fa2-7b18-4d5f-9d02-5c76a0e2b931",
  "clientId": "99517341-5c92-43b7-a44d-8c531e2ed090",
  "merchantId": "4bd22f46-88ea-4af8-8185-7298362e2839",
  "fiatRub": 10000,
  "clientUnits": "100000000",
  "rate": {
    "id": "a61a93f0-c4ec-4bad-9587-98b4749e9349",
    "pair": "USDT/RUB",
    "rubPerUsdt": "100.00",
    "updatedAt": "2026-11-03T09:00:00.000Z"
  },
  "payoutAddress": "0x1111111111111111111111111111111111111111",
  "status": "AWAITING_PAYMENT",
  "version": 2,
  "requisites": {
    "account": "DEMO-REQUISITES-NOT-FOR-PAYMENT",
    "bank": "Демонстрационный банк",
    "recipient": "Демо-получатель"
  },
  "savedRequisiteId": "f5d66215-0be9-452d-b27b-d97160586f88",
  "paymentExpiresAt": "2026-11-03T09:30:00.000Z",
  "dispute": null
}
```

### POST /orders/{id}/receipt
Отправьте исходный PDF непосредственно в теле запроса.

Content-Type: application/pdf. Не multipart, не JSON, не base64. До 8 MiB, от 1 до 20 страниц, без шифрования и активного содержимого. Допустимы AWAITING_PAYMENT, PAYMENT_REVIEW и DISPUTED. Один и тот же PDF для этой заявки не создаёт новый документ. До 5 уникальных PDF на заявку. Принятый PDF не подтверждает поступление денег и сам по себе не запускает выплату.

Success: HTTP 201.
- id (UUID · path, required): ID собственной заявки, полученный при создании.
- body (binary · PDF, required): Байты PDF. requestId не нужен: документ дедуплицируется по содержимому.

```sh
curl --request POST 'https://api.myplata.net/v1/orders/018f6fa2-7b18-4d5f-9d02-5c76a0e2b931/receipt' \
  --header 'Authorization: Bearer <PLATA_API_KEY>' \
  --header 'Content-Type: application/pdf' \
  --data-binary '@receipt.pdf'
```

Response example:
```json
{
  "documentId": "1731cafe-cb44-4b82-a690-3cb1c7131111",
  "status": "PAYMENT_REVIEW"
}
```

### GET /orders/{id}/receipt
Последняя принятая квитанция по заявке в формате PDF.

Ответ — бинарный PDF. По умолчанию Content-Disposition: inline. Добавьте ?download=1 для скачивания как вложения. Это авторизованный запрос: обычная ссылка без Bearer-заголовка не заменяет серверное получение файла.

Success: HTTP 200.
- id (UUID · path, required): ID собственной заявки, полученный при создании.
- download (string · query): Необязательно. Значение «1» включает attachment вместо inline.

```sh
curl --request GET 'https://api.myplata.net/v1/orders/018f6fa2-7b18-4d5f-9d02-5c76a0e2b931/receipt' \
  --header 'Authorization: Bearer <PLATA_API_KEY>' \
  --output receipt.pdf
```

Response example:
```text
%PDF-1.7
[бинарное содержимое квитанции]
%%EOF
```

### GET /orders/{id}/history
Последовательность изменений заявки, отсортированная по version.

Возвращается JSON-массив без обёртки items. Названия полей здесь используют snake_case. Технические action, request_id, event_id и trace_id могут быть null. Используйте статус заявки для принятия решений, а историю — для объяснения изменений.

Success: HTTP 200.
- id (UUID · path, required): ID собственной заявки, полученный при создании.

```sh
curl --request GET 'https://api.myplata.net/v1/orders/018f6fa2-7b18-4d5f-9d02-5c76a0e2b931/history' \
  --header 'Authorization: Bearer <PLATA_API_KEY>'
```

Response example:
```json
[
  {
    "version": 1,
    "from_status": null,
    "to_status": "CREATED",
    "occurred_at": "2026-11-03T09:00:00.000Z",
    "action": "create-order",
    "request_id": "917c0421-6123-4e68-b6b3-1f8d92f0b470",
    "event_id": null,
    "trace_id": null
  },
  {
    "version": 2,
    "from_status": "CREATED",
    "to_status": "AWAITING_PAYMENT",
    "occurred_at": "2026-11-03T09:00:00.000Z",
    "action": "create-order",
    "request_id": "917c0421-6123-4e68-b6b3-1f8d92f0b470",
    "event_id": null,
    "trace_id": null
  }
]
```

### POST /orders/{id}/cancel
Отмена неоплаченной заявки до прикрепления квитанции.

Отмена доступна только в AWAITING_PAYMENT, если квитанция ещё не принята. CREATED, AWAITING_REQUISITES, PAYMENT_REVIEW, DISPUTED и PAYOUT_PENDING этим методом не отменяются. При успехе резерв освобождается. Для повторной отправки используйте исходный requestId.

Success: HTTP 201.
- id (UUID · path, required): ID собственной заявки, полученный при создании.
- requestId (UUID · body, required): UUID команды. Повторяйте тот же UUID и тело после таймаута. Для новой команды создайте новый UUID.

```sh
curl --request POST 'https://api.myplata.net/v1/orders/018f6fa2-7b18-4d5f-9d02-5c76a0e2b931/cancel' \
  --header 'Authorization: Bearer <PLATA_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "requestId": "876867cb-555c-4a38-a930-ecc0c5413168"
}'
```

Response example:
```json
{
  "network": "BSC_USDT",
  "id": "018f6fa2-7b18-4d5f-9d02-5c76a0e2b931",
  "clientId": "99517341-5c92-43b7-a44d-8c531e2ed090",
  "merchantId": "4bd22f46-88ea-4af8-8185-7298362e2839",
  "fiatRub": 10000,
  "clientUnits": "100000000",
  "rate": {
    "id": "a61a93f0-c4ec-4bad-9587-98b4749e9349",
    "pair": "USDT/RUB",
    "rubPerUsdt": "100.00",
    "updatedAt": "2026-11-03T09:00:00.000Z"
  },
  "payoutAddress": "0x1111111111111111111111111111111111111111",
  "status": "CANCELLED",
  "version": 2,
  "requisites": {
    "account": "DEMO-REQUISITES-NOT-FOR-PAYMENT",
    "bank": "Демонстрационный банк",
    "recipient": "Демо-получатель"
  },
  "savedRequisiteId": "f5d66215-0be9-452d-b27b-d97160586f88",
  "paymentExpiresAt": "2026-11-03T09:30:00.000Z",
  "dispute": null
}
```

### GET /webhook
Адрес уведомлений и состояние подключения вашей интеграции.

Если вебхук ещё не настроен, ответ — null. Секрет подписи не возвращается. Настройки принадлежат API-ключу, с которым выполнен запрос.

Success: HTTP 200.


```sh
curl --request GET 'https://api.myplata.net/v1/webhook' \
  --header 'Authorization: Bearer <PLATA_API_KEY>'
```

Response example:
```json
{
  "id": "6c4bbc53-3228-4c2e-9f4f-4657af693f62",
  "url": "https://receiver.example.com/plata",
  "enabled": true,
  "updatedAt": "2026-11-03T09:00:00.000Z"
}
```

### POST /webhook
Подключите HTTPS-адрес для получения обновлений заявок.

Один адрес на интеграцию. Передайте все четыре поля. Повтор с тем же requestId и телом безопасен. Изменение настроек отменяет очередь предыдущей конфигурации: новые события будут доставляться по новым условиям. Уже отправляемый запрос может завершиться. Исторические события не отправляются; сверяйте состояние через GET /orders/{id}.

Success: HTTP 201.
- requestId (UUID · body, required): UUID команды. Повторяйте тот же UUID и тело после таймаута. Для новой команды создайте новый UUID.
- url (string · body, required): Публичный HTTPS-адрес, порт 443. Без логина, пароля и фрагмента. Локальные адреса и перенаправления не поддерживаются.
- secret (string · body, required): Отдельный секрет подписи: whsec_ + 32 случайных байта в base64url без padding (43 символа). Сохраните у себя; API его не возвращает.
- enabled (boolean · body, required): true — получать новые события, false — отключить доставку и отменить очередь.

```sh
curl --request POST 'https://api.myplata.net/v1/webhook' \
  --header 'Authorization: Bearer <PLATA_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "requestId": "917c0421-6123-4e68-b6b3-1f8d92f0b470",
  "url": "https://receiver.example.com/plata",
  "secret": "whsec_ddddddddddddddddddddddddddddddddddddddddddd",
  "enabled": true
}'
```

Response example:
```json
{
  "id": "6c4bbc53-3228-4c2e-9f4f-4657af693f62",
  "url": "https://receiver.example.com/plata",
  "enabled": true,
  "updatedAt": "2026-11-03T09:00:00.000Z"
}
```

## Webhook notification example

```json
{
  "id": "d6cfd7a4-824f-4b28-8df0-af5771a9884c",
  "type": "order.updated",
  "apiVersion": "v1",
  "createdAt": "2026-11-03T09:05:00.000Z",
  "data": {
    "network": "BSC_USDT",
    "id": "018f6fa2-7b18-4d5f-9d02-5c76a0e2b931",
    "clientId": "99517341-5c92-43b7-a44d-8c531e2ed090",
    "merchantId": "4bd22f46-88ea-4af8-8185-7298362e2839",
    "fiatRub": 10000,
    "clientUnits": "100000000",
    "rate": {
      "id": "a61a93f0-c4ec-4bad-9587-98b4749e9349",
      "pair": "USDT/RUB",
      "rubPerUsdt": "100.00",
      "updatedAt": "2026-11-03T09:00:00.000Z"
    },
    "payoutAddress": "0x1111111111111111111111111111111111111111",
    "status": "COMPLETED",
    "version": 5,
    "requisites": {
      "account": "DEMO-REQUISITES-NOT-FOR-PAYMENT",
      "bank": "Демонстрационный банк",
      "recipient": "Демо-получатель"
    },
    "savedRequisiteId": "f5d66215-0be9-452d-b27b-d97160586f88",
    "paymentExpiresAt": "2026-11-03T09:30:00.000Z",
    "dispute": null
  }
}
```

## Verify a webhook (Node.js / TypeScript)

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

export function verifyPlataWebhook(rawBody: Buffer, headers: Record<string, string | undefined>, secret: string, now = Math.floor(Date.now() / 1000)) {
  const id = headers['x-plata-event-id'] ?? '';
  const timestamp = headers['x-plata-timestamp'] ?? '';
  const signature = headers['x-plata-signature'] ?? '';
  if (!/^[0-9a-f-]{36}$/i.test(id) || !/^[0-9]{1,12}$/.test(timestamp) ||
      Math.abs(now - Number(timestamp)) > 300 || !/^v1=[a-f0-9]{64}$/.test(signature)) {
    throw new Error('Invalid webhook headers');
  }
  const expected = createHmac('sha256', secret)
    .update(timestamp + '.' + id + '.').update(rawBody).digest();
  const received = Buffer.from(signature.slice(3), 'hex');
  if (received.length !== expected.length || !timingSafeEqual(received, expected)) {
    throw new Error('Invalid webhook signature');
  }
  const event = JSON.parse(rawBody.toString('utf8'));
  if (event.id !== id || event.type !== 'order.updated' || event.apiVersion !== 'v1') {
    throw new Error('Unsupported webhook event');
  }
  return event;
}

// Use express.raw({ type: 'application/json', limit: '64kb' })
// on the webhook route BEFORE express.json().
// Pass req.body unchanged; secret = process.env.PLATA_WEBHOOK_SECRET.
// In one DB transaction: insert event.id into an inbox with UNIQUE(id),
// and enqueue processing. Return 2xx only after that transaction commits.
// A duplicate event.id also receives 2xx. Apply data only when its version
// is greater than the stored version for data.id. Keep IDs for at least 30 days.
// Synchronize the receiver clock. After downtime reconcile via GET /orders/{id}.
```

## Errors
- 400 WEBHOOK_DESTINATION_FORBIDDEN / WEBHOOK_URL_INVALID: Нужен публичный HTTPS-адрес на порту 443, без логина, пароля и фрагмента.
- 503 WEBHOOK_DNS_UNAVAILABLE: Не удалось проверить DNS получателя. Проверьте домен и повторите позже.
- 503 WEBHOOK_UNAVAILABLE_CHECK_REQUEST_STATUS: Сервис настройки временно недоступен. Повторите исходный запрос с тем же requestId.
- 400 INVALID_INPUT: Проверьте типы, UUID, обязательные и лишние поля. fiatRub — целое JSON-число в рублях.
- 400 PAYOUT_NETWORK_MISMATCH: Нужен ненулевой адрес BSC вида 0x + 40 hex-символов.
- 400 PAYOUT_AMOUNT_LIMIT: Превышен настроенный максимальный размер выплаты.
- 400 PDF_REQUIRED / INVALID_PDF / PDF_UNREADABLE_OR_ACTIVE: Отправьте допустимый PDF в бинарном теле с Content-Type: application/pdf.
- 401 API_KEY_INVALID: Ключ неверен, истёк или отозван. Получите действующий ключ у администратора.
- 403 FORBIDDEN: Интеграция не имеет доступа к этой заявке или действию.
- 404 ORDER_NOT_FOUND: Проверьте сохранённый id заявки.
- 409 IDEMPOTENCY_CONFLICT: requestId уже использован с другим телом. Не меняйте данные при повторе команды.
- 409 RATE_CHANGED: Переданный rateId больше не актуален. Получите GET /rate и подтвердите новые условия перед созданием заявки.
- 409 INVALID_ORDER_STATE: Действие недоступно в текущем статусе. Сначала перечитайте заявку.
- 409 RECEIPT_ALREADY_USED / RECEIPT_LIMIT: Этот PDF использован в другой заявке, либо достигнут лимит 5 уникальных PDF.
- 429 API_RATE_LIMIT: Не более 120 запросов в минуту на ключ. Уменьшите частоту и повторите позже.
- 503 TRADING_NOT_ENABLED: Приём заявок временно выключен.
- 503 PROCESSING_UNAVAILABLE_CHECK_REQUEST_STATUS: Результат команды может быть неизвестен. Сохраните requestId; не создавайте дубликат.
- 503 DOCUMENT_UNAVAILABLE: Документ отсутствует или сервис документов временно недоступен.

## Agent implementation checklist
- Использовать 9 описанных операций, включая GET /rate и GET/POST /webhook. Курс определяется API-ключом; clientId в запросе курса не передаётся.
- Сохранять id заявки, requestId команды и исходное тело. Не создавать новый UUID при сетевом повторе.
- Передавать fiatRub целым JSON-числом в рублях. USDT в Order: строка units × 10⁶. rubPerUsdt: десятичная строка рублей за 1 USDT. rateId позволяет проверить курс при создании.
- Отправлять PDF бинарным телом, а не multipart/base64. Не приравнивать квитанцию к подтверждению оплаты.
- Обрабатывать все статусы. COMPLETED и CANCELLED — финальные; PAYOUT_PENDING и DISPUTED — нет.
- Хранить API-ключ только на сервере. Примеры в документации не являются рабочими ключами или платёжными реквизитами.

## Integration boundaries
- All nine integration operations use Authorization: Bearer <PLATA_API_KEY>. The actual key is client-scoped and begins plata_live_.
- API rate limit: 120 authenticated requests per minute per integration key; Retry-After is not guaranteed.
- GET /orders/mine and /orders/available are merchant-only and are not client listing endpoints. Persist your own order IDs.
- Request bodies are strict: unknown JSON fields are rejected. No currency, network, callbackUrl or externalId field in create-order.
- POST methods described here return 201, not 200. Reads return 200. History is a bare array with snake_case fields.
- GET Order has no transactionHash or transactionId field. Do not invent a receipt URL in Order.
- Cancellation is only allowed in AWAITING_PAYMENT before any accepted receipt. CREATED is not cancellable by this method.
- The API does not let a client confirm RUB receipt, open a merchant dispute or resolve it. Those are role-protected merchant/admin operations.
- Configure GET/POST /webhook. Outbound order.updated notifications use HMAC, not the API Bearer key. Verify raw bytes, timestamp and event ID; deduplicate by event.id and apply only increasing data.version. Use GET /orders/{id} to reconcile after downtime. Polling every 3–5 seconds with backoff remains a fallback.
- POST /orders can return CREATED or AWAITING_PAYMENT. Replayed POST responses are snapshots, so read the order for a fresh state.
- Upload permits AWAITING_PAYMENT, PAYMENT_REVIEW, DISPUTED. An open dispute stays DISPUTED after another receipt. A synchronous attach happens before acknowledgement, but concurrent processing may advance the returned status.
- A PDF is limited to 8 MiB / 20 pages, no encryption or active content, at most 5 distinct PDFs per order. The same file is deduplicated for the same order; cross-order reuse is rejected.
- Orders use 6-decimal USDT accounting even though BSC USDT uses 18 on-chain decimals. Do not use floats for arithmetic.
- Rates are updated by PLATA at any time, without daily expiry. Individual client rates override the default. GET /rate identifies the client from the Bearer key. Pass optional rateId to POST /orders to reject a changed rate with 409 RATE_CHANGED; omission uses the current rate. Existing orders retain their original rate and payout. Payout amount limits are operator-configured. Demonstration rates and the 10,000 USDT cap are not guaranteed production values.

## Suggested verification
Test creation with and without auto-assigned requisites; duplicate and conflicting requestId; polling to terminal states; raw PDF upload and deduplication; cancellation before and after a receipt; auth failures; 429/backoff; non-JSON gateway failures; integer precision. Never run these mutations against production just to validate documentation.
