Webhooks

LIVE
Setting Up

Your webhook URL is configured on your API key. Go to your dashboard to set or update it.

Delivery

Webhooks are sent as POST requests with a JSON body. Expect a response time of under 10 seconds.

Webhook Format

All webhooks follow a standard structure. The plan or item field depends on the purchase type.

Successful Purchase
{
  "event": "data.purchase",
  "reference_code": "TRXDATA20260615100238ONI",
  "status": "successful",
  "message": "Data purchase completed.",
  "plan": "MTN 1GB Monthly",
  "recipient": "08031234567",
  "amount": 500.00,
  "completed_at": "2026-06-15T10:05:00+01:00"
}
Failed Purchase
{
  "event": "data.purchase",
  "reference_code": "TRXDATA20260615100238ONI",
  "status": "failed",
  "message": "Transaction failed. Your wallet has been refunded.",
  "plan": "MTN 1GB Monthly",
  "recipient": "08031234567",
  "amount": 500.00,
  "completed_at": "2026-06-15T10:05:00+01:00"
}
With Token or Pin

For electricity and exam purchases, a pin field is included when the token or pin is generated:

{
  "event": "electricity.purchase",
  "reference_code": "TRXEE20260615104818CQ3",
  "status": "successful",
  "message": "Electricity purchase completed.",
  "item": "Ikeja Electricity - IKEDC (PHCN)",
  "recipient": "1234567890",
  "amount": 5000.00,
  "pin": "1234-5678-9012-3456-7890",
  "completed_at": "2026-06-15T10:05:00+01:00"
}
Webhook Fields
Field Type Description
event string Purchase event (e.g. data.purchase)
reference_code string Your transaction reference for status checks
status string successful or failed
message string Human-readable status description
plan string Plan name (for data and tv purchases)
item string Item name (for airtime, electricity, bet, and education purchases)
recipient string Phone number, smartcard, meter number, or customer ID
amount float Amount charged
pin string Token or exam pin (electricity and education only)
completed_at string Timestamp when the transaction was finalised
Webhook Security & Verification

Pairgate provides optional webhook verification to ensure that incoming webhook requests are genuinely from Pairgate and haven't been tampered with during transmission. When enabled, each webhook request includes an HMAC SHA-256 signature that you can verify using your webhook secret.

Enabling Webhook Verification
  1. Navigate to your API Dashboard
  2. Locate the Webhook Security section
  3. Toggle Webhook Verification to ON
  4. Your webhook secret will be generated and displayed only once
  5. Copy and securely store your secret (e.g., in your environment variables)
Webhook Headers

When webhook verification is enabled, each request includes the following additional headers:

Header Description
X-Pairgate-Timestamp Unix timestamp (in seconds) when the webhook was sent
X-Pairgate-Signature HMAC SHA-256 signature of the payload
How Signature Verification Works
  1. Concatenate the timestamp and raw JSON payload with a dot (`.`)
  2. Calculate the HMAC SHA-256 hash using your webhook secret as the key
  3. Compare your calculated signature with the X-Pairgate-Signature header
  4. Use constant-time comparison to prevent timing attacks
signed_payload = timestamp + "." + raw_json_payload
signature = HMAC_SHA256(secret, signed_payload)
Verification Examples
PHP
<?php

$secret = 'PG_secrete_YOUR_SECRET_HERE';
$timestamp = $_SERVER['HTTP_X_PAIRGATE_TIMESTAMP'];
$providedSignature = $_SERVER['HTTP_X_PAIRGATE_SIGNATURE'];
$rawPayload = file_get_contents('php://input');

// Optional: Verify timestamp is within 5 minutes
if (abs(time() - (int)$timestamp) > 300) {
    http_response_code(400);
    exit('Webhook expired');
}

// Calculate expected signature
$signedPayload = $timestamp . '.' . $rawPayload;
$expectedSignature = hash_hmac('sha256', $signedPayload, $secret);

// Verify using constant-time comparison
if (hash_equals($expectedSignature, $providedSignature)) {
    // Webhook is authentic - process it
    $payload = json_decode($rawPayload, true);
    // Your processing logic here
} else {
    http_response_code(401);
    exit('Invalid signature');
}
Node.js (Express)
const crypto = require('crypto');

app.post('/webhook', (req, res) => {
    const secret = 'PG_secrete_YOUR_SECRET_HERE';
    const timestamp = req.headers['x-pairgate-timestamp'];
    const providedSignature = req.headers['x-pairgate-signature'];
    const rawPayload = JSON.stringify(req.body);

    // Optional: Verify timestamp is within 5 minutes
    if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
        return res.status(400).send('Webhook expired');
    }

    // Calculate expected signature
    const signedPayload = timestamp + '.' + rawPayload;
    const expectedSignature = crypto
        .createHmac('sha256', secret)
        .update(signedPayload)
        .digest('hex');

    // Verify using constant-time comparison
    if (crypto.timingSafeEqual(
        Buffer.from(expectedSignature),
        Buffer.from(providedSignature)
    )) {
        // Webhook is authentic - process it
        res.status(200).send('OK');
    } else {
        res.status(401).send('Invalid signature');
    }
});
Python (Flask)
import hmac
import hashlib
from flask import Flask, request

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    secret = 'PG_secrete_YOUR_SECRET_HERE'
    timestamp = request.headers.get('X-Pairgate-Timestamp')
    provided_signature = request.headers.get('X-Pairgate-Signature')
    raw_payload = request.get_data().decode('utf-8')

    # Optional: Verify timestamp is within 5 minutes
    import time
    if abs(time.time() - int(timestamp)) > 300:
        return 'Webhook expired', 400

    # Calculate expected signature
    signed_payload = timestamp + '.' + raw_payload
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        signed_payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    # Verify using constant-time comparison
    if hmac.compare_digest(expected_signature, provided_signature):
        return 'OK', 200
    else:
        return 'Invalid signature', 401
Best Practices
  1. Store your secret securely - Use environment variables or secure secret management
  2. Always verify signatures - Verify authenticity before processing
  3. Check timestamps - Reject webhooks older than 5 minutes to prevent replay attacks
  4. Use constant-time comparison - Use hash_equals (PHP), timingSafeEqual (Node.js), or compare_digest (Python)
  5. Respond quickly - Return a 200 OK as soon as possible
  6. Store raw payload - Keep original payload for debugging and audit
Troubleshooting
Issue Solution
Signature mismatch Ensure you're using the raw JSON payload exactly as received
Missing headers Verification may not be enabled in your dashboard
Expired timestamp Check server time synchronization
Lost secret Toggle verification off and on to generate a new secret
Retry Policy

If your server does not return a 2xx response, we retry:

Attempt Delay
1st 5 seconds
2nd 15 seconds
3rd 60 seconds

After 3 failed attempts, the webhook is dropped and logged.

Verifying Webhooks

Use the reference_code to fetch the full transaction status from GET /transaction/status . This serves as a backup if a webhook is missed.