CoinPay
Для разработчиков

API

Обзор

API принимает заявки на перевод и отдаёт их текущее состояние. Обработка асинхронная: заявка регистрируется сразу, а финальный исход (подтверждение или отказ) становится известен позже — его нужно запрашивать через статус операции.

  • Все запросы к /transfers/** подписываются HMAC-SHA256 секретом терминала — см. Аутентификация.
  • Актуальный курс конверсии для терминала — GET /api/v1/rates/{terminalId} (без подписи), см. Получение курса.
  • Идемпотентность приёма — по полю externalId, см. создание заявки.
  • Каждая операция привязана к терминалу (канал интеграции); терминал видит только свои операции.
  • Статусы операций PENDING / COMPLETED / REJECTED — см. Статусы операции.

Соглашения

ТемаПравило
Дата обновления2026-07-17 · v1.1.0
Base URLhttps://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 тело пустое → хэш пустой строки

Шаги формирования подписи

  1. Взять текущее время UTC, ISO-8601 c Z — это значение пойдёт и в X-Timestamp, и в канон-строку (один и тот же литерал).
  2. Сериализовать тело ровно в те байты, что уйдут в сеть; после подсчёта хэша тело не переформатировать (пробелы, порядок ключей, экранирование должны остаться неизменными).
  3. Посчитать bodyHash = hex(SHA-256(rawBody)), lowercase.
  4. Собрать канон-строку (см. выше).
  5. signature = hex(HMAC-SHA256(secret, canonical)), lowercase.
  6. Отправить запрос с заголовками 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-IdrequiredUUID терминала-источника. Единственный источник правды о терминале; в теле POST поле terminalId должно совпадать с ним.
X-TimestamprequiredВремя запроса, UTC ISO-8601 с Z. Допустимое отклонение часов — ±5 минут, иначе 401.
X-Signaturerequiredhex(HMAC-SHA256(secret, canonical)), lowercase.
Content-Typerequired*application/json для запросов с телом (POST).
X-Trace-IdoptionalСквозной идентификатор трассировки. Если не задан — генерируется сервером и возвращается в ответе. Присутствует в поле traceId тела ошибки.

Получение курса

Возвращает актуальный курс конверсии, персональный для терминала. Вызывайте его, чтобы получить значение поля rate перед созданием заявки: курс индивидуален для каждого партнёра и может меняться во времени. Курсовой профиль сопоставляется терминалу на нашей стороне — партнёру достаточно передать свой terminalId.

Подпись не требуется. Заголовки X-Terminal-Id / X-Timestamp / X-Signature для этого запроса не нужны — достаточно указать terminalId в пути.

Path-параметры

В пути /rates/{terminalId} подставляется {terminalId}:

ПараметрТипОписание
terminalIdUUIDrequiredИдентификатор вашего терминала.

Пример запроса

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 запроса на создание заявки.

ПолеТипОписание
currencyOnestring(3)Базовая валюта (за amountOne единиц которой указан курс).
currencyTwostring(3)Котируемая валюта.
amountOnedecimalБазовое количество, всегда "1".
amountTwodecimalСколько currencyTwo приходится на amountOne currencyOne — актуальный курс.

На текущем этапе курс отдаётся по паре RUB → BYN; query-параметры не поддерживаются.

Коды ответа

HTTPcodeКогда
200код не возвращаетсяАктуальный курс терминала.
404NOT_FOUNDДля терминала не настроен курсовой профиль.
422TERMINAL_NOT_FOUNDТерминал с указанным terminalId не найден.
500INTERNAL_ERRORКурсовой сервис недоступен или внутренняя ошибка.

Создание заявки

Создаёт операцию: в ответе PENDING (далее асинхронная обработка) либо сразу REJECTED с reasonCode = TERMINAL_INACTIVE, если терминал отключён. Идемпотентно по externalId. Поле reasonCode присутствует только при status = REJECTED; при PENDING опускается.

Тело запроса

ПолеТипОписание
externalIdstringrequiredИдемпотентный ключ интегратора; уникален в рамках сервиса.
terminalIdUUIDrequiredТерминал-источник. Должен совпадать с X-Terminal-Id.
source.currencystring(3)requiredВалюта списания, ISO-4217.
source.amountdecimalrequiredСумма списания, > 0, 2 знака.
target.currencystring(3)requiredВалюта зачисления, ISO-4217.
target.amountdecimalrequiredСумма зачисления, > 0, 2 знака.
ratedecimalrequiredКурс конвертации, > 0, до 12 знаков.
executionDeadlinedatetimerequiredМомент (UTC), до которого операция может быть исполнена. Не в прошлом — иначе асинхронный отказ REJECTED / VALIDATION_FAILED.
descriptionstringoptionalПроизвольный комментарий, бизнес-логикой не интерпретируется.
senderobjectrequiredРеквизиты отправителя — см. модель.
recipientobjectrequiredРеквизиты получателя — см. модель.

Пример запроса

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"
}

Коды ответа

HTTPcodeКогда
201кода ошибки нетЗаявка создана (PENDING либо REJECTED / TERMINAL_INACTIVE).
200кода ошибки нетИдемпотентный повтор по externalId.
400VALIDATION_ERRORТело не прошло валидацию (пустые/некорректные поля).
401UNAUTHORIZEDПодпись не прошла проверку или terminalId тела ≠ X-Terminal-Id.
409EXTERNAL_ID_CONFLICTexternalId уже использован другим терминалом.
422TERMINAL_NOT_FOUNDТерминал с указанным terminalId не найден.
500INTERNAL_ERRORВнутренняя ошибка сервера.

Статус операции

Возвращает текущее состояние операции. Действует изоляция по терминалу: операция видна только терминалу-владельцу. Запрос статуса чужой операции отвечает 404 (существование чужой операции не подтверждается). Поле reasonCode присутствует только при status = REJECTED (см. каталог); description — человекочитаемая детализация (например, текст отказа от PSP), может быть null.

Path-параметры

В пути /transfers/{id}/status подставляется {id}:

ПараметрТипОписание
idUUIDrequiredИдентификатор операции из ответа при создании заявки.

Пример запроса

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"
}

Коды ответа

HTTPcodeКогда
200код не возвращаетсяТекущее состояние операции.
401UNAUTHORIZEDПодпись не прошла проверку.
404TRANSFER_NOT_FOUNDОперация не найдена либо принадлежит другому терминалу.
500INTERNAL_ERRORВнутренняя ошибка сервера.

Модель данных

Отправитель

Объект sender в теле запроса.

ПолеТипОписание
firstNamestringrequiredИмя (латиница).
lastNamestringrequiredФамилия.
middleNamestringrequiredОтчество.
birthDatedaterequiredДата рождения, YYYY-MM-DD.
fullAddressstringrequiredПолный адрес одной строкой.
countrystringrequiredСтрана, ISO-3166 alpha-2.
statestringrequiredРегион / область.
citystringrequiredГород.
streetstringrequiredУлица, дом, квартира.
zipstringrequiredПочтовый индекс.

Получатель

Объект recipient в теле запроса.

ПолеТипОписание
cardstringrequiredНомер карты получателя.
holderstringrequiredИмя держателя карты (латиница).

Денежная величина

Объекты source (списание) и target (зачисление) в теле запроса.

ПолеТипОписание
currencystring(3)requiredКод валюты ISO-4217.
amountdecimalrequiredСумма строкой, > 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"
}
codeHTTPОписание
VALIDATION_ERROR400Некорректное тело запроса. message перечисляет проблемные поля.
UNAUTHORIZED401Подпись/заголовки не прошли проверку, протух X-Timestamp, неизвестный терминал или рассинхрон terminalId.
TRANSFER_NOT_FOUND404Операция не найдена либо принадлежит другому терминалу.
NOT_FOUND404Ресурс не найден. В частности, для терминала не настроен курсовой профиль (GET /rates).
EXTERNAL_ID_CONFLICT409externalId уже связан с другим терминалом.
TERMINAL_NOT_FOUND422Терминал с указанным terminalId не существует.
INTERNAL_ERROR500Непредвиденная ошибка на стороне сервиса.

На этой странице