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

Начало работы

Этот сценарий доводит интеграцию от выдачи доступов до первого подтверждённого QR-платежа.

Что потребуется

ЗначениеКто создаётГде используется
partnerIDNUMUSВ каждом подписанном запросе
paymentPointIDNUMUS APIНужен только при наличии нескольких платёжных точек
Пара ключей Ed25519КлиентПриватный ключ подписывает запрос; публичный ключ регистрируется в NUMUS
Секрет callbackNUMUS APIПолучается один раз после настройки адресов и проверяет уведомления
callbackUrlКлиентОдин адрес backend для событий по всем способам оплаты
returnUrlКлиентАдрес возврата браузера покупателя после платёжной формы
NUMUS не запрашивает приватный ключ Ed25519. Передайте только публичный ключ в формате 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)

Рабочий пример на 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

  1. Убедитесь, что NUMUS подтвердил активацию именно этого partnerID и публичного ключа.
  2. Проверьте, что поля partnerID и key в заголовке точно совпадают с зарегистрированными значениями.
  3. Сверьте время на backend: допустимое расхождение не превышает пяти минут.
  4. Используйте новый nonce для каждой попытки.
  5. Проверьте путь, отсортированную query string, Idempotency-Key, регистр HTTP-метода и точные байты body.

Не создавайте новый финансовый запрос, пока не найдена причина ошибки подписи.

В разделе «Методы API» можно переключать транспортные примеры между JavaScript, Python, PHP, Go, Java, C# и другими языками. Для Python, Go и Node.js выше приведены отдельные рабочие примеры криптографической части.

Задать callbackUrl и returnUrl

Сделайте это один раз после получения доступа. callbackUrl относится ко всему партнёру. Сохранённый returnUrl используется по умолчанию для всех платежей.

PUT 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

POST 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"
}
Не записывайте тело ответа в application-логи. Сразу перенесите secret в secret manager. Если ответ потерялся, повторите тот же запрос с тем же Idempotency-Key в течение пяти минут, но сформируйте новые timestamp, nonce и подпись Authorization.

Получить платёжные точки

GET https://api.numus.online/api/integration/payment-points

Это подписанный запрос. Ответ содержит только платёжные точки текущего партнёра и включённые для них способы оплаты:

{
  "items": [
    {
      "paymentPointID": "shop-001-main",
      "storeID": "store-001",
      "name": "Интернет-магазин",
      "status": "ACTIVE",
      "paymentMethods": ["CARD", "SBP_DYNAMIC_QR"]
    }
  ]
}
При одной активной точке не передавайте её идентификатор в платёжном запросе: NUMUS определит партнёра по подписи и выберет точку автоматически. При нескольких точках передайте paymentPointID. Банк и внутренний маршрут клиент не выбирает.

Первый QR-платёж

  1. Настройте адреса интеграцииПодписанный PUT /api/integration/endpoints сохраняет callbackUrl и returnUrl.
  2. Проверьте платёжные точкиЕсли точка одна, её идентификатор в запросе не нужен.
  3. Создайте платёжПодписанный POST /api/qr на api.numus.online принимает сумму и Idempotency-Key.
  4. Получите готовый QRИспользуйте nspkurl или qrImage. Если их ещё нет, опрашивайте statusUrl.
  5. Оплатите тестовый QRПосле подтверждения статус станет COMPLETED.
  6. Примите callbackПроверьте подпись и обработайте event ID ровно один раз.
Открыть подробную документацию по QR

Уведомление о завершённом платеже

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 с уникальным ограничением и измените состояние заказа.

Проверка перед запуском