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​
- Transaction Initiated: You create a collection or disbursement
- Status Changes: Transaction progresses through various states
- Webhook Sent: GCA Pay sends HTTP POST to your endpoint
- Your Response: Your server responds with HTTP 200
- 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​
Event | Description |
---|---|
collection.status_updated | Collection transaction status changed |
disbursement.status_updated | Disbursement transaction status changed |
Status Values​
Collections​
PENDING
→ Customer has been prompted to paySUCCESS
→ Payment completed successfullyFAILED
→ Payment failed or was rejectedTIMEOUT
→ Payment request timed out
Disbursements​
PROCESSING
→ Disbursement is being processedSUCCESS
→ Money sent successfullyFAILED
→ Disbursement failedINSUFFICIENT_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​
- Postman: Create test requests to your webhook endpoint
- Webhook.site: Generate test URLs for webhook testing
- 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​
- Check URL: Ensure webhook URL is correct and accessible
- Verify HTTPS: Webhooks only sent to HTTPS endpoints
- Check Firewall: Ensure your server accepts incoming requests
- 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​
- Collections API → - Accept customer payments
- Disbursements API → - Send money to recipients
- Transaction Inquiry → - Query transaction status