Callbacks и JWT
Интеграция должна выполнить POST запрос с аутентификацией в ReactivePay на URL: /callbacks/v2/gateway_callbacks/{token}.
В заголовке Authorization: Bearer <TOKEN> передается JWT. Для подписи используется ключи из настроек мерчанта: settings.sign_key (Merchant Sign Key).
Структура callback-запроса
Заголовок раздела «Структура callback-запроса»{ "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 |
Сессия истекла |
Подробнее — в Настройки и Статусы.
Шифрование merchant_private_key (поле secure)
Заголовок раздела «Шифрование merchant_private_key (поле secure)»{ "secure": { "encrypted_data": "<base64>", "iv_value": "<base64>" }}| Поле | Описание |
|---|---|
encrypted_data |
merchant_private_key, зашифрованный AES-256-CBC, в base64 |
iv_value |
Случайный инициализационный вектор (16 байт) в base64 |
Алгоритм
Заголовок раздела «Алгоритм»- Входные данные:
payment.merchant_private_keyиз исходного/payзапроса - Ключ шифрования:
settings.sign_key - Алгоритм: AES-256-CBC
- IV: случайные 16 байт
- Результат: base64-строка, помещаемая в
secure.encrypted_data; IV помещается вsecure.iv_valueв base64
Подпись JWT (HS512)
Заголовок раздела «Подпись JWT (HS512)»После формирования 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);}def merchant_token_encrypt(merchant_token: str, sign_key: str) -> dict: def pad(data: bytes) -> bytes: pad_len = 16 - (len(data) % 16) return data + bytes([pad_len] * pad_len)
key = sign_key.encode('utf-8')[:32] iv = get_random_bytes(16)
cipher = AES.new(key, AES.MODE_CBC, iv) padded_data = pad(merchant_token.encode('utf-8')) encrypted = cipher.encrypt(padded_data)
return { "encrypted_data": base64.b64encode(encrypted).decode('utf-8'), "iv_value": base64.b64encode(iv).decode('utf-8') }
async def callback_body(data: GatewayCallback): raw_data = data.model_dump(exclude_none=True) private_key, token = db.get_token(raw_data.get("INT_REF")) logger.info(f"Gateway callback body: {raw_data}")
token, callback_body = gateway_callback_body(raw_data)
secure_data = merchant_token_encrypt(bearer_token, config.SIGN_KEY) jwt_payload = { **callback_body, "secure": secure_data } jwt_token = jwt.encode( payload=jwt_payload, key=config.SIGN_KEY, algorithm="HS512" ) callback_headers = { "Authorization": f"Bearer {jwt_token}", "Content-Type": "application/json" } return json.dumps(callback_body, separators=(',', ':'), ensure_ascii=False)package connect
import ( "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/rand" "crypto/sha512" "encoding/base64" "encoding/json" "fmt")
type CallbackPayload struct { Status string `json:"status"` Currency string `json:"currency"` Amount int `json:"amount"` Reason *string `json:"reason,omitempty"`}
type SecureBlock struct { EncryptedData string `json:"encrypted_data"` IVValue string `json:"iv_value"`}
type JWTPayload struct { Payload CallbackPayload `json:"payload"` Secure SecureBlock `json:"secure"`}
func genIV() []byte { iv := make([]byte, 16) rand.Read(iv) return iv}
func pkcs7Pad(data []byte, blockSize int) []byte { padding := blockSize - len(data)%blockSize padText := make([]byte, padding) for i := range padText { padText[i] = byte(padding) } return append(data, padText...)}
// AES-256-CBC encryption with PKCS7 paddingfunc encryptMerchantKey(merchantKey string, signKey []byte, iv []byte) (string, string, error) { block, err := aes.NewCipher(signKey) if err != nil { return "", "", err }
plainText := pkcs7Pad([]byte(merchantKey), block.BlockSize()) mode := cipher.NewCBCEncrypter(block, iv) cipherText := make([]byte, len(plainText)) mode.CryptBlocks(cipherText, plainText)
encryptedData := base64.StdEncoding.EncodeToString(cipherText) ivValue := base64.StdEncoding.EncodeToString(iv)
return encryptedData, ivValue, nil}
// Encode JWT using HMAC-SHA512func encodeJWT(payload any, signKey []byte) (string, error) { header := map[string]string{ "alg": "HS512", "typ": "JWT", }
headerJSON, _ := json.Marshal(header) payloadJSON, _ := json.Marshal(payload)
base64Header := base64.RawURLEncoding.EncodeToString(headerJSON) base64Payload := base64.RawURLEncoding.EncodeToString(payloadJSON)
message := fmt.Sprintf("%s.%s", base64Header, base64Payload)
mac := hmac.New(sha512.New, signKey) mac.Write([]byte(message)) signature := mac.Sum(nil) base64Signature := base64.RawURLEncoding.EncodeToString(signature)
return fmt.Sprintf("%s.%s.%s", base64Header, base64Payload, base64Signature), nil}
// Create JWT with encrypted merchant keyfunc CreateJWT(payload CallbackPayload, merchantKey string, signKey []byte) (string, error) { iv := genIV()
encryptedData, ivValue, err := encryptMerchantKey(merchantKey, signKey, iv) if err != nil { return "", err }
jwtPayload := JWTPayload{ Payload: payload, Secure: SecureBlock{ EncryptedData: encryptedData, IVValue: ivValue, }, }
return encodeJWT(jwtPayload, signKey)}use aes::cipher::block_padding;use base64::{Engine, engine::general_purpose};use serde::{Deserialize, Serialize};
use crate::connect::callback::CallbackPayload;
#[derive(Serialize, Deserialize)]struct SecureBlock { encrypted_data: String, iv_value: String,}
#[derive(Serialize)]struct JwtPayload<'a> { #[serde(flatten)] payload: &'a CallbackPayload, secure: SecureBlock,}
fn gen_iv() -> [u8; 16] { use rand::TryRngCore; let mut iv = [0u8; 16]; rand::rngs::OsRng.try_fill_bytes(&mut iv).unwrap(); iv}
fn encrypt_merchant_key( merchant_key: &str, sign_key: [u8; 32], iv: [u8; 16],) -> anyhow::Result<(String, String)> { use aes::cipher::{BlockEncryptMut, KeyIvInit};
let res = cbc::Encryptor::<aes::Aes256>::new(&sign_key.into(), &iv.into()) .encrypt_padded_vec_mut::<block_padding::Pkcs7>(merchant_key.as_bytes()); let encrypted_data = general_purpose::STANDARD.encode(&res); let iv_base64 = general_purpose::STANDARD.encode(iv); Ok((encrypted_data, iv_base64))}
// Function to create the JWTpub fn create_jwt( payload: &CallbackPayload, merchant_key: &str, sign_key: &[u8; 32],) -> anyhow::Result<String> { let iv = gen_iv(); // Encrypt the merchant key let (encrypted_data, iv_value) = encrypt_merchant_key(merchant_key, *sign_key, iv)?;
// Create the JWT payload let payload = JwtPayload { payload, secure: SecureBlock { encrypted_data, iv_value, }, };
let token = j::encode(&payload, sign_key)?; Ok(token)}