NUMUS Документация API

Callback-уведомления

Вы принимаете события только от NUMUS. Формат и подпись одинаковы для QR, карт, привязок и возвратов и не меняются при переключении внутреннего платёжного маршрута.

Куда передать callbackUrl

Настройте адрес один раз подписанным запросом. Не добавляйте его в каждый QR или карточный платёж.

PUT https://api.numus.online/api/integration/endpoints
{
  "callbackUrl": "https://api.merchant.example/webhooks/numus",
  "returnUrl": "https://merchant.example/payment/return"
}
АдресКто вызываетНазначение
Ваш callbackUrl Сервис уведомлений NUMUS Доставить события платежей, привязок и возвратов, перечисленные ниже.
Ваш returnUrl Браузер покупателя Вернуть покупателя после платёжной формы. Этот переход не подтверждает оплату.
Отгружайте заказ только после подписанного payment.completed или после чтения статуса через API.

События привязки

После первичного платежа с savePaymentMethod: true NUMUS отправляет binding.created. После отзыва — binding.revoked. Подпись и правила повтора те же.

{
  "schemaVersion": 1,
  "eventId": "0190c5d2-85a0-731c-980d-7cf86eb0774f",
  "type": "binding.created",
  "aggregateVersion": 2,
  "createdAt": "2026-07-24T18:15:00Z",
  "data": {
    "bindingId": "0190c5d1-0f08-7fc1-b3e8-a8c4ce278977",
    "customerId": "customer-381",
    "status": "ACTIVE",
    "consentId": "0190c5d0-e386-7290-a81b-0836d60ed896",
    "period": "P1M",
    "nextChargeAt": "2026-08-24T18:15:00Z",
    "bankOperationId": null,
    "occurredAt": "2026-07-24T18:15:00Z"
  }
}

Событие не содержит внутренний токен или сведения о платёжном маршруте. Сохраните bindingId вместе с customerId.

События и порядок версий

СобытиеКогда отправляется
payment.completedПлатёж подтверждён и записан в финансовый журнал NUMUS.
payment.failedПлатёж впервые перешёл в окончательное состояние REJECTED, EXPIRED, FAILED или CANCELLED. Для PENDING событие не отправляется.
binding.createdПосле успешного первичного платежа создана активная привязка.
binding.revokedПривязка отозвана и новые списания запрещены.
refund.pendingКоманда полного или частичного возврата сохранена и ожидает результата.
refund.completedУказанная в событии сумма возврата подтверждена.
refund.failedВозврат не подтверждён; используйте нормализованный статус из data.
payment.refundedПодтверждённые возвраты достигли полной суммы, исходный платёж переведён в состояние REFUNDED.

Все события одного платежа имеют общий монотонно возрастающий aggregateVersion. Обрабатывайте их в этом порядке и не применяйте событие с версией ниже уже сохранённой.

Исходящее событие payment.failed

Событие доступно в TEST и LIVE. Сохраните событие с дедупликацией по eventId до ответа HTTP 200. Операции, завершившиеся до включения события, автоматически повторно не отправляются.
{
  "schemaVersion": 1,
  "eventId": "0190c5ad-2f51-7b1d-9f2e-e0a01d8f9a31",
  "type": "payment.failed",
  "aggregateVersion": 1,
  "createdAt": "2026-08-06T15:10:00Z",
  "data": {
    "paymentId": null,
    "orderId": "0190c5ad-2f51-7b1d-9f2e-e0a01d8f9a30",
    "merchantOrderId": "order-2026-00043",
    "status": "REJECTED",
    "amount": "1250.50",
    "currency": "RUB",
    "paymentMethod": "CARD",
    "failureCode": "PAYMENT_REJECTED",
    "statusReason": "provider_rejected",
    "bankOperationId": null,
    "occurredAt": "2026-08-06T15:09:59Z"
  }
}

Для сопоставления любого результата используйте data.orderId: он совпадает с id, возвращённым при создании платежа. data.paymentId появляется только в payment.completed и обозначает подтверждённую финансовую транзакцию; в payment.failed это поле всегда равно null. Поле data.statusReason необязательно и содержит только нормализованную причину NUMUS без названия банка, маршрута или внутреннего банковского кода. Не делайте бизнес-логику зависимой от его наличия.

NUMUS отправляет payment.failed один раз при первом окончательном переходе. Повторное или запоздалое отрицательное событие не создаёт второй callback и не может изменить уже подтверждённый платёж.

