Skip to main content

Webhooks

Receive real-time notifications about transaction status changes.

Overview​

Webhooks allow you to receive automatic notifications when transactions change status. This enables you to update your systems in real-time without polling our API.

How Webhooks Work​

  1. Transaction Initiated: You create a collection or disbursement
  2. Status Changes: Transaction progresses through various states
  3. Webhook Sent: GCA Pay sends HTTP POST to your endpoint
  4. Your Response: Your server responds with HTTP 200
  5. Retry Logic: Failed deliveries are retried automatically

Webhook Endpoint Setup​

Configure your webhook URL in the GCA Pay merchant dashboard under Settings > Webhooks.

Requirements​

  • HTTPS: Your endpoint must use HTTPS (SSL/TLS)
  • Response: Must respond with HTTP 200 status code
  • Timeout: Must respond within 30 seconds
  • Content-Type: Must accept application/json

Webhook Payload​

Collection Status Update​

{
"event_type": "collection.status_updated",
"timestamp": "2024-03-15T10:30:00Z",
"transaction": {
"id": "TXN_123456789",
"reference": "GCA_REF_987654321",
"type": "collection",
"amount": "1000",
"currency": "TZS",
"phone_number": "+255683542710",
"status": "SUCCESS",
"previous_status": "PENDING",
"created_at": "2024-03-15T10:25:00Z",
"completed_at": "2024-03-15T10:30:00Z",
"external_id": "YOUR_REF_123",
"network": "airtel_money"
},
"merchant": {
"id": "MERCHANT_123",
"name": "ABC Company Ltd"
}
}

Disbursement Status Update​

{
"event_type": "disbursement.status_updated",
"timestamp": "2024-03-15T10:35:00Z",
"transaction": {
"id": "TXN_987654321",
"reference": "GCA_REF_123456789",
"type": "disbursement",
"amount": "1000",
"currency": "TZS",
"phone_number": "+255683542710",
"status": "SUCCESS",
"previous_status": "PROCESSING",
"created_at": "2024-03-15T10:30:00Z",
"completed_at": "2024-03-15T10:35:00Z",
"external_id": "PAYOUT_456",
"network": "airtel_money"
},
"merchant": {
"id": "MERCHANT_123",
"name": "ABC Company Ltd"
}
}

Event Types​

EventDescription
collection.status_updatedCollection transaction status changed
disbursement.status_updatedDisbursement transaction status changed

Status Values​

Collections​

  • PENDING → Customer has been prompted to pay
  • SUCCESS → Payment completed successfully
  • FAILED → Payment failed or was rejected
  • TIMEOUT → Payment request timed out

Disbursements​

  • PROCESSING → Disbursement is being processed
  • SUCCESS → Money sent successfully
  • FAILED → Disbursement failed
  • INSUFFICIENT_FUNDS → Merchant account has insufficient balance

Implementation Examples​

Node.js/Express​

const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

// Webhook endpoint
app.post('/webhooks/gcapay', (req, res) => {
try {
const webhook = req.body;

// Verify webhook signature (recommended)
if (!verifySignature(req)) {
return res.status(401).send('Invalid signature');
}

// Handle the webhook
handleWebhook(webhook);

// Respond with 200 to acknowledge receipt
res.status(200).send('OK');
} catch (error) {
console.error('Webhook error:', error);
res.status(500).send('Internal error');
}
});

function handleWebhook(webhook) {
const { event_type, transaction } = webhook;

switch (event_type) {
case 'collection.status_updated':
handleCollectionUpdate(transaction);
break;

case 'disbursement.status_updated':
handleDisbursementUpdate(transaction);
break;

default:
console.log('Unknown event type:', event_type);
}
}

function handleCollectionUpdate(transaction) {
console.log(`Collection ${transaction.id} status: ${transaction.status}`);

if (transaction.status === 'SUCCESS') {
// Payment successful - update your database
updateOrderStatus(transaction.external_id, 'paid');
sendConfirmationEmail(transaction);
} else if (transaction.status === 'FAILED') {
// Payment failed - handle accordingly
updateOrderStatus(transaction.external_id, 'payment_failed');
notifyCustomer(transaction);
}
}

function handleDisbursementUpdate(transaction) {
console.log(`Disbursement ${transaction.id} status: ${transaction.status}`);

if (transaction.status === 'SUCCESS') {
// Disbursement successful
updatePayoutStatus(transaction.external_id, 'completed');
notifyRecipient(transaction);
} else if (transaction.status === 'FAILED') {
// Disbursement failed
updatePayoutStatus(transaction.external_id, 'failed');
handleFailedPayout(transaction);
}
}

app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});

Python/Flask​

from flask import Flask, request, jsonify
import json
import hmac
import hashlib

app = Flask(__name__)

@app.route('/webhooks/gcapay', methods=['POST'])
def handle_webhook():
try:
webhook_data = request.get_json()

# Verify webhook signature (recommended)
if not verify_signature(request):
return jsonify({'error': 'Invalid signature'}), 401

# Handle the webhook
handle_webhook_data(webhook_data)

# Return 200 to acknowledge receipt
return jsonify({'status': 'success'}), 200

except Exception as e:
print(f"Webhook error: {e}")
return jsonify({'error': 'Internal error'}), 500

def handle_webhook_data(webhook):
event_type = webhook.get('event_type')
transaction = webhook.get('transaction')

