Handling Callbacks
Learn how to receive and handle callbacks for pay-in and payout events.
When a pay-in or payout transaction is processed, our system will send a callback (also known as a webhook) to a URL that you provide in your initial request. This callback informs your application about the final status of the transaction, whether it was successful or failed.
Callback Verification
To ensure that callbacks received by your server are genuinely from our Gateway and haven't been tampered with, we include a cryptographic signature in the request headers.
Authentication Mechanism
We use HMAC-SHA256 to sign the callback data. Every callback request includes the following header:
| Header | Description |
|---|---|
X-Verification-Token | The HMAC-SHA256 signature generated by the Gateway |
Verification Process
- Retrieve your Webhook Secret
Contact the support team to obtain your Webhook Secret. Keep this secret secure and never expose it in client-side code or public repositories.
- Extract data from the callback body
Parse the JSON payload and extract three fields:
payment_id,amount, andstatus.Ensure
amountis treated as a string exactly as it appears in the JSON. If it arrives as a number, convert it to match the Gateway's string representation. - Construct the message
Concatenate the fields with a colon (
:) separator in this exact order:payment_id:amount:statusFor example:
pay_123456:100.00:SUCCESS - Compute the signature
Create an HMAC-SHA256 hash of the constructed message using your Webhook Secret as the key, then convert it to a hexadecimal digest.
- Compare signatures
The hex digest of your computed hash must match the
X-Verification-Tokenheader value. Use a timing-safe comparison function to prevent timing attacks.If the signatures match, the callback is authentic and can be processed safely.
Implementation Examples
import hmac
import hashlib
def verify_callback(request, webhook_secret):
# 1. Extract header and body
token = request.headers.get('X-Verification-Token')
data = request.json # Assuming JSON body is parsed
payment_id = data.get('payment_id')
amount = str(data.get('amount')) # Ensure string format
status = data.get('status')
if not token or not all([payment_id, amount, status]):
return False # Missing data
# 2. Construct message
message = f"{payment_id}:{amount}:{status}"
# 3. Compute expected signature
expected_token = hmac.new(
webhook_secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
# 4. Secure compare
return hmac.compare_digest(token, expected_token)const crypto = require('crypto');
function verifyCallback(req, webhookSecret) {
// 1. Extract header and body
const token = req.headers['x-verification-token'];
const { payment_id, amount, status } = req.body;
if (!token || !payment_id || amount === undefined || !status) {
return false; // Missing data
}
// 2. Construct message (ensure amount is string)
const message = `${payment_id}:${String(amount)}:${status}`;
// 3. Compute expected signature
const expectedToken = crypto
.createHmac('sha256', webhookSecret)
.update(message)
.digest('hex');
// 4. Secure compare (constant time)
const tokenBuffer = Buffer.from(token, 'utf8');
const expectedBuffer = Buffer.from(expectedToken, 'utf8');
if (tokenBuffer.length !== expectedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(tokenBuffer, expectedBuffer);
}<?php
function verify_callback($headers, $body_json, $webhook_secret) {
// 1. Extract header
$token = $headers['X-Verification-Token'] ?? '';
// Parse body
$data = json_decode($body_json, true);
$payment_id = $data['payment_id'] ?? '';
$amount = (string)($data['amount'] ?? '');
$status = $data['status'] ?? '';
if (!$token || !$payment_id || !$amount || !$status) {
return false;
}
// 2. Construct message
$message = "{$payment_id}:{$amount}:{$status}";
// 3. Compute expected signature
$expected_token = hash_hmac('sha256', $message, $webhook_secret);
// 4. Secure compare
return hash_equals($expected_token, $token);
}
?>IP Whitelisting (Deprecated)
Deprecated: IP whitelisting should no longer be used as a verification method. Use the HMAC signature verification method above instead. This provides stronger security and protection against spoofed callbacks.
Setting Up a Local Tunnel for Testing
To test callbacks during local development, your application running on localhost needs to be accessible from the public internet. Tools like ngrok can create a secure tunnel to your local machine.
- Install ngrok
Follow the installation instructions on the ngrok website.
- Run Your Local Server
Start the web server for your application on a specific port (e.g.,
8000). - Start ngrok
Open a new terminal window and start ngrok to forward a public URL to your local port.
ngrok http 8000 - Use the Forwarding URL
Ngrok will provide you with a public URL (e.g.,
https://random-string.ngrok.io). Use this URL as thecallback_urlwhen you make a pay-in or payout request. All callbacks will now be sent to your local application.
Callback Requests
We will send a POST request to your specified callback URL with a JSON body containing the transaction details.
If your callback URL does not respond with a 200 status code, we will retry the request up to 3 times, with a 30-second interval between each attempt. We only consider the callback successfully delivered upon receiving a 200 response.
POST /your-callback-endpoint HTTP/1.1
Host: your-domain.com
Content-Type: application/json
User-Agent: Payzio-Callback-Service
X-Verification-Token: 8f3b2c1a5d6e7f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3
{
"amount": 500,
"utr": "TESTUTR123",
"payment_id": "GYrQ1SrDMF8awMDqgkl7Brw1uG2zqkq9",
"status": "SUCCESS"
}Pay-in Callbacks
Here are the possible request bodies for a pay-in callback.
SUCCESS
{
"amount": 500,
"utr": "TESTUTR123",
"payment_id": "GYrQ1SrDMF8awMDqgkl7Brw1uG2zqkq9",
"status": "SUCCESS"
}FAILED
{
"amount": 500,
"utr": "TESTUTR123",
"payment_id": "g9RUutDeYmxIreY3Xw4tieKVS6eZqRuR",
"status": "FAILED"
}Payout Callbacks
Here are the possible request bodies for a payout callback.
SUCCESS
{
"amount": 1,
"utr": "TESTUTR456",
"payment_id": "WDrimcTVug0xnuck5ljtJTFRjgfNlIxT",
"status": "SUCCESS",
}FAILED
{
"amount": 1,
"utr": "TESTUTR456",
"payment_id": "W9nPAQY60yaF3wqjz4giNR4xn78oZkHP",
"status": "FAILED",
}