Webhooks

Overview

Webhooks allow your application to receive real-time notifications about transaction status changes. When a PayIn or PayOut transaction is completed or fails, we'll send a POST request to your configured callback URL.

ℹ️ Note: Webhooks are sent asynchronously. Your endpoint should respond with HTTP 200 to acknowledge receipt. We will retry failed deliveries with exponential backoff.
⚠️ Important: Always verify webhook signatures to ensure requests are coming from our servers. Never trust webhook data without verification.

Webhook Structure

All webhooks follow a consistent structure containing the event type, timestamp, and transaction data.

PayIn Webhook Example
{
  "event": "payin.successful",
  "timestamp": 1770994473,
  "data": {
    "transaction_id": "TR260213.145421.B0EE88D4",
    "tx_ref": "unique-tx-ref-123",
    "amount": "1000.00",
    "fee": "25.00",
    "total_amount": "1025.00",
    "currency": "CDF",
    "status": "SUCCESS",
    "channel": "mobile_money",
    "payment_method": "mtn-ci",
    "summary": "Payment received via MTN Mobile Money",
    "failure_reason": null,
    "created_at": "2026-02-13T14:54:21+00:00",
    "updated_at": "2026-02-13T14:54:31+00:00",
    "mobile_money": {
      "phone_number": "0707070707",
      "network": "mtn-ci",
      "mno_transaction_id": "MP240213.1454.A12345"
    },
    "extras": {
      "order_id": "ORD-123",
      "customer_id": "CUST-456"
    },
    "account_balance": {
      "previous_balance": "10000.00",
      "current_balance": "11000.00",
      "balance_type": "AVAILABLE"
    }
  }
}
Card PayIn Webhook Example
{
  "event": "payin.successful",
  "timestamp": 1770994473,
  "data": {
    "transaction_id": "TR260213.145421.C1FF99E5",
    "tx_ref": "unique-tx-ref-456",
    "amount": "500.00",
    "fee": "12.50",
    "total_amount": "512.50",
    "settled_amount": "500.00",
    "currency": "USD",
    "status": "SUCCESS",
    "channel": "card",
    "payment_method": "visa",
    "summary": "Card payment received",
    "failure_reason": null,
    "bearer": "customer",
    "card": {
      "last4": "4242",
      "brand": "VISA",
      "scheme": "visa",
      "issuer": "BNP PARIBAS",
      "expiry_month": "12",
      "expiry_year": "2028",
      "issuer_country": "FR"
    },
    "extras": {
      "order_id": "ORD-123"
    },
    "account_balance": {
      "previous_balance": "1000.00",
      "current_balance": "1500.00",
      "balance_type": "AVAILABLE"
    },
    "created_at": "2026-02-13T14:54:21+00:00",
    "updated_at": "2026-02-13T14:54:31+00:00"
  }
}
PayOut Webhook Example
{
  "event": "payout.successful",
  "timestamp": 1770994473,
  "data": {
    "transaction_id": "TR260213.145421.B0EE88D4",
    "tx_ref": "123kj0e30m2",
    "amount": "100.00",
    "fee": "15.00",
    "total_amount": "115.00",
    "currency": "ZMW",
    "status": "SUCCESS",
    "channel": "mobile_money",
    "payment_method": "airtel",
    "summary": "Transfer completed successfully",
    "failure_reason": null,
    "created_at": "2026-02-13T14:54:21+00:00",
    "updated_at": "2026-02-13T14:54:31+00:00",
    "mobile_money": {
      "phone_number": "260773317519",
      "network": "airtel",
      "mno_transaction_id": "MO240213.1454.B67890"
    },
    "extras": {
      "test": "tes",
      "order_id": "ORD-789"
    },
    "account_balance": {
      "previous_balance": "895.00",
      "current_balance": "780.00",
      "balance_type": "AVAILABLE"
    }
  }
}
Webhook Fields
Field Type Description
event string Event type: payin.successful, payin.failed, payin.pending, payin.cancelled, payout.successful, payout.failed, payout.pending, payout.cancelled
timestamp integer Unix timestamp when webhook was sent
data object Complete transaction details including status, amounts, and metadata
data.card object|null Card details — present only for PayIn transactions paid via card (channel: "card"). null otherwise.
data.card.last4 string Last 4 digits of the card number
data.card.brand string Card network brand (e.g. VISA, MASTERCARD)
data.card.scheme string Card scheme including the card type (e.g. Mastercard credit, Visa debit, Amex credit)
data.card.issuer string Name of the bank or institution that issued the card
data.card.expiry_month string Card expiry month (MM)
data.card.expiry_year string Card expiry year (YYYY)
data.card.issuer_country string ISO 3166-1 alpha-2 country code of the card-issuing bank (e.g. FR, US)

