Перейти к содержимому

Callbacks и JWT

Интеграция должна выполнить POST запрос с аутентификацией в ReactivePay на URL: /callbacks/v2/gateway_callbacks/{token}.

В заголовке Authorization: Bearer <TOKEN> передается JWT. Для подписи используется ключи из настроек мерчанта: settings.sign_key (Merchant Sign Key).

{
"token": "tok_xxxx",
"status": "declined",
"amount": 10000,
"currency": "USD",
"reason": "Insufficient funds",
"secure": {
"encrypted_data": "...",
"iv_value": "..."
},
"logs": [...]
}
Поле Тип Обязательное Описание
token string да Токен из payment.token исходного запроса /pay
status string да approved, declined, refunded, pending, expired
amount integer да Сумма в копейках
currency string да Валюта (ISO 4217)
reason string да* Причина отказа (обязательно при status: "declined")
secure object да Зашифрованный merchant_private_key (AES-256-CBC)
logs InteractionLog нет Лог входящего вебхука от провайдера

* Отсутствие reason при status: "declined" вызовет ошибку Declined payment must have a reason.

Статус Описание
approved Платёж подтверждён
declined Отказ (обязательно указать reason)
refunded Средства возвращены
pending В обработке
expired Сессия истекла

Подробнее — в Настройки и Статусы.

{
"secure": {
"encrypted_data": "<base64>",
"iv_value": "<base64>"
}
}
Поле Описание
encrypted_data merchant_private_key, зашифрованный AES-256-CBC, в base64
iv_value Случайный инициализационный вектор (16 байт) в base64
  1. Входные данные: payment.merchant_private_key из исходного /pay запроса
  2. Ключ шифрования: settings.sign_key
  3. Алгоритм: AES-256-CBC
  4. IV: случайные 16 байт
  5. Результат: base64-строка, помещаемая в secure.encrypted_data; IV помещается в secure.iv_value в base64

После формирования payload с полем secure, весь JSON подписывается алгоритмом HS512 с тем же ключом settings.sign_key:

Header: { "alg": "HS512", "typ": "JWT" }

Возможные ошибки при обработке коллбека:

Ошибка Причина
Invalid JWT signature Неверная подпись JWT
Invalid status Статус не входит в список допустимых
Declined payment must have a reason Отказ без указания reason
Payment not found or already in final status Платёж не найден или уже завершён
Invalid payment gatewayable type Несоответствие типа шлюза и провайдера
import * as jose from "jose";
import crypto from "node:crypto";
type SecureBlock = {
encrypted_data: string;
iv_value: string;
};
type JwtPayload = {
[key: string]: any;
secure: SecureBlock;
};
function generateIV(): Buffer {
return crypto.randomBytes(16);
}
function encryptMerchantKey(
merchantKey: string,
signKey: Buffer, // 32 bytes
iv: Buffer,
): { encryptedData: string; ivBase64: string } {
let cipher = crypto.createCipheriv("aes-256-cbc", signKey, iv);
let encrypted = cipher.update(merchantKey, "utf8", "base64");
encrypted += cipher.final("base64");
return {
encryptedData: encrypted,
ivBase64: iv.toString("base64"),
};
}
export async function createJwt(
payload: Record<string, any>,
merchantKey: string,
signKey: Buffer,
) {
let iv = generateIV();
let { encryptedData, ivBase64 } = encryptMerchantKey(
merchantKey,
signKey,
iv,
);
let jwtPayload: JwtPayload = {
...payload,
secure: {
encrypted_data: encryptedData,
iv_value: ivBase64,
},
};
return await new jose.SignJWT(jwtPayload)
.setProtectedHeader({
alg: "HS512",
typ: "JWT",
})
.sign(signKey);
}