Webhooks
Webhook notifications allow you to efficiently track payments/deposits states, and enable you to automate communication internally or with your customers.They are push notifications sent as webhook message to URLs you defined.
You can subscribe to the payments/deposit events. When one of these events is triggered, we send a request to a URL of your choice in a JSON format. Along with the event, data about the payment/deposit is included, which we refer to as the payload.
Supported event types
| Event type | Description |
|---|---|
| PAYMENT | Subscribe the transaction of payment |
| DEPOSIT | Subscribe the transaction of deposit |
Decrypting Webhook Payloads
To ensure the security of webhook notifications, all webhook payloads are encrypted using AES-256-CBC.
Step 1: Receive the Webhook
When a webhook is triggered, you'll receive a POST request with the following structure:
{
"iv": "c6407a24ca95deaec313786321ec1e39",
"encrypted": "encrypted_payload_here"
}
Step 2: Decrypt the Payload
Decrypt the payload using AES-256-CBC encryption.
- Algorithm: AES-256-CBC
- Key Size: 32 bytes (256 bits)
- IV Size: 16 bytes (128 bits)
- Encoding: Hex for encrypted data and IV
- Node.js
- Python
- Java
- PHP
- C#
- Go
const { createDecipheriv } = require("crypto");
function decryptWebhookPayload(encryptedData, secret, iv) {
// Create decipher with AES-256-CBC
const decipher = createDecipheriv(
"aes-256-cbc",
Buffer.from(secret, "utf-8"),
Buffer.from(iv, "hex")
);
// Decrypt the data
let decrypted = decipher.update(encryptedData, "hex", "utf8");
decrypted += decipher.final("utf8");
// Parse and return JSON
return JSON.parse(decrypted);
}
// Usage
const secret = "your_32_byte_secret_key_here"; // 32 characters for AES-256
const iv = "1234567890abcdef1234567890abcdef"; // 16 bytes in hex (32 chars)
const encryptedData = webhookData.data; // Hex encoded encrypted data
try {
const decryptedPayload = decryptWebhookPayload(encryptedData, secret, iv);
console.log("Decrypted payload:", decryptedPayload);
} catch (error) {
console.error("Decryption failed:", error.message);
}
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import json
def decrypt_webhook_payload(encrypted_data, secret, iv):
"""
Decrypt webhook payload using AES-256-CBC
Args:
encrypted_data: Hex encoded encrypted string
secret: 32-byte secret key (UTF-8 string)
iv: 16-byte initialization vector (hex string)
Returns:
Decrypted payload as dictionary
"""
# Convert inputs to bytes
key = secret.encode('utf-8')
iv_bytes = bytes.fromhex(iv)
encrypted_bytes = bytes.fromhex(encrypted_data)
# Create cipher and decrypt
cipher = AES.new(key, AES.MODE_CBC, iv_bytes)
decrypted = cipher.decrypt(encrypted_bytes)
# Remove PKCS7 padding and parse JSON
unpadded = unpad(decrypted, AES.block_size)
return json.loads(unpadded.decode('utf-8'))
# Usage
secret = 'your_32_byte_secret_key_here' # 32 characters
iv = '1234567890abcdef1234567890abcdef' # 32 hex chars (16 bytes)
encrypted_data = webhook_data['data']
try:
decrypted_payload = decrypt_webhook_payload(encrypted_data, secret, iv)
print('Decrypted payload:', decrypted_payload)
except Exception as e:
print(f'Decryption failed: {e}')
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
public class WebhookDecryptor {
public static JsonObject decryptWebhookPayload(
String encryptedData,
String secret,
String iv
) throws Exception {
// Convert hex string to bytes
byte[] encryptedBytes = hexStringToByteArray(encryptedData);
byte[] ivBytes = hexStringToByteArray(iv);
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
// Initialize cipher
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
// Decrypt
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
String decryptedString = new String(decryptedBytes, StandardCharsets.UTF_8);
// Parse JSON
Gson gson = new Gson();
return gson.fromJson(decryptedString, JsonObject.class);
}
private static byte[] hexStringToByteArray(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i+1), 16));
}
return data;
}
}
// Usage
String secret = "your_32_byte_secret_key_here"; // 32 characters
String iv = "1234567890abcdef1234567890abcdef"; // 32 hex chars
String encryptedData = webhookData.get("data").getAsString();
try {
JsonObject decryptedPayload = WebhookDecryptor.decryptWebhookPayload(
encryptedData,
secret,
iv
);
System.out.println("Decrypted payload: " + decryptedPayload);
} catch (Exception e) {
System.err.println("Decryption failed: " + e.getMessage());
}
<?php
function decryptWebhookPayload($encryptedData, $secret, $iv) {
/**
* Decrypt webhook payload using AES-256-CBC
*
* @param string $encryptedData Hex encoded encrypted string
* @param string $secret 32-byte secret key
* @param string $iv 16-byte IV (hex encoded)
* @return array Decrypted payload as associative array
*/
// Convert hex to binary
$encryptedBinary = hex2bin($encryptedData);
$ivBinary = hex2bin($iv);
// Decrypt using AES-256-CBC
$decrypted = openssl_decrypt(
$encryptedBinary,
'aes-256-cbc',
$secret,
OPENSSL_RAW_DATA,
$ivBinary
);
if ($decrypted === false) {
throw new Exception('Decryption failed: ' . openssl_error_string());
}
// Parse JSON
return json_decode($decrypted, true);
}
// Usage
$secret = 'your_32_byte_secret_key_here'; // 32 characters
$iv = '1234567890abcdef1234567890abcdef'; // 32 hex chars (16 bytes)
$encryptedData = $webhookData['data'];
try {
$decryptedPayload = decryptWebhookPayload($encryptedData, $secret, $iv);
print_r($decryptedPayload);
} catch (Exception $e) {
error_log('Decryption failed: ' . $e->getMessage());
}
?>
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json.Linq;
public class WebhookDecryptor
{
public static JObject DecryptWebhookPayload(
string encryptedData,
string secret,
string iv)
{
// Convert hex strings to bytes
byte[] encryptedBytes = HexStringToByteArray(encryptedData);
byte[] ivBytes = HexStringToByteArray(iv);
byte[] keyBytes = Encoding.UTF8.GetBytes(secret);
using (Aes aes = Aes.Create())
{
aes.Key = keyBytes;
aes.IV = ivBytes;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (ICryptoTransform decryptor = aes.CreateDecryptor())
using (MemoryStream msDecrypt = new MemoryStream(encryptedBytes))
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
string decryptedJson = srDecrypt.ReadToEnd();
return JObject.Parse(decryptedJson);
}
}
}
private static byte[] HexStringToByteArray(string hex)
{
byte[] bytes = new byte[hex.Length / 2];
for (int i = 0; i < bytes.Length; i++)
{
bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
}
return bytes;
}
}
// Usage
string secret = "your_32_byte_secret_key_here"; // 32 characters
string iv = "1234567890abcdef1234567890abcdef"; // 32 hex chars
string encryptedData = webhookData["data"].ToString();
try
{
JObject decryptedPayload = WebhookDecryptor.DecryptWebhookPayload(
encryptedData,
secret,
iv
);
Console.WriteLine("Decrypted payload: " + decryptedPayload);
}
catch (Exception ex)
{
Console.Error.WriteLine("Decryption failed: " + ex.Message);
}
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/hex"
"encoding/json"
"fmt"
)
// DecryptWebhookPayload decrypts webhook payload using AES-256-CBC
func DecryptWebhookPayload(encryptedData, secret, iv string) (map[string]interface{}, error) {
// Convert hex strings to bytes
encryptedBytes, err := hex.DecodeString(encryptedData)
if err != nil {
return nil, fmt.Errorf("failed to decode encrypted data: %w", err)
}
ivBytes, err := hex.DecodeString(iv)
if err != nil {
return nil, fmt.Errorf("failed to decode IV: %w", err)
}
keyBytes := []byte(secret)
// Create AES cipher block
block, err := aes.NewCipher(keyBytes)
if err != nil {
return nil, fmt.Errorf("failed to create cipher: %w", err)
}
// Create CBC mode decryptor
mode := cipher.NewCBCDecrypter(block, ivBytes)
// Decrypt the data
decrypted := make([]byte, len(encryptedBytes))
mode.CryptBlocks(decrypted, encryptedBytes)
// Remove PKCS7 padding
decrypted, err = removePKCS7Padding(decrypted)
if err != nil {
return nil, fmt.Errorf("failed to remove padding: %w", err)
}
// Parse JSON
var result map[string]interface{}
if err := json.Unmarshal(decrypted, &result); err != nil {
return nil, fmt.Errorf("failed to parse JSON: %w", err)
}
return result, nil
}
// removePKCS7Padding removes PKCS7 padding from decrypted data
func removePKCS7Padding(data []byte) ([]byte, error) {
length := len(data)
if length == 0 {
return nil, fmt.Errorf("invalid padding: empty data")
}
padding := int(data[length-1])
if padding > length || padding > aes.BlockSize {
return nil, fmt.Errorf("invalid padding size")
}
return data[:length-padding], nil
}
// Usage
func main() {
secret := "your_32_byte_secret_key_here" // 32 characters for AES-256
iv := "1234567890abcdef1234567890abcdef" // 32 hex chars (16 bytes)
encryptedData := "your_encrypted_data_here"
decryptedPayload, err := DecryptWebhookPayload(encryptedData, secret, iv)
if err != nil {
fmt.Printf("Decryption failed: %v\n", err)
return
}
fmt.Printf("Decrypted payload: %+v\n", decryptedPayload)
}
Step 3: Process the Decrypted Data
Once decrypted, the payload will contain the complete event data:
{
"subscriptionId": "4e408b3d-5f70-423d-b940-f7192cd77252",
"eventType": "PAYMENT",
"eventStatus": "SCHEDULED",
"timestamp": "2025-12-08T05:53:17.372Z",
"eventObject": {
"id": "4e408b3d-5f70-223d-b940-f7192cd77252",
"clientId": "72644e73-21ee-4cd7-9c56-04e6a2a96465",
"referenceNo": "20251208-PTW122",
"currencyCode": "USD",
"chargeFee": 160,
"amount": 123,
"beneficiaryId": "649e7865-3f93-4940-bfa5-d4324b24316a",
"paymentReference": "INV-123",
"paymentDate": "2025-12-08",
"purposeCode": "GOODS",
"sourceOfFunds": "Salary",
"status": "SCHEDULED",
"createdTime": "2025-12-08T05:53:17.000Z",
"failureReason": null
}
}
Retry Policy
To ensure reliable webhook delivery, our system implements an automatic retry mechanism for failed webhook deliveries.
Retry Mechanism
- Retry Trigger: Webhooks will be retried if your endpoint returns a non-2xx HTTP status code
- Delivery Timeout: Each webhook delivery attempt has a timeout of 30 seconds
- Maximum Retry Duration: The system will continue retrying for up to 1 hour
- Exponential Backoff: The retry strategy uses exponential backoff:
- Initial retry intervals are short after the first failure
- Retry intervals gradually increase with each subsequent failure
- This approach balances quick recovery with system stability
Troubleshooting
Common Issues
- Decryption fails (Wrong encryption key)
Check encryption key matches the one in your dashboard - Decryption fails (Wrong IV)
Verify you're using the IV from the webhook payload - Invalid JSON (Padding issue)
Ensure proper PKCS7 padding removal after decryption - Webhook not received (Firewall/Network)
Ensure your endpoint is publicly accessible and accepts POST requests