Webhook Events

The following events are sent via webhooks:

Event Description When Triggered
payin.successful PayIn completed successfully Customer payment received and funds credited to your wallet
payin.failed PayIn failed Customer payment failed or was cancelled
payout.successful PayOut completed successfully Funds successfully sent to beneficiary
payout.failed PayOut failed Payout failed (e.g., invalid number, insufficient funds, provider error)
payin.pending PayIn submitted to provider Payment initiated and awaiting provider confirmation
payin.cancelled PayIn cancelled Customer cancelled the payment (e.g., dismissed card form)
payout.pending PayOut submitted to provider Payout initiated and awaiting provider confirmation
payout.cancelled PayOut cancelled Payout was cancelled before processing

POST Resend Webhook

https://genesyspay.com/api/v2/webhooks/resend/{identifier}

Manually trigger a webhook resend for a completed or failed transaction. This is useful if your server was down or failed to process the original webhook.

ℹ️ Note: You can only resend webhooks for transactions with status SUCCESS or FAILED. Pending transactions cannot be resent.
Path Parameters
Parameter Type Required Description
identifier string Required Transaction ID or tx_ref of the transaction
Example Request (using Transaction ID)
curl -X POST https://genesyspay.com/api/v2/webhooks/resend/TR260213.145421.B0EE88D4 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_PRIVATE_KEY"
Example Request (using tx_ref)
curl -X POST https://genesyspay.com/api/v2/webhooks/resend/unique-tx-ref-123 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_PRIVATE_KEY"
Success Response
{
  "status": "success",
  "message": "Webhook resend queued successfully",
  "data": {
    "transaction_id": "TR260213.145421.B0EE88D4",
    "tx_ref": "unique-tx-ref-123"
  }
}
Error Responses
HTTP Status Error Code Description
404 NOT_FOUND Transaction not found or not eligible for webhook resend (must be SUCCESS or FAILED)
422 VALIDATION_ERROR Invalid identifier format
401 UNAUTHORIZED Invalid or missing API key
500 INTERNAL_ERROR Failed to queue webhook resend
Not Found Error Example
{
  "status": "error",
  "message": "Transaction not found or not eligible for webhook resend",
  "error_code": "NOT_FOUND"
}

Webhook Security

To ensure webhooks are coming from our servers and not malicious third parties, we include a signature in the X-Webhook-Signature header.

Signature Verification

The signature is generated using HMAC SHA256 with your webhook secret key:

// PHP Example
$payload = file_get_contents('php://input');
$signature = hash_hmac('sha256', $payload, $your_webhook_secret);
$receivedSignature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

if (!hash_equals($signature, $receivedSignature)) {
    http_response_code(403);
    exit('Invalid signature');
}

$data = json_decode($payload, true);
// Process webhook...
// Node.js Example
const crypto = require('crypto');
const express = require('express');

app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
    const signature = crypto
        .createHmac('sha256', YOUR_WEBHOOK_SECRET)
        .update(req.body)
        .digest('hex');
    
    const receivedSignature = req.headers['x-webhook-signature'];
    
    if (signature !== receivedSignature) {
        return res.status(403).send('Invalid signature');
    }
    
    const data = JSON.parse(req.body.toString());
    // Process webhook...
    res.status(200).send('OK');
});

Implementing Webhook Endpoints

