Callbacks
The /inbound and /outbound endpoints are async. The API returns 202 Accepted immediately with a transaction_id and Submitted status. The final outcome (Successful or Failed) is delivered later via webhook.
Configuring your callback URL
Two ways, per priority:
- Per-merchant-channel (recommended) — set once from the merchant dashboard, applies to every transaction on that channel.
- Per-request — pass
callback_urlin the/inboundbody. Overrides the per-channel value for that transaction only.
If neither is set, Moko queues callbacks internally but they are not delivered until you configure one.
Delivery
- HTTP
POSTfrom Moko to your configured URL - Retries: up to 5 attempts with exponential backoff (1s, 5s, 25s, 2min, 10min)
- After 5 failed attempts, the callback is marked
Failedand stops. Replay is possible via the merchant dashboard. - TLS 1.2+ required on your endpoint. Plain HTTP callbacks fail.
- Timeout: 15 seconds. If your endpoint takes longer, the callback is retried.
Headers
Content-Type: application/json
X-Moko-Signature: sha256=<hmac>
X-Moko-Timestamp: <unix-seconds>
X-Moko-Reference: <your reference>
User-Agent: Moko-Afrika-Webhook/1.0X-Moko-Signature— HMAC-SHA256 of the request body signed with yourmerchant_secret.X-Moko-Timestamp— Unix seconds. Reject callbacks older than 5 minutes to prevent replay attacks.X-Moko-Reference— same asreferencein the body, provided in a header for quick logging / correlation without parsing the body.
Payload
{
"reference": "TXN_001",
"transaction_id": "IMT-IN-20260814-A1B2C3-D4E5",
"status": "Successful",
"operator": "mpesa",
"amount": 10.00,
"currency": "USD",
"customer_number": "243812345678",
"subscriber_name": "MARIE KABILA",
"telco_reference": "MPESA-XY7ZW",
"created_at": "2026-08-14T14:00:00.000000",
"completed_at": "2026-08-14T14:00:23.000000"
}Possible status values:
Successful— money delivered to beneficiaryFailed— telco declined (insufficient balance on our end, telco outage, blocked beneficiary, etc.); wallet is auto-refundedReversed— post-successful reversal (rare; forces a follow-up callback)
Signature verification (Python)
import hmac, hashlib, time
def verify_moko_callback(body: bytes, headers: dict, merchant_secret: str) -> bool:
sig = headers.get("X-Moko-Signature", "")
timestamp = headers.get("X-Moko-Timestamp", "")
# 1. Reject old callbacks (replay window)
if abs(time.time() - int(timestamp)) > 300:
return False
# 2. Recompute signature
expected = "sha256=" + hmac.new(
merchant_secret.encode(),
body,
hashlib.sha256,
).hexdigest()
# 3. Constant-time compare
return hmac.compare_digest(sig, expected)Signature verification (Node.js)
import crypto from 'node:crypto';
export function verifyMokoCallback(body, headers, merchantSecret) {
const sig = headers['x-moko-signature'] || '';
const timestamp = headers['x-moko-timestamp'] || '';
// 1. Reject old callbacks
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
// 2. Recompute signature
const expected =
'sha256=' +
crypto.createHmac('sha256', merchantSecret).update(body).digest('hex');
// 3. Constant-time compare
return crypto.timingSafeEqual(
Buffer.from(sig),
Buffer.from(expected),
);
}Idempotency on your side
Your callback endpoint must be idempotent. Moko may deliver the same callback twice under rare race conditions (a slow retry that arrives after a manual replay). Use the transaction_id as the dedup key and short-circuit on second delivery.
Replaying a callback
From the merchant dashboard, open the transaction detail and click Replay callback. This resends the callback synchronously (waits for your endpoint) and returns the HTTP code your endpoint responded with. Useful for validating a fix without generating fresh test volume.
