Callback-уведомления
Вы принимаете события только от NUMUS. Формат и подпись одинаковы для QR, карт, привязок и возвратов и не меняются при переключении внутреннего платёжного маршрута.
Куда передать callbackUrl
Настройте адрес один раз подписанным запросом. Не добавляйте его в каждый QR или карточный платёж.
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
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 до
подтверждения ротации.
Как отвечать
- Прочитайте исходное тело запросаНе преобразовывайте JSON до проверки подписи.
- Проверьте время и HMACСравнивайте подпись константным временем.
- Сохраните идентификатор событияИспользуйте уникальное ограничение в базе и меняйте заказ в той же транзакции.
- Верните HTTP 200Повтор уже сохранённого события тоже должен получить HTTP 200 без повторной отгрузки.
| Ваш ответ | Поведение NUMUS |
|---|---|
200 | Доставка считается завершённой. |
408, 425, 429, 500–599 или сетевая ошибка | NUMUS повторяет доставку с увеличивающейся паузой. |
Другой 4xx | Ошибка считается постоянной; автоматические попытки прекращаются. |
| Другой код | Доставка не подтверждена. |
Восстановление пропущенного уведомления
Уведомление ускоряет обработку, но не заменяет сверку. Для QR используйте
GET /api/payments/{paymentPointID}. Для карточного платежа —
GET /api/card/{id}. Оба запроса подписываются Ed25519.