API
Обзор
API принимает заявки на перевод и отдаёт их текущее состояние. Обработка асинхронная: заявка регистрируется сразу, а финальный исход (подтверждение или отказ) становится известен позже — его нужно запрашивать через статус операции.
- Все запросы к
/transfers/**подписываются HMAC-SHA256 секретом терминала — см. Аутентификация. - Актуальный курс конверсии для терминала —
GET /api/v1/rates/{terminalId}(без подписи), см. Получение курса. - Идемпотентность приёма — по полю
externalId, см. создание заявки. - Каждая операция привязана к терминалу (канал интеграции); терминал видит только свои операции.
- Статусы операций
PENDING / COMPLETED / REJECTED— см. Статусы операции.
Соглашения
| Тема | Правило |
|---|---|
| Дата обновления | 2026-07-17 · v1.1.0 |
| Base URL | https://api-test.coinpay.by/api/v1 |
| Формат | application/json, UTF-8 |
| Кодировка | UTF-8, тело и ответы — JSON. |
| Время | Бизнес-поля (executionDeadline, createdAt, updatedAt) — UTC, ISO-8601 без таймзоны: 2026-06-23T00:30:00. Заголовок X-Timestamp — UTC ISO-8601 с Z. |
| Суммы | Строки с фиксированной точностью 2 знака: "10000.00". Передаются строкой во избежание потери точности. |
| Курс | rate — строка, до 12 знаков после запятой. Сколько единиц target за 1 единицу source. |
| Валюты | ISO-4217, 3 буквы: RUB, BYN. |
| Идентификаторы | UUID v4. |
| Персональные данные | ФИО, имя держателя — латиницей. |
Аутентификация
Каждый запрос к /api/v1/transfers/** подписывается по схеме HMAC-SHA256 симметричным секретом
терминала. При подключении банк получает пару: terminalId (UUID) и secret (сырой ключ). Секрет на
стороне сервиса хранится зашифрованным; в открытом виде существует только у интегратора.
Канонизация запроса
Строка для подписи собирается из четырёх частей, разделённых символом \n (LF):
METHOD // POST или GET
PATH // /api/v1/transfers (без host и query)
X-Timestamp // тот же литерал, что уйдёт в заголовке
hex(SHA-256(body))// для GET тело пустое → хэш пустой строкиШаги формирования подписи
- Взять текущее время UTC, ISO-8601 c
Z— это значение пойдёт и вX-Timestamp, и в канон-строку (один и тот же литерал). - Сериализовать тело ровно в те байты, что уйдут в сеть; после подсчёта хэша тело не переформатировать (пробелы, порядок ключей, экранирование должны остаться неизменными).
- Посчитать
bodyHash = hex(SHA-256(rawBody)), lowercase. - Собрать канон-строку (см. выше).
signature = hex(HMAC-SHA256(secret, canonical)), lowercase.- Отправить запрос с заголовками
X-Terminal-Id,X-Timestamp,X-Signature.
Пример запроса
Подпись собирается через openssl, затем запрос уходит через curl:
SECRET="<secret>"; TS="2026-06-22T19:02:12Z"
BODY='{"externalId":"...","terminalId":"...", ... }'
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $2}')
CANON=$(printf 'POST\n/api/v1/transfers\n%s\n%s' "$TS" "$BODY_HASH")
SIG=$(printf '%s' "$CANON" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
curl -X POST https://<host>/api/v1/transfers \
-H 'Content-Type: application/json' \
-H "X-Terminal-Id: <terminalId>" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG" \
--data "$BODY"Заголовки
| Заголовок | Описание | |
|---|---|---|
X-Terminal-Id | required | UUID терминала-источника. Единственный источник правды о терминале; в теле POST поле terminalId должно совпадать с ним. |
X-Timestamp | required | Время запроса, UTC ISO-8601 с Z. Допустимое отклонение часов — ±5 минут, иначе 401. |
X-Signature | required | hex(HMAC-SHA256(secret, canonical)), lowercase. |
Content-Type | required* | application/json для запросов с телом (POST). |
X-Trace-Id | optional | Сквозной идентификатор трассировки. Если не задан — генерируется сервером и возвращается в ответе. Присутствует в поле traceId тела ошибки. |
Получение курса
Возвращает актуальный курс конверсии, персональный для терминала. Вызывайте его, чтобы получить
значение поля rate перед созданием заявки: курс индивидуален для каждого партнёра
и может меняться во времени. Курсовой профиль сопоставляется терминалу на нашей стороне — партнёру
достаточно передать свой terminalId.
Подпись не требуется. Заголовки X-Terminal-Id / X-Timestamp / X-Signature для этого запроса
не нужны — достаточно указать terminalId в пути.
Path-параметры
В пути /rates/{terminalId} подставляется {terminalId}:
| Параметр | Тип | Описание | |
|---|---|---|---|
terminalId | UUID | required | Идентификатор вашего терминала. |
Пример запроса
curl https://api-test.coinpay.by/api/v1/rates/3b9aa1d4-7c2e-4d1a-9f3b-5e8c1a2d6f80Ответ
{
"currencyOne": "RUB",
"currencyTwo": "BYN",
"amountOne": "1",
"amountTwo": "0.036352"
}Читается как amountOne currencyOne = amountTwo currencyTwo: в примере 1 RUB = 0.036352 BYN.
Значение amountTwo — это актуальный курс, который подставляется в поле rate запроса на
создание заявки.
| Поле | Тип | Описание |
|---|---|---|
currencyOne | string(3) | Базовая валюта (за amountOne единиц которой указан курс). |
currencyTwo | string(3) | Котируемая валюта. |
amountOne | decimal | Базовое количество, всегда "1". |
amountTwo | decimal | Сколько currencyTwo приходится на amountOne currencyOne — актуальный курс. |
На текущем этапе курс отдаётся по паре RUB → BYN; query-параметры не поддерживаются.
Коды ответа
| HTTP | code | Когда |
|---|---|---|
| 200 | код не возвращается | Актуальный курс терминала. |
| 404 | NOT_FOUND | Для терминала не настроен курсовой профиль. |
| 422 | TERMINAL_NOT_FOUND | Терминал с указанным terminalId не найден. |
| 500 | INTERNAL_ERROR | Курсовой сервис недоступен или внутренняя ошибка. |
Создание заявки
Создаёт операцию: в ответе PENDING (далее асинхронная обработка) либо сразу REJECTED с
reasonCode = TERMINAL_INACTIVE, если терминал отключён. Идемпотентно по externalId. Поле
reasonCode присутствует только при status = REJECTED; при PENDING опускается.
Тело запроса
| Поле | Тип | Описание | |
|---|---|---|---|
externalId | string | required | Идемпотентный ключ интегратора; уникален в рамках сервиса. |
terminalId | UUID | required | Терминал-источник. Должен совпадать с X-Terminal-Id. |
source.currency | string(3) | required | Валюта списания, ISO-4217. |
source.amount | decimal | required | Сумма списания, > 0, 2 знака. |
target.currency | string(3) | required | Валюта зачисления, ISO-4217. |
target.amount | decimal | required | Сумма зачисления, > 0, 2 знака. |
rate | decimal | required | Курс конвертации, > 0, до 12 знаков. |
executionDeadline | datetime | required | Момент (UTC), до которого операция может быть исполнена. Не в прошлом — иначе асинхронный отказ REJECTED / VALIDATION_FAILED. |
description | string | optional | Произвольный комментарий, бизнес-логикой не интерпретируется. |
sender | object | required | Реквизиты отправителя — см. модель. |
recipient | object | required | Реквизиты получателя — см. модель. |
Пример запроса
curl -X POST https://api-test.coinpay.by/api/v1/transfers \
-H 'Content-Type: application/json' \
-H "X-Terminal-Id: <terminalId>" \
-H "X-Timestamp: <timestamp>" \
-H "X-Signature: <signature>" \
--data '{
"externalId": "f42ac22b-58cc-4335-a237-0e03b3c3d473",
"terminalId": "3b9aa1d4-7c2e-4d1a-9f3b-5e8c1a2d6f80",
"source": { "currency": "RUB", "amount": "10000.00" },
"target": { "currency": "BYN", "amount": "360.19" },
"rate": "0.036352000000",
"executionDeadline": "2026-06-23T00:30:00",
"description": "Monthly support",
"sender": {
"firstName": "John", "lastName": "Doe", "middleName": "Nikolaevich",
"birthDate": "1987-05-22",
"fullAddress": "ul. Sadovaya 17, kv. 42, Voronezh, 394000, RU",
"country": "RU", "state": "Voronezh Oblast", "city": "Voronezh",
"street": "ul. Sadovaya 17, kv. 42", "zip": "394000"
},
"recipient": { "card": "9112874453219031", "holder": "Anastasia Kovaleva" }
}'Ответ
Тело ответа одинаково для 201 и 200:
{
"id": "9b1f8e22-0e0d-4b94-9a3d-3a2e5f1c7e10",
"externalId": "f42ac22b-58cc-4335-a237-0e03b3c3d473",
"status": "PENDING",
"executionDeadline": "2026-06-23T00:30:00",
"createdAt": "2026-06-22T19:02:12",
"updatedAt": "2026-06-22T19:02:12"
}Коды ответа
| HTTP | code | Когда |
|---|---|---|
| 201 | кода ошибки нет | Заявка создана (PENDING либо REJECTED / TERMINAL_INACTIVE). |
| 200 | кода ошибки нет | Идемпотентный повтор по externalId. |
| 400 | VALIDATION_ERROR | Тело не прошло валидацию (пустые/некорректные поля). |
| 401 | UNAUTHORIZED | Подпись не прошла проверку или terminalId тела ≠ X-Terminal-Id. |
| 409 | EXTERNAL_ID_CONFLICT | externalId уже использован другим терминалом. |
| 422 | TERMINAL_NOT_FOUND | Терминал с указанным terminalId не найден. |
| 500 | INTERNAL_ERROR | Внутренняя ошибка сервера. |
Статус операции
Возвращает текущее состояние операции. Действует изоляция по терминалу: операция видна только
терминалу-владельцу. Запрос статуса чужой операции отвечает 404 (существование чужой операции не
подтверждается). Поле reasonCode присутствует только при status = REJECTED (см.
каталог); description — человекочитаемая детализация (например, текст отказа
от PSP), может быть null.
Path-параметры
В пути /transfers/{id}/status подставляется {id}:
| Параметр | Тип | Описание | |
|---|---|---|---|
id | UUID | required | Идентификатор операции из ответа при создании заявки. |
Пример запроса
curl https://api-test.coinpay.by/api/v1/transfers/9b1f8e22-0e0d-4b94-9a3d-3a2e5f1c7e10/status \
-H "X-Terminal-Id: <terminalId>" \
-H "X-Timestamp: <timestamp>" \
-H "X-Signature: <signature>"Ответ
{
"id": "9b1f8e22-0e0d-4b94-9a3d-3a2e5f1c7e10",
"status": "REJECTED",
"reasonCode": "PROVIDER_DECLINED",
"description": "Card declined by issuer (51)",
"executionDeadline": "2026-06-23T00:30:00",
"updatedAt": "2026-06-22T19:05:00"
}Коды ответа
| HTTP | code | Когда |
|---|---|---|
| 200 | код не возвращается | Текущее состояние операции. |
| 401 | UNAUTHORIZED | Подпись не прошла проверку. |
| 404 | TRANSFER_NOT_FOUND | Операция не найдена либо принадлежит другому терминалу. |
| 500 | INTERNAL_ERROR | Внутренняя ошибка сервера. |
Модель данных
Отправитель
Объект sender в теле запроса.
| Поле | Тип | Описание | |
|---|---|---|---|
firstName | string | required | Имя (латиница). |
lastName | string | required | Фамилия. |
middleName | string | required | Отчество. |
birthDate | date | required | Дата рождения, YYYY-MM-DD. |
fullAddress | string | required | Полный адрес одной строкой. |
country | string | required | Страна, ISO-3166 alpha-2. |
state | string | required | Регион / область. |
city | string | required | Город. |
street | string | required | Улица, дом, квартира. |
zip | string | required | Почтовый индекс. |
Получатель
Объект recipient в теле запроса.
| Поле | Тип | Описание | |
|---|---|---|---|
card | string | required | Номер карты получателя. |
holder | string | required | Имя держателя карты (латиница). |
Денежная величина
Объекты source (списание) и target (зачисление) в теле запроса.
| Поле | Тип | Описание | |
|---|---|---|---|
currency | string(3) | required | Код валюты ISO-4217. |
amount | decimal | required | Сумма строкой, > 0, 2 знака после запятой. |
Статусы операции
Наружу отдаётся публичная проекция состояния — внутренние этапы обработки не раскрываются. Поле
status в ответах на создание заявки и статус операции принимает одно из трёх значений:
| status | Категория | Значение |
|---|---|---|
PENDING | в работе | Заявка принята и обрабатывается. Финальный исход — через запрос статуса. |
COMPLETED | успех | Перевод подтверждён платёжной системой. |
REJECTED | отказ | Операция отклонена. Причина — в поле reasonCode (см. ниже), детализация — в description. |
Причина отказа
Поле reasonCode. Присутствует только при status = REJECTED; для PENDING / COMPLETED опускается.
| reasonCode | Значение |
|---|---|
VALIDATION_FAILED | Не прошла бизнес-валидация запроса (в т.ч. executionDeadline в прошлом, security-проверка). Детали — в description. |
PROCESSING_ERROR | Отказ на стороне внутренней проводки. |
ISSUER_DECLINED | Отказ банка-эмитента карты. |
PROVIDER_DECLINED | Отказ платёжного провайдера. |
LIMIT_EXCEEDED | Превышены лимиты. |
TERMINAL_INACTIVE | Терминал-источник деактивирован — операция отклонена без обработки. |
Ошибки
Все ошибки возвращаются единым форматом. Поле traceId совпадает с заголовком X-Trace-Id ответа —
указывайте его при обращении в поддержку для быстрой локализации запроса в логах.
{
"code": "UNAUTHORIZED",
"message": "Terminal authentication failed",
"traceId": "b2c1...e7f0"
}| code | HTTP | Описание |
|---|---|---|
VALIDATION_ERROR | 400 | Некорректное тело запроса. message перечисляет проблемные поля. |
UNAUTHORIZED | 401 | Подпись/заголовки не прошли проверку, протух X-Timestamp, неизвестный терминал или рассинхрон terminalId. |
TRANSFER_NOT_FOUND | 404 | Операция не найдена либо принадлежит другому терминалу. |
NOT_FOUND | 404 | Ресурс не найден. В частности, для терминала не настроен курсовой профиль (GET /rates). |
EXTERNAL_ID_CONFLICT | 409 | externalId уже связан с другим терминалом. |
TERMINAL_NOT_FOUND | 422 | Терминал с указанным terminalId не существует. |
INTERNAL_ERROR | 500 | Непредвиденная ошибка на стороне сервиса. |