Skip to content

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:

  1. Per-merchant-channel (recommended) — set once from the merchant dashboard, applies to every transaction on that channel.
  2. Per-request — pass callback_url in the /inbound body. 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 POST from 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 Failed and 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.0
  • X-Moko-Signature — HMAC-SHA256 of the request body signed with your merchant_secret.
  • X-Moko-Timestamp — Unix seconds. Reject callbacks older than 5 minutes to prevent replay attacks.
  • X-Moko-Reference — same as reference in the body, provided in a header for quick logging / correlation without parsing the body.

Payload

json
{
  "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 beneficiary
  • Failed — telco declined (insufficient balance on our end, telco outage, blocked beneficiary, etc.); wallet is auto-refunded
  • Reversed — post-successful reversal (rare; forces a follow-up callback)

Signature verification (Python)

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)

javascript
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.

Operated by SODOTECH SARL (RCCM CD/KNG/RCCM/24-B-00910). Documentation is versioned alongside the API. Confidential — for integration partners.