Webhooks
Receive real-time notifications for transaction status changes using Click Airtime webhooks.
Webhooks are only available in the Version 2 API. The Version 1 API does not support webhooks — use polling via GET /adp/transactions instead.
Webhooks allow your application to receive real-time HTTP callbacks when transaction statuses change. Instead of polling the API for updates, your server is notified automatically.
How Webhooks Work
1. Your app creates a transaction via the API
2. Click Airtime processes the transaction
3. When the status changes, we send an HTTP POST to your webhook URL
4. Your server acknowledges receipt with a 200 response
Setting Up Webhooks
Register a Webhook URL
Configure your webhook endpoint in the Enterprise dashboard under Settings > Webhooks, or via the API:
curl -X POST https://api.clickairtime.com/enterprise/webhooks \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-Company-ID: YOUR_COMPANY_ID" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/clickairtime",
"events": ["transaction.completed", "transaction.failed"],
"secret": "whsec_your_signing_secret"
}'
Supported Events
| Event | Description |
|---|---|
transaction.completed | Airtime was successfully delivered to the recipient |
transaction.failed | Transaction failed after all retry attempts |
transaction.pending | Transaction is awaiting processing |
transaction.refunded | Transaction was refunded to the wallet |
wallet.low_balance | Wallet balance dropped below the configured threshold |
Webhook Payload
When an event occurs, Click Airtime sends an HTTP POST request to your configured URL with a JSON payload:
{
"id": "evt_a1b2c3d4e5f6",
"type": "transaction.completed",
"timestamp": "2025-01-15T10:30:02.500Z",
"data": {
"transactionId": "txn_7g8h9i0j1k2l",
"status": "completed",
"recipient": {
"phoneNumber": "+233541112259",
"network": "MTN Ghana",
"country": "Ghana"
},
"product": {
"type": "custom_airtime",
"localAmount": 5.00,
"currency": "GHS"
},
"sourceAmount": 0.34,
"sourceCurrency": "USD",
"idempotencyKey": "txn_abc123_001",
"completedAt": "2025-01-15T10:30:02.500Z"
}
}
Verifying Webhook Signatures
Every webhook request includes a signature in the X-Click-Signature header. Always verify this signature to ensure the request originated from Click Airtime.
The signature is an HMAC-SHA256 hash of the request body using your webhook secret.
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js example
app.post('/webhooks/clickairtime', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-click-signature'];
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
switch (event.type) {
case 'transaction.completed':
handleTransactionCompleted(event.data);
break;
case 'transaction.failed':
handleTransactionFailed(event.data);
break;
}
// Always respond with 200 to acknowledge receipt
res.status(200).send('OK');
});import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = 'whsec_your_signing_secret'
def verify_webhook_signature(payload, signature, secret):
expected = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route('/webhooks/clickairtime', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Click-Signature', '')
payload = request.get_data(as_text=True)
if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
return 'Invalid signature', 401
event = request.get_json()
if event['type'] == 'transaction.completed':
handle_transaction_completed(event['data'])
elif event['type'] == 'transaction.failed':
handle_transaction_failed(event['data'])
# Always respond with 200 to acknowledge receipt
return 'OK', 200$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_CLICK_SIGNATURE'] ?? '';
$secret = getenv('WEBHOOK_SECRET');
$expected = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
echo 'Invalid signature';
exit;
}
$event = json_decode($payload, true);
switch ($event['type']) {
case 'transaction.completed':
handleTransactionCompleted($event['data']);
break;
case 'transaction.failed':
handleTransactionFailed($event['data']);
break;
}
// Always respond with 200 to acknowledge receipt
http_response_code(200);
echo 'OK';Retry Policy
If your webhook endpoint does not respond with a 2xx status code within 10 seconds, Click Airtime will retry the delivery:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 24 hours |
After 5 failed attempts, the webhook delivery is marked as failed. You can view failed deliveries and manually retry them from the Enterprise dashboard under Settings > Webhooks > Delivery Log.
Best Practices
-
Respond quickly -- Return a
200response as soon as you receive the webhook. Process the event asynchronously using a job queue. -
Verify signatures -- Always validate the
X-Click-Signatureheader to prevent spoofing. -
Handle duplicates -- Webhooks may be delivered more than once. Use the event
idfield to deduplicate. -
Use HTTPS -- Your webhook endpoint must use HTTPS in production. HTTP endpoints are only accepted in sandbox mode.
-
Log all events -- Store the raw webhook payload for debugging and audit trails.
-
Monitor delivery failures -- Set up alerts for consecutive webhook delivery failures in your dashboard.
Testing Webhooks: Use the sandbox environment to test webhook delivery without affecting production data. You can also trigger test events from the Enterprise dashboard under Settings > Webhooks > Send Test Event.