Requirements
  • Endpoint must be publicly accessible via HTTPS
  • Respond with HTTP 200 within 10 seconds
  • Verify webhook signature before processing
  • Handle duplicate webhooks (use tx_ref for idempotency)
  • Process webhooks asynchronously (queue for background processing)
Best Practices
  • Respond Quickly: Return HTTP 200 immediately, then process the webhook data asynchronously
  • Be Idempotent: Use tx_ref to prevent duplicate processing if webhook is resent
  • Verify Signatures: Always verify the X-Webhook-Signature header
  • Log Everything: Keep detailed logs of all webhook receipts for debugging
  • Handle Retries: We retry failed webhooks - ensure your endpoint can handle duplicates
  • Monitor Failures: Use the resend endpoint if you miss webhooks due to downtime
Example Implementation (PHP/Laravel)
public function handleWebhook(Request $request)
{
    // 1. Verify signature
    $signature = hash_hmac('sha256', $request->getContent(), config('services.payment.webhook_secret'));
    if (!hash_equals($signature, $request->header('X-Webhook-Signature', ''))) {
        return response()->json(['error' => 'Invalid signature'], 403);
    }

    // 2. Respond immediately
    // Queue for async processing
    ProcessWebhook::dispatch($request->all());
    
    return response()->json(['status' => 'received'], 200);
}

// In your job/queue handler
public function handle(array $webhookData)
{
    $txRef = $webhookData['data']['tx_ref'];
    $event = $webhookData['event'];
    $status = $webhookData['data']['status'];

    // Check if already processed (idempotency)
    if (WebhookLog::where('tx_ref', $txRef)->exists()) {
        return; // Already processed
    }

    // Log webhook
    WebhookLog::create(['tx_ref' => $txRef, 'event' => $event]);

    // Process based on event
    switch ($event) {
        case 'payin.successful':
            // Update order status, fulfill order, etc.
            // Check card details if card payment
            if ($webhookData['data']['channel'] === 'card' && isset($webhookData['data']['card'])) {
                $card = $webhookData['data']['card'];
                // Store $card['brand'], $card['last4'], etc.
            }
            Order::where('tx_ref', $txRef)->update(['status' => 'paid']);
            break;

        case 'payin.pending':
            // Payment submitted to provider, waiting for confirmation
            Order::where('tx_ref', $txRef)->update(['status' => 'pending']);
            break;

        case 'payin.cancelled':
            // Customer cancelled the payment
            Order::where('tx_ref', $txRef)->update(['status' => 'cancelled']);
            break;
            
        case 'payout.successful':
            // Mark payout as completed
            Payout::where('tx_ref', $txRef)->update(['status' => 'completed']);
            break;

        case 'payout.pending':
            Payout::where('tx_ref', $txRef)->update(['status' => 'pending']);
            break;

        case 'payout.cancelled':
            Payout::where('tx_ref', $txRef)->update(['status' => 'cancelled']);
            break;
            
        case 'payin.failed':
        case 'payout.failed':
            // Handle failure
            $reason = $webhookData['data']['failure_reason'];
            // Log failure, notify user, etc.
            break;
    }
}

Webhook Retry Logic

If your endpoint fails to respond with HTTP 200, we will automatically retry sending the webhook with the following schedule:

Attempt Delay
1st retry 1 minute after initial attempt
2nd retry 5 minutes after 1st retry
3rd retry 15 minutes after 2nd retry
4th retry 1 hour after 3rd retry
5th retry (final) 6 hours after 4th retry
⚠️ Note: After all retries are exhausted, you can manually trigger a resend using the webhook resend endpoint.

Testing Webhooks

During development, you can test webhook handling using these approaches:

1. Use a Tunnel Service

Tools like ngrok allow you to expose your local development server:

ngrok http 8000
# Use the generated HTTPS URL as your callback_url
2. Manual Resend

Create a test transaction in sandbox mode, then use the resend endpoint to trigger webhooks:

curl -X POST https://genesyspay.com/api/v2/webhooks/resend/YOUR_TX_REF \
  -H "Authorization: Bearer YOUR_PRIVATE_KEY"
3. Webhook Testing Tools

Use services like webhook.site or requestbin.com to inspect webhook payloads during development.