Skip to content

Callbacks (Webhooks)

When a transaction reaches a terminal state, the gateway delivers the result to your server via an HTTP callback (webhook) to the resultUrl you provided in the original request.


When Callbacks Are Sent

Status Callback Sent
success Yes — terminal
failed Yes — terminal
pending No — initial state, not terminal

At most one callback attempt is made per transaction — and the attempt itself may be skipped when the per-merchant circuit breaker is open. There is no automatic retry; if delivery fails or is skipped, use the Payment Status endpoints to reconcile (see Delivery Policy).


Callback Flow

The diagram below summarises a single callback delivery — from the moment a transaction reaches a terminal state to the gateway recording the outcome of one delivery attempt.

flowchart TD
    A[Transaction reaches terminal state<br/>status = success or failed] --> CB{Circuit breaker<br/>open for this merchant?}

    CB -->|Yes| K[Delivery skipped<br/>Reconcile via GET /status endpoints]
    CB -->|No| B[Gateway POSTs callback to resultUrl<br/>with X-API-KEY header and JSON body<br/>15-second timeout]

    B --> C[Merchant validates X-API-KEY,<br/>processes payload, returns response]
    C --> D{Merchant response}

    D -->|Any 2xx| E[Delivery recorded as successful<br/>Stop]
    D -->|Non-2xx, timeout,<br/>or no response| J[Delivery recorded as failed<br/>No automatic retry<br/>Reconcile via GET /status endpoints]

Callback Request

POST {resultUrl} HTTP/1.1
Content-Type: application/json
X-API-KEY: <your-brand-api-key>

Headers

Header Description
Content-Type Always application/json
X-API-KEY Your brand's API key. You must validate this header on every incoming callback to confirm the request originates from the PayAlo gateway. See Verifying Callback Authenticity.

Callback Body

The body is the Mmo Transaction — the same shape returned by the Payment Status endpoints. Property names are camelCase; status, type, and flow use the V2 vocabulary listed below.

Same wire format as GET /status/{ref}

Callbacks and the status endpoints serialise the same transaction object. If you already have a deserialiser for the status response, you can reuse it for callbacks.

Top-level fields

Field Type Nullable Description
status string No "pending", "success", or "failed". Only "success" and "failed" ever arrive in a callback.
type string No "payin", "payout", or "tax".
flow string No "direct", "web", "qr", or "push".
gatewayReference string No Unique transaction identifier assigned by the gateway (ULID format).
merchantReference string Yes The merchantReference you submitted. null for push callbacks — provider-initiated transactions have no merchant request to echo.
reconciliationReference string Yes The reconciliationReference you submitted (or the merchantReference fallback for merchant-initiated flows). null for push callbacks.
providerReference string Yes Payment provider's own transaction reference.
party Msisdn Party No End-user details (payer for pay-ins, payee for pay-outs). For push callbacks the values come from the provider.
method string No Payment method key (e.g. "mpesa-ke").
country string No ISO 3166-1 alpha-2 country code.
requestedAmount Money No Amount originally requested.
finalAmount Money Yes Final settled amount. null if the transaction failed before settlement.
labels object Yes Key-value metadata you submitted with the request. null for push callbacks (no merchant request to attach metadata to).
createdAt string No When the transaction was created (ISO 8601 UTC).
completedAt string Yes When the transaction reached a terminal state (ISO 8601 UTC).
completionSource string Yes What resolved the transaction. Known values: "webhook", "poll", "manual". Treat unknown values as informational.
errorCode string Yes Machine-readable async error code when status == "failed".
errorMessage string Yes Human-readable description of the error.
providerData Provider Data Yes Provider-specific details (name, title, fee, native error codes). null if the transaction was not routed to a provider.

Example Callback Body — Successful Pay-In

{
  "status": "success",
  "type": "payin",
  "flow": "direct",
  "gatewayReference": "b2p01j3abcdef0000000000000000a1b2",
  "merchantReference": "dep-20240601-001",
  "reconciliationReference": "INV-2024-001",
  "providerReference": "MPESA-REC-99887766",
  "party": {
    "id": "user-42",
    "msisdn": "+254712345678",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "[email protected]"
  },
  "method": "mpesa-ke",
  "country": "KE",
  "requestedAmount": {
    "value": 500.00,
    "currency": "KES"
  },
  "finalAmount": {
    "value": 500.00,
    "currency": "KES"
  },
  "labels": { "orderId": "ORD-2024-001" },
  "createdAt": "2024-06-01T12:34:56.000000Z",
  "completedAt": "2024-06-01T12:35:12.000000Z",
  "completionSource": "webhook",
  "errorCode": null,
  "errorMessage": null,
  "providerData": {
    "name": "mpesa",
    "title": "M-Pesa Kenya",
    "fee": {
      "value": 10.00,
      "currency": "KES"
    },
    "partyData": null,
    "errorCode": "0",
    "errorMessage": "Success"
  }
}

Example Callback Body — Push (Provider-Initiated) Pay-In

Push callbacks fire for provider-initiated deposits (offline deposits) where the end user paid through a provider channel and the gateway only learns about the transaction after the fact. There is no original merchant request, so several fields that are normally echoed back are null.

Push callbacks fire only on success

Push callbacks fire when the gateway routes a provider notification to a successful state. They are not sent for failed or pending push transactions.

