Skip to content

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.

{
"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.

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
  1. Input data: payment.merchant_private_key from original /pay request
  2. Encryption key: settings.sign_key
  3. Algorithm: AES-256-CBC
  4. IV: random 16 bytes
  5. Result: base64 string placed in secure.encrypted_data; IV placed in secure.iv_value in base64

After forming payload with secure field, entire JSON is signed with HS512 algorithm using same key settings.sign_key:

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

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
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);
}