Webhooks & Callbacks
The Direct Payments API pushes real-time transaction updates to your server via HTTPS POST webhooks.
You will receive a callback the moment an STK Push or B2C transaction reaches its final status.
| Requirement | Details |
|---|---|
| HTTPS only | Must have a valid SSL certificate (Let’s Encrypt is fine) |
| Publicly accessible | Must be reachable from the internet |
| Accepts POST | JSON payload in request body |
| Responds quickly | Return 200 OK within 10 seconds (process asynchronously) |
| Idempotent | Safely handle duplicate callbacks (same payment_id) |
- Whitelist our callback IPs (mandatory for production)
- Verify the incoming IP is in the allowed list
- (Optional) Verify signature header when we roll it out
- Never trust the payload without verifying the source IP
216.219.95.54196.201.214.206196.201.214.207
216.219.95.54
| Field | Description |
|---|---|
payment_id | Unique ID you received when initiating the request |
status | Final status (Success, Failed, Cancelled, Timeout) |
timestamp | ISO timestamp of the callback |
{"payment_id": "550e8400-e29b-41d4-a716-446655440000","merchant_request_id": "MERCHANT-20251120-001","checkout_request_id": "ws_CO_20112025143022","status": "Success","result_code": "0","result_description": "The service request is processed successfully.","initial_amount": 1250.00,"actual_amount": 1250.00,"mpesa_receipt_number": "SKL9P2M4XQ","transaction_date": "20251120143245","phone_number": "254708374149","payer_phone": "254708374149","payer_name": "JOHN DOE","account_reference": "INV-2025-0891","metadata": {"customer_id": "CUST-8871","plan": "premium-annual"},"timestamp": "2025-11-20 14:32:50"}
| Status | Action Required |
|---|---|
Success | Credit user, fulfill order, send confirmation |
Failed | Show error message, allow retry |
Cancelled | User cancelled – prompt to retry |
Timeout | No response – prompt to retry |
{"payment_id": "550e8400-e29b-41d4-a716-446655440001","conversation_id": "AG_20251120_000123456789","originator_conversation_id": "OC_20251120_987654321","transaction_type": "b2c","status": "Success","result_code": "0","result_description": "The service request is processed successfully.","transaction_id": "RKJ3M9P2XQ","amount": 2500.00,"recipient_phone": "254708374149","recipient_name": "Jane Doe","command_id": "BusinessPayment","charges": {"charges_paid": 125.00,"utility_balance": 987500.00,"working_balance": 862375.00},"timestamp": "2025-11-20 14:30:50"}
Always store payment_id and check before processing:
// Pseudo-codeif (webhookAlreadyProcessed(payment_id)) {return res.status(200).json({ message: "Already processed" });}processTransaction(data);markAsProcessed(payment_id);res.status(200).json({ message: "Received" });
const express = require('express');const app = express();app.use(express.json());const ALLOWED_IPS = ['196.201.214.200', '196.201.214.206', /* ... all IPs */];app.post('/webhook/payments', (req, res) => {const ip = req.ip || req.connection.remoteAddress;if (!ALLOWED_IPS.includes(ip)) {return res.status(403).send('Forbidden');}const payload = req.body;// Log for debuggingconsole.log('Webhook received:', payload);// Fire-and-forget processingprocessWebhookAsync(payload);// Immediate acknowledgmentres.status(200).json({ status: 'received' });});app.listen(3000);
<?php// routes/webhook.php$allowedIps = ['196.201.214.200', '196.201.214.206', /* ... */];$ip = $_SERVER['REMOTE_ADDR'];if (!in_array($ip, $allowedIps)) {http_response_code(403);exit('Forbidden');}$payload = file_get_contents('php://input');$data = json_decode($payload, true);// Logfile_put_contents(storage_path('logs/webhooks.log'), now() . ' ' . $payload . PHP_EOL, FILE_APPEND);// Prevent duplicatesif (Webhook::where('payment_id', $data['payment_id'])->exists()) {http_response_code(200);echo json_encode(['status' => 'already_processed']);exit;}// Process asynchronously (queue, job, etc.)ProcessPaymentCallback::dispatch($data);http_response_code(200);echo json_encode(['status' => 'queued']);
You’re now fully equipped to receive and securely process real-time payment notifications!
Previous: STK Push • B2C Deposits • Next: Testing & Go-Live Checklist