Callbacks and JWT
Integration must perform a POST request with authentication to ReactivePay at URL: /callbacks/v2/gateway_callbacks/{token}.
In the header Authorization: Bearer <TOKEN>, a JWT is passed. For signing, keys from merchant settings: settings.sign_key (Merchant Sign Key) are used.
Callback Request Structure
Section titled “Callback Request Structure”{ "token": "tok_xxxx", "status": "declined", "amount": 10000, "currency": "USD", "reason": "Insufficient funds", "secure": { "encrypted_data": "...", "iv_value": "..." }, "logs": [...]}| Field | Type | Required | Description |
|---|---|---|---|
token |
string | yes | Token from payment.token of original /pay request |
status |
string | yes | approved, declined, refunded, pending, expired |
amount |
integer | yes | Amount in kopecks |
currency |
string | yes | Currency (ISO 4217) |
reason |
string | yes* | Decline reason (required when status: "declined") |
secure |
object | yes | Encrypted merchant_private_key (AES-256-CBC) |
logs |
InteractionLog | no | Log of incoming webhook from provider |
* Absence of reason when status: "declined" will cause error Declined payment must have a reason.
Allowed Statuses
Section titled “Allowed Statuses”| Status | Description |
|---|---|
approved |
Payment approved |
declined |
Decline (reason must be specified) |
refunded |
Funds returned |
pending |
In processing |
expired |
Session expired |
More details — in Settings and Statuses.
Encryption of merchant_private_key (field secure)
Section titled “Encryption of merchant_private_key (field secure)”{ "secure": { "encrypted_data": "<base64>", "iv_value": "<base64>" }}| Field | Description |
|---|---|
encrypted_data |
merchant_private_key encrypted with AES-256-CBC, in base64 |
iv_value |
Random initialization vector (16 bytes) in base64 |
Algorithm
Section titled “Algorithm”- Input data:
payment.merchant_private_keyfrom original/payrequest - Encryption key:
settings.sign_key - Algorithm: AES-256-CBC
- IV: random 16 bytes
- Result: base64 string placed in
secure.encrypted_data; IV placed insecure.iv_valuein base64
JWT Signing (HS512)
Section titled “JWT Signing (HS512)”After forming payload with secure field, entire JSON is signed with HS512 algorithm using same key settings.sign_key:
Header: { "alg": "HS512", "typ": "JWT" }Callback Errors
Section titled “Callback Errors”Possible errors during callback processing:
| Error | Reason |
|---|---|
Invalid JWT signature |
Invalid JWT signature |
Invalid status |
Status not in allowed list |
Declined payment must have a reason |
Decline without reason |
Payment not found or already in final status |
Payment not found or already finalized |
Invalid payment gatewayable type |
Gateway and provider type mismatch |
Example of Signature Generation
Section titled “Example of Signature Generation”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)}