Исходящее событие payment.completed

X-Numus-Event-ID: 0190c5ad-2f51-7b1d-9f2e-e0a01d8f9a21
X-Numus-Event-Type: payment.completed
X-Numus-Timestamp: 1784912400
X-Numus-Signature: whk_0123456789ab:v1=64_LOWERCASE_HEX_CHARACTERS
Content-Type: application/json

{
  "schemaVersion": 1,
  "eventId": "0190c5ad-2f51-7b1d-9f2e-e0a01d8f9a21",
  "type": "payment.completed",
  "aggregateVersion": 1,
  "createdAt": "2026-07-24T18:12:00Z",
  "data": {
    "paymentId": "0190c5ad-2f51-7b1d-9f2e-e0a01d8f9a22",
    "orderId": "0190c5ad-2f51-7b1d-9f2e-e0a01d8f9a20",
    "merchantOrderId": "order-2026-00042",
    "status": "COMPLETED",
    "amount": "1250.50",
    "currency": "RUB",
    "paymentMethod": "CARD",
    "bankOperationId": "bop_0190c5ad-2f51-7b1d-9f2e-e0a01d8f9a23",
    "occurredAt": "2026-07-24T18:11:59Z"
  }
}

Формат тела версионируется полем schemaVersion. Для версии 1 полезная нагрузка всегда находится в объекте data; поля старого конверта id, occurredAt и payment не используются.

Для QR data.orderId совпадает с id ответа POST /api/qr, а data.merchantOrderId содержит пустую строку. Для карточных и повторных платежей data.merchantOrderId содержит переданный клиентом orderID или merchantOrderId.

Подпись считается по точным байтам:

HMAC-SHA256(
  Base64Decode(callbackSecret),
  timestamp + "." + rawBody
)

Поле secret возвращается в Base64. Перед вычислением HMAC декодируйте его один раз: результатом должны быть 32 байта. В качестве rawBody используйте исходные байты HTTP-тела ровно в том виде, в котором они пришли, до разбора или повторной сериализации JSON. Не удаляйте и не добавляйте пробелы или переносы строк и не меняйте порядок полей. Если тело пришло одной строкой, подписывается одна строка; если в нём есть переносы, они также входят в подпись. eventID в подписываемую строку не входит — он нужен для защиты бизнес-обработки от повторов.

Как получить rawBody

Go (net/http):       rawBody, err := io.ReadAll(r.Body)
Node.js (Express):  app.post(path, express.raw({ type: "application/json" }), handler)
PHP:                $rawBody = file_get_contents("php://input");
Python (FastAPI):   rawBody = await request.body()

Проверяйте подпись по этим байтам и только после успешной проверки разбирайте JSON. В Node.js не подключайте express.json() перед обработчиком callback-маршрута.

Формат значения заголовка: keyId:v1=lowercaseHexHmac. Во время ротации в одном заголовке могут находиться две подписи через запятую. Выберите подпись по известному keyId, вычислите HMAC соответствующим секретом и сравните шестнадцатеричные значения константным временем. Callback считается подлинным, если прошла проверку хотя бы одна подпись с известным keyId.

После настройки callbackUrl получите секрет один раз подписанным POST /api/integration/callback-secret и храните отдельно от Ed25519-ключа API. Обычный GET его не возвращает. При подозрении на компрометацию немедленно сообщите NUMUS и не доверяйте callback до подтверждения ротации.

Как отвечать

  1. Прочитайте исходное тело запросаНе преобразовывайте JSON до проверки подписи.
  2. Проверьте время и HMACСравнивайте подпись константным временем.
  3. Сохраните идентификатор событияИспользуйте уникальное ограничение в базе и меняйте заказ в той же транзакции.
  4. Верните HTTP 200Повтор уже сохранённого события тоже должен получить HTTP 200 без повторной отгрузки.
Ваш ответПоведение NUMUS
200Доставка считается завершённой.
408, 425, 429, 500–599 или сетевая ошибкаNUMUS повторяет доставку с увеличивающейся паузой.
Другой 4xxОшибка считается постоянной; автоматические попытки прекращаются.
Другой кодДоставка не подтверждена.

Восстановление пропущенного уведомления

Уведомление ускоряет обработку, но не заменяет сверку. Для QR используйте GET /api/payments/{paymentPointID}. Для карточного платежа — GET /api/card/{id}. Оба запроса подписываются Ed25519.

Проверить приём уведомлений