Payzio API Docs

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:

HeaderDescription
X-Verification-TokenThe HMAC-SHA256 signature generated by the Gateway

Verification Process

  1. 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.

  2. Extract data from the callback body

    Parse the JSON payload and extract three fields: payment_id, amount, and status.

    Ensure amount is 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.

  3. Construct the message

    Concatenate the fields with a colon (:) separator in this exact order:

    payment_id:amount:status

    For example: pay_123456:100.00:SUCCESS

  4. 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.

  5. Compare signatures

    The hex digest of your computed hash must match the X-Verification-Token header 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.

  1. Install ngrok

    Follow the installation instructions on the ngrok website.

  2. Run Your Local Server

    Start the web server for your application on a specific port (e.g., 8000).

  3. Start ngrok

    Open a new terminal window and start ngrok to forward a public URL to your local port.

    ngrok http 8000
  4. Use the Forwarding URL

    Ngrok will provide you with a public URL (e.g., https://random-string.ngrok.io). Use this URL as the callback_url when 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",
}

On this page