{
  "status": "success",
  "type": "payin",
  "flow": "push",
  "gatewayReference": "b2p01j3push000000000000000000e1f2",
  "merchantReference": null,
  "reconciliationReference": null,
  "providerReference": "MPESA-REC-44556677",
  "party": {
    "id": "user-42",
    "msisdn": "+254712345678",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": null
  },
  "method": "mpesa-ke",
  "country": "KE",
  "requestedAmount": {
    "value": 250.00,
    "currency": "KES"
  },
  "finalAmount": {
    "value": 250.00,
    "currency": "KES"
  },
  "labels": null,
  "createdAt": "2024-06-01T14:00:00.000000Z",
  "completedAt": "2024-06-01T14:00:01.000000Z",
  "completionSource": "webhook",
  "errorCode": null,
  "errorMessage": null,
  "providerData": {
    "name": "mpesa",
    "title": "M-Pesa Kenya",
    "fee": null,
    "partyData": null,
    "errorCode": "0",
    "errorMessage": "Success"
  }
}

For push callbacks, deduplicate using gatewayReference exactly as you would for any other flow.

Example Callback Body — Failed Pay-In

{
  "status": "failed",
  "type": "payin",
  "flow": "direct",
  "gatewayReference": "b2p01j3xyzabc0000000000000000a3b4",
  "merchantReference": "dep-20240601-002",
  "reconciliationReference": "dep-20240601-002",
  "providerReference": null,
  "party": {
    "id": "user-42",
    "msisdn": "+254712345678",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": null
  },
  "method": "mpesa-ke",
  "country": "KE",
  "requestedAmount": {
    "value": 1000.00,
    "currency": "KES"
  },
  "finalAmount": null,
  "labels": null,
  "createdAt": "2024-06-01T13:00:00.000000Z",
  "completedAt": "2024-06-01T13:01:30.000000Z",
  "completionSource": "webhook",
  "errorCode": "user_insufficient_funds",
  "errorMessage": "End user has insufficient funds",
  "providerData": {
    "name": "mpesa",
    "title": "M-Pesa Kenya",
    "fee": null,
    "partyData": null,
    "errorCode": "2001",
    "errorMessage": "Insufficient balance"
  }
}

Expected Response

Your endpoint must respond within 15 seconds with any 2xx HTTP status code. The gateway records delivery as successful on any 2xx and stops retrying.

A response body is optional and informational only — the gateway logs the body (up to 1 MB) for debugging but does not parse or act on its contents. The examples in this documentation return a small acknowledgement body such as {"status": "ok"} for clarity, but you can return an empty body, a different shape, or any other text and the result is the same.

HTTP/1.1 200 OK
Content-Type: application/json

{ "status": "ok" }

Respond quickly, process later

Accept the callback and return 2xx immediately. Perform heavy operations (database writes, order fulfilment, notifications) in a background job, not in the HTTP request handler. See Webhook Best Practices for implementation examples.


Verifying Callback Authenticity

Callbacks include your brand's API key in the X-API-KEY header. You must validate this header on every incoming callback to confirm the request originates from the PayAlo gateway.

Verification steps:

  1. Extract the X-API-KEY header from the incoming request.
  2. Compare it to the API key issued for your brand using a constant-time comparison.
  3. Reject any request where the header is missing or does not match.
import secrets

def handle_callback(request):
    presented = request.headers.get("X-API-KEY", "")
    if not secrets.compare_digest(presented, EXPECTED_API_KEY):
        return Response(status=401)

    payment = request.json()
    if payment["status"] not in ("success", "failed"):
        return Response(status=400)

    # Enqueue for async processing keyed on payment["gatewayReference"]
    return Response(json={"status": "ok"}, status=200)

Warning

There is currently no HMAC signature on callbacks. The X-API-KEY header is the sole verification mechanism. Ensure your callback endpoint is only accessible over HTTPS to prevent interception.

See Authentication & Security — Validate Callback Authenticity for additional guidance.


Delivery Policy

Each callback is delivered with at most one attempt — there is no automatic retry, and the attempt may be skipped entirely when the per-merchant circuit breaker is open (see below). When the attempt is made, if your endpoint does not return a 2xx response within the timeout, the delivery is recorded as failed and the gateway does not try again for this transaction.

Setting Value
Attempts per transaction 1
Per-attempt timeout 15 seconds

Per-merchant circuit breaker

To protect both your infrastructure and ours, the gateway runs a circuit breaker per merchant on the callback channel. When the breaker is open, subsequent callbacks for the same merchant are short-circuited — they are recorded as failed without an HTTP call. The breaker is internal to the gateway; you do not interact with it directly, but its behaviour affects how soon you can expect deliveries to resume after a sustained outage on your side.

Setting Value
Sampling window 60 seconds
Minimum requests in window 5
Failure ratio to open 50%
Open duration 30 seconds

A failure is any non-2xx response, timeout, or transport error. Once the breaker opens it stays open for 30 seconds, then enters a half-open state where the next callback probes your endpoint — success closes the breaker, failure reopens it.

Recovery — always reconcile via status

Whether a delivery failed on its single attempt or was short-circuited by the breaker, the transaction itself is not lost. Use the Payment Status endpoints to reconcile any transactions for which you did not receive a successful callback. This is the only supported recovery path.

The gateway does not inspect the response body — a 2xx always counts as successful delivery regardless of payload contents, and there is no in-band way to request another delivery for an existing transaction. Fetch the status instead.


Idempotency

Your callback endpoint should be idempotent. In rare cases (e.g. a gateway process restart between the HTTP send and recording the delivery), the same callback may be delivered more than once.

Use the gatewayReference field as a deduplication key:

def process_callback(payload):
    ref = payload["gatewayReference"]
    if already_processed(ref):
        return  # Skip — already handled
    with db.transaction():
        record_as_processed(ref)
        update_order_status(payload)

See Webhook Best Practices for more implementation patterns.