Callbacks
Set up and handle callbacks and webhooks
Callbacks
Callbacks (webhooks) allow you to receive real-time notifications about transaction statuses and other events. This enables you to build responsive applications that react immediately to payment updates.
Overview
When you initiate a payment or send an SMS, you can specify a callback_url where we'll send status updates. This eliminates the need to constantly poll our API for updates.
Setting Up Callbacks
1. Provide Callback URL
Include the callback_url parameter in your API requests:
{
"service_id": 1,
"reference": "Transfer to momo",
"customer_number": "0541234567",
"transaction_id": "79",
"callback_url": "https://your-domain.com/webhook/payment-status"
}2. Make Your Endpoint Accessible
Your callback URL must be:
- Publicly accessible (not behind authentication)
- Respond with HTTP 200 status
- Handle POST requests
- Respond within 30 seconds
3. Handle the Callback
Process the incoming callback data and respond appropriately.
Callback Format
All callbacks are sent as POST requests with JSON payloads:
{
"transaction_id": "79",
"status": "000",
"status_desc": "Transaction Successful",
"network_transaction_id": "57537797464",
"message": "SUCCESSFUL",
"amount": 0.1,
"currency_code": "GHS",
"customer_number": "0541234567",
"reference": "Transfer to momo",
"timestamp": "2025-08-31T23:45:00Z"
}Callback Fields
| Field | Type | Description |
|---|---|---|
transaction_id | string | Your original transaction ID |
status | string | Transaction status code |
status_desc | string | Human-readable status description |
network_transaction_id | string | Network's transaction ID |
message | string | Status message |
amount | number | Transaction amount |
currency_code | string | Currency code |
customer_number | string | Customer's phone number |
reference | string | Your reference |
timestamp | string | ISO 8601 timestamp |
Status Codes
| Code | Description | Meaning |
|---|---|---|
000 | Transaction Successful | Payment completed successfully |
001 | Transaction Failed | Payment failed |
002 | Transaction Pending | Payment still processing |
003 | Transaction Cancelled | Payment was cancelled |
Implementation Examples
Node.js/Express
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook/payment-status', (req, res) => {
const { transaction_id, status, status_desc, amount } = req.body;
console.log(`Transaction ${transaction_id}: ${status_desc}`);
if (status === '000') {
// Payment successful
handleSuccessfulPayment(transaction_id, amount);
} else if (status === '001') {
// Payment failed
handleFailedPayment(transaction_id, status_desc);
}
// Always respond with 200
res.status(200).send('OK');
});
function handleSuccessfulPayment(transactionId, amount) {
// Update your database
// Send confirmation email
// Update order status
console.log(`Payment ${transactionId} successful: ${amount}`);
}
function handleFailedPayment(transactionId, reason) {
// Log the failure
// Notify customer
// Update order status
console.log(`Payment ${transactionId} failed: ${reason}`);
}
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});Python/Flask
from flask import Flask, request, jsonify
import json
app = Flask(__name__)
@app.route('/webhook/payment-status', methods=['POST'])
def payment_webhook():
data = request.get_json()
transaction_id = data.get('transaction_id')
status = data.get('status')
status_desc = data.get('status_desc')
amount = data.get('amount')
print(f"Transaction {transaction_id}: {status_desc}")
if status == '000':
handle_successful_payment(transaction_id, amount)
elif status == '001':
handle_failed_payment(transaction_id, status_desc)
return jsonify({'status': 'received'}), 200
def handle_successful_payment(transaction_id, amount):
# Update database
# Send confirmation email
# Update order status
print(f"Payment {transaction_id} successful: {amount}")
def handle_failed_payment(transaction_id, reason):
# Log failure
# Notify customer
# Update order status
print(f"Payment {transaction_id} failed: {reason}")
if __name__ == '__main__':
app.run(host='0.0.0.0', port=3000)PHP
<?php
// webhook/payment-status.php
// Get the raw POST data
$input = file_get_contents('php://input');
$data = json_decode($input, true);
$transaction_id = $data['transaction_id'];
$status = $data['status'];
$status_desc = $data['status_desc'];
$amount = $data['amount'];
error_log("Transaction {$transaction_id}: {$status_desc}");
if ($status === '000') {
handle_successful_payment($transaction_id, $amount);
} elseif ($status === '001') {
handle_failed_payment($transaction_id, $status_desc);
}
// Always return 200
http_response_code(200);
echo json_encode(['status' => 'received']);
function handle_successful_payment($transaction_id, $amount) {
// Update database
// Send confirmation email
// Update order status
error_log("Payment {$transaction_id} successful: {$amount}");
}
function handle_failed_payment($transaction_id, $reason) {
// Log failure
// Notify customer
// Update order status
error_log("Payment {$transaction_id} failed: {$reason}");
}
?>Security Considerations
1. Verify Callback Source
While callbacks come from our servers, you should implement additional verification:
app.post('/webhook/payment-status', (req, res) => {
// Verify the request is from Bridge
const signature = req.headers['x-bridge-signature'];
const payload = JSON.stringify(req.body);
if (!verifySignature(payload, signature)) {
return res.status(401).send('Unauthorized');
}
// Process the callback
processCallback(req.body);
res.status(200).send('OK');
});2. Idempotency
Handle duplicate callbacks gracefully:
const processedTransactions = new Set();
app.post('/webhook/payment-status', (req, res) => {
const { transaction_id } = req.body;
// Check if already processed
if (processedTransactions.has(transaction_id)) {
return res.status(200).send('Already processed');
}
// Process the callback
processCallback(req.body);
processedTransactions.add(transaction_id);
res.status(200).send('OK');
});3. Timeout Handling
Implement proper timeout handling:
app.post('/webhook/payment-status', (req, res) => {
// Set timeout
req.setTimeout(25000); // 25 seconds
try {
processCallback(req.body);
res.status(200).send('OK');
} catch (error) {
console.error('Callback processing error:', error);
res.status(500).send('Processing error');
}
});Testing Callbacks
1. Use ngrok for Local Development
# Install ngrok
npm install -g ngrok
# Expose your local server
ngrok http 3000
# Use the ngrok URL as your callback_url
# https://abc123.ngrok.io/webhook/payment-status2. Test with Webhook.site
# Visit webhook.site to get a test URL
# Use the provided URL as your callback_url
curl -X POST "https://api.bridgeagw.com/make_payment" \
-H "Authorization: Basic <your_credentials>" \
-H "Content-Type: application/json" \
-d '{
"service_id": 1,
"callback_url": "https://webhook.site/your-unique-id",
"transaction_id": "test-123"
}'3. Mock Callback for Testing
// Mock callback for testing
const mockCallback = {
transaction_id: "test-123",
status: "000",
status_desc: "Transaction Successful",
network_transaction_id: "57537797464",
message: "SUCCESSFUL",
amount: 0.1,
currency_code: "GHS",
customer_number: "0541234567",
reference: "Test Payment",
timestamp: new Date().toISOString()
};
// Test your callback handler
processCallback(mockCallback);Retry Policy
If your callback endpoint doesn't respond with HTTP 200, we'll retry:
- Retry Count: 3 attempts
- Retry Interval: 5 minutes between attempts
- Timeout: 30 seconds per attempt
After 3 failed attempts, the callback will be marked as failed and won't be retried.
Best Practices
- Always Respond with 200: Even if processing fails, respond with HTTP 200 to prevent retries
- Process Asynchronously: Don't block the callback response with heavy processing
- Log Everything: Log all callbacks for debugging and auditing
- Handle Duplicates: Implement idempotency to handle duplicate callbacks
- Validate Data: Always validate the callback data before processing
- Monitor Performance: Monitor your callback endpoint performance
Troubleshooting
Common Issues
- Callback Not Received: Check if your endpoint is accessible and responding with 200
- Timeout Errors: Ensure your endpoint responds within 30 seconds
- Duplicate Callbacks: Implement idempotency to handle duplicates
- Processing Errors: Log errors and handle them gracefully
Debugging
// Add comprehensive logging
app.post('/webhook/payment-status', (req, res) => {
console.log('Callback received:', {
headers: req.headers,
body: req.body,
timestamp: new Date().toISOString()
});
try {
processCallback(req.body);
res.status(200).send('OK');
} catch (error) {
console.error('Callback processing error:', error);
res.status(200).send('Error logged'); // Still return 200
}
});