Начало работы
Этот сценарий доводит интеграцию от выдачи доступов до первого подтверждённого QR-платежа.
Что потребуется
| Значение | Кто создаёт | Где используется |
|---|---|---|
partnerID | NUMUS | В каждом подписанном запросе |
paymentPointID | NUMUS API | Нужен только при наличии нескольких платёжных точек |
| Пара ключей Ed25519 | Клиент | Приватный ключ подписывает запрос; публичный ключ регистрируется в NUMUS |
| Секрет callback | NUMUS API | Получается один раз после настройки адресов и проверяет уведомления |
callbackUrl | Клиент | Один адрес backend для событий по всем способам оплаты |
returnUrl | Клиент | Адрес возврата браузера покупателя после платёжной формы |
ssh-ed25519 AAAA.... Генерация ключа не
регистрирует его автоматически: дождитесь подтверждения активации
partnerID со стороны NUMUS. До активации подписанные
запросы возвращают 401 unauthorized.
После настройки адресов получите секрет подписанным
POST /api/integration/callback-secret. Он не совпадает с
приватным ключом API и не возвращается методом GET.
Сохраните ответ сразу: новый запрос после пятиминутного окна безопасного
повтора вернёт 409. При подозрении на компрометацию
немедленно сообщите NUMUS и не доверяйте callback до подтверждения
ротации.
Создать ключ Ed25519
Откройте локальный генератор в браузере или используйте готовый скрипт на Node.js 20 или новее. Оба варианта создают приватный PKCS#8 и публичный OpenSSH:
curl -fsSLO https://doc.numus.online/examples/generate-ed25519.mjs
node generate-ed25519.mjs
Храните numus-private.pem в менеджере секретов. Не
добавляйте его в Git, Docker image, документацию или переменные
клиентского приложения.
Передайте NUMUS ваш partnerID и содержимое файла
numus-public.pub. Начинайте проверку API только после
подтверждения, что эта пара зарегистрирована и активна.
Подписать запрос
Заголовок Authorization содержит JSON:
{
"partnerID": "partner-001",
"key": "ssh-ed25519 AAAAC3...",
"sign": "BASE64_SIGNATURE",
"timestamp": "1784912400",
"nonce": "8f651495424f37947d94e1ac4e0ab14a"
}
Подписывается ровно эта строка, без перевода строки в конце:
timestamp + "\n" +
nonce + "\n" +
METHOD + "\n" +
ESCAPED_PATH + "\n" +
CANONICAL_QUERY + "\n" +
IDEMPOTENCY_KEY_OR_EMPTY + "\n" +
LOWERCASE_HEX_SHA256(RAW_BODY)
timestamp— Unix-время в секундах; допустимое расхождение часов — пять минут;nonce— случайная строка длиной 16–128 символов; повторное значение отклоняется;METHODпередаётся в верхнем регистре;CANONICAL_QUERYсодержит параметры, отсортированные по имени, без начального?; если параметров нет — пустая строка;IDEMPOTENCY_KEY_OR_EMPTY— точное значение единственного заголовкаIdempotency-Keyили пустая строка;- хеш считается от тех же байтов JSON, которые отправляются по сети.
Рабочий пример на Node.js
import { createHash, randomBytes, sign } from "node:crypto";
import { readFileSync } from "node:fs";
const partnerID = "partner-001";
const publicKey = readFileSync("./numus-public.pub", "utf8").trim();
const privateKey = readFileSync("./numus-private.pem");
const method = "PUT";
const path = "/api/integration/endpoints";
const canonicalQuery = "";
const idempotencyKey = "";
const payload = {
callbackUrl: "https://api.merchant.example/webhooks/numus",
returnUrl: "https://merchant.example/payment/return"
};
const body = JSON.stringify(payload);
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = randomBytes(16).toString("hex");
const bodyHash = createHash("sha256").update(body).digest("hex");
const canonical = [
timestamp,
nonce,
method,
path,
canonicalQuery,
idempotencyKey,
bodyHash
].join("\n");
const signature = sign(null, Buffer.from(canonical), privateKey).toString("base64");
const authorization = JSON.stringify({
partnerID,
key: publicKey,
sign: signature,
timestamp,
nonce
});
const response = await fetch("https://api.numus.online" + path, {
method,
headers: {
"Content-Type": "application/json",
"Authorization": authorization
},
body
});
console.log(response.status, await response.text());
Готовые примеры для backend
Примеры ниже самостоятельно читают PKCS#8-ключ, формируют точную
каноническую строку, подписывают её и отправляют запрос. Замените
partner-001 и тестовые URL своими значениями.
Если сервер вернул 401
- Убедитесь, что NUMUS подтвердил активацию именно этого
partnerIDи публичного ключа. - Проверьте, что поля
partnerIDиkeyв заголовке точно совпадают с зарегистрированными значениями. - Сверьте время на backend: допустимое расхождение не превышает пяти минут.
- Используйте новый
nonceдля каждой попытки. - Проверьте путь, отсортированную query string,
Idempotency-Key, регистр HTTP-метода и точные байтыbody.
Не создавайте новый финансовый запрос, пока не найдена причина ошибки подписи.
Задать callbackUrl и returnUrl
Сделайте это один раз после получения доступа. callbackUrl
относится ко всему партнёру. Сохранённый returnUrl
используется по умолчанию для всех платежей.
https://api.numus.online/api/integration/endpoints
{
"callbackUrl": "https://api.merchant.example/webhooks/numus",
"returnUrl": "https://merchant.example/payment/return"
}
| Поле | Назначение | Важно |
|---|---|---|
callbackUrl | Сервер NUMUS отправляет сюда окончательный результат: payment.completed или payment.failed | Возвращайте HTTP 200 только после сохранения события |
returnUrl | Сюда возвращается браузер покупателя | Не используйте возврат браузера как подтверждение оплаты |
Оба значения должны быть абсолютными публичными HTTPS-адресами без
фрагмента. Текущие значения возвращает подписанный
GET /api/integration/endpoints.
В POST /api/card и POST /api/qr можно передать
свой returnUrl для конкретного заказа. Допускается другой
путь и query string, но домен, схема и порт должны совпадать с
сохранённым адресом. Так NUMUS поддерживает разные страницы результата
без возможности подменить домен возврата.
Получить секрет проверки callback
https://api.numus.online/api/integration/callback-secret
Вызовите метод один раз после сохранения адресов. Запрос подписывается
тем же ключом Ed25519 и обязательно содержит уникальный
Idempotency-Key длиной 16–128 символов.
{
"confirmSecureStorage": true
}
{
"secret": "BASE64_32_BYTE_SECRET",
"keyId": "whk_0123456789ab",
"algorithm": "HMAC-SHA256",
"signatureHeader": "X-Numus-Signature",
"issuedAt": "2026-07-27T12:00:00Z",
"replayUntil": "2026-07-27T12:05:00Z"
}
secret в secret manager. Если ответ потерялся, повторите
тот же запрос с тем же Idempotency-Key в течение пяти
минут, но сформируйте новые timestamp, nonce и подпись Authorization.
Получить платёжные точки
https://api.numus.online/api/integration/payment-points
Это подписанный запрос. Ответ содержит только платёжные точки текущего партнёра и включённые для них способы оплаты:
{
"items": [
{
"paymentPointID": "shop-001-main",
"storeID": "store-001",
"name": "Интернет-магазин",
"status": "ACTIVE",
"paymentMethods": ["CARD", "SBP_DYNAMIC_QR"]
}
]
}
paymentPointID. Банк и внутренний маршрут клиент не
выбирает.
Первый QR-платёж
- Настройте адреса интеграцииПодписанный PUT /api/integration/endpoints сохраняет callbackUrl и returnUrl.
- Проверьте платёжные точкиЕсли точка одна, её идентификатор в запросе не нужен.
- Создайте платёжПодписанный POST /api/qr на api.numus.online принимает сумму и Idempotency-Key.
- Получите готовый QRИспользуйте nspkurl или qrImage. Если их ещё нет, опрашивайте statusUrl.
- Оплатите тестовый QRПосле подтверждения статус станет COMPLETED.
- Примите callbackПроверьте подпись и обработайте event ID ровно один раз.
Уведомление о завершённом платеже
NUMUS отправляет POST на callbackUrl из настроек интеграции:
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-24T17:30:00Z",
"data": {
"paymentId": "0190c5ad-2f51-7b1d-9f2e-e0a01d8f9a22",
"orderId": "0190c5ad-2f51-7b1d-9f2e-e0a01d8f9a20",
"merchantOrderId": "",
"status": "COMPLETED",
"amount": "1490.50",
"currency": "RUB",
"paymentMethod": "QR",
"bankOperationId": "bop_0190c5ad-2f51-7b1d-9f2e-e0a01d8f9a23",
"occurredAt": "2026-07-24T17:29:59Z"
}
}
Для QR поле data.orderId совпадает с id,
возвращённым методом POST /api/qr, а
data.merchantOrderId передаётся пустой строкой. Сохраняйте
соответствие между этим id и заказом в своей базе.
Этот же data.orderId приходит в
payment.failed. У неуспешной операции
data.paymentId равен null, потому что
подтверждённая финансовая транзакция не создавалась. Поэтому для
универсального сопоставления результата используйте
data.orderId, а успешный data.paymentId
сохраняйте для последующей сверки и возврата.
Подпись — HMAC-SHA256 от точной последовательности байтов:
HMAC-SHA256(
Base64Decode(callbackSecret),
timestamp + "." + rawBody
)
Секрет callback передаётся клиенту в Base64. Декодируйте его один раз
до 32 исходных байт. rawBody — точные байты HTTP-тела до
разбора JSON: включая все полученные пробелы и переносы строк. Не
нормализуйте и не сериализуйте тело повторно. eventID в подписываемую строку не входит и
используется для идемпотентной обработки события. Значение заголовка
имеет формат keyId:v1=lowercaseHexHmac; при ротации один
заголовок может содержать две подписи через запятую. Сначала проверьте
время и хотя бы одну подпись с известным keyId, затем в
одной транзакции сохраните eventID с уникальным
ограничением и измените состояние заказа.
- на первое событие верните
200только после сохранения в базе; - на повтор того же
eventIDверните200, но не повторяйте бизнес-операцию; - на неверную подпись верните
401или403; - поле
data.amountсодержит точную десятичную строку с двумя знаками; не преобразуйте деньги через двоичныйfloat.
Проверка перед запуском
- приватный ключ хранится только на backend и не попадает в логи;
- часы backend синхронизированы, nonce не повторяется;
- один заказ всегда использует один Idempotency-Key;
- callbackUrl и returnUrl настроены через
PUT /api/integration/endpoints; GET /api/integration/payment-pointsвозвращает доступныеpaymentPointID;- секрет callback один раз получен через подписанный POST и сохранён отдельно от ключа API;
- callback доступен по HTTPS и не отвечает перенаправлением;
- event ID сохраняется с уникальным ограничением;
- повтор запроса и повтор callback не создают второй платёж;
- история
GET /api/payments/{paymentPointID}совпадает с учётом клиента; - вы работаете только с идентификаторами NUMUS из ответа API.