if event_type == 'collection.status_updated':
handle_collection_update(transaction)
elif event_type == 'disbursement.status_updated':
handle_disbursement_update(transaction)
else:
print(f"Unknown event type: {event_type}")

def handle_collection_update(transaction):
print(f"Collection {transaction['id']} status: {transaction['status']}")

if transaction['status'] == 'SUCCESS':
# Payment successful
update_order_status(transaction['external_id'], 'paid')
send_confirmation_email(transaction)
elif transaction['status'] == 'FAILED':
# Payment failed
update_order_status(transaction['external_id'], 'payment_failed')
notify_customer(transaction)

def handle_disbursement_update(transaction):
print(f"Disbursement {transaction['id']} status: {transaction['status']}")

if transaction['status'] == 'SUCCESS':
# Disbursement successful
update_payout_status(transaction['external_id'], 'completed')
notify_recipient(transaction)
elif transaction['status'] == 'FAILED':
# Disbursement failed
update_payout_status(transaction['external_id'], 'failed')
handle_failed_payout(transaction)

if __name__ == '__main__':
app.run(debug=True, port=3000)

PHP​

<?php

// Handle webhook POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input = file_get_contents('php://input');
$webhook = json_decode($input, true);

if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON']);
exit;
}

try {
// Verify webhook signature (recommended)
if (!verifySignature($_SERVER['HTTP_SIGNATURE'] ?? '', $input)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}

// Handle the webhook
handleWebhook($webhook);

// Return 200 to acknowledge receipt
http_response_code(200);
echo json_encode(['status' => 'success']);

} catch (Exception $e) {
error_log("Webhook error: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Internal error']);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}

function handleWebhook($webhook) {
$eventType = $webhook['event_type'] ?? '';
$transaction = $webhook['transaction'] ?? [];

switch ($eventType) {
case 'collection.status_updated':
handleCollectionUpdate($transaction);
break;

case 'disbursement.status_updated':
handleDisbursementUpdate($transaction);
break;

default:
error_log("Unknown event type: $eventType");
}
}

function handleCollectionUpdate($transaction) {
$id = $transaction['id'];
$status = $transaction['status'];

error_log("Collection $id status: $status");

if ($status === 'SUCCESS') {
// Payment successful
updateOrderStatus($transaction['external_id'], 'paid');
sendConfirmationEmail($transaction);
} elseif ($status === 'FAILED') {
// Payment failed
updateOrderStatus($transaction['external_id'], 'payment_failed');
notifyCustomer($transaction);
}
}

function handleDisbursementUpdate($transaction) {
$id = $transaction['id'];
$status = $transaction['status'];

error_log("Disbursement $id status: $status");

if ($status === 'SUCCESS') {
// Disbursement successful
updatePayoutStatus($transaction['external_id'], 'completed');
notifyRecipient($transaction);
} elseif ($status === 'FAILED') {
// Disbursement failed
updatePayoutStatus($transaction['external_id'], 'failed');
handleFailedPayout($transaction);
}
}

?>

Security​

Webhook Signatures​

Verify webhook authenticity using HMAC signatures:

function verifySignature(req) {
const signature = req.headers['x-gcapay-signature'];
const payload = JSON.stringify(req.body);
const secret = process.env.GCAPAY_WEBHOOK_SECRET;

const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');

return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}

Best Practices​

  • Verify Signatures: Always verify webhook signatures
  • Idempotency: Handle duplicate webhooks gracefully
  • Rate Limiting: Implement rate limiting on webhook endpoints
  • Logging: Log all webhook receipts for debugging
  • Error Handling: Return proper HTTP status codes

Testing Webhooks​

Local Development​

Use tools like ngrok to expose your local server:

# Install ngrok
npm install -g ngrok

# Expose local port 3000
ngrok http 3000

# Use the HTTPS URL in your webhook settings
# https://abc123.ngrok.io/webhooks/gcapay

Webhook Testing Tools​

  1. Postman: Create test requests to your webhook endpoint
  2. Webhook.site: Generate test URLs for webhook testing
  3. RequestBin: Capture and inspect webhook requests

Retry Logic​

GCA Pay automatically retries failed webhook deliveries:

  • Initial Retry: 30 seconds after failure
  • Subsequent Retries: Exponential backoff (1min, 5min, 15min, 1hr)
  • Max Retries: 5 attempts over 24 hours
  • Timeout: 30 seconds per attempt

Failure Scenarios​

Webhooks are retried when:

  • HTTP status code is not 200
  • Request times out (>30 seconds)
  • Connection refused or network error
  • SSL/TLS certificate issues

Common Issues​

Webhook Not Received​

  1. Check URL: Ensure webhook URL is correct and accessible
  2. Verify HTTPS: Webhooks only sent to HTTPS endpoints
  3. Check Firewall: Ensure your server accepts incoming requests
  4. Review Logs: Check application and server logs for errors

Duplicate Webhooks​

Handle duplicate webhooks using idempotency:

const processedWebhooks = new Set();

function handleWebhook(webhook) {
const webhookId = `${webhook.transaction.id}_${webhook.timestamp}`;

if (processedWebhooks.has(webhookId)) {
console.log('Duplicate webhook ignored:', webhookId);
return;
}

processedWebhooks.add(webhookId);

// Process the webhook
processWebhookData(webhook);
}

Next Steps​