API Documentation

API reference

Send mobile money payment requests to the gateway and receive the result on your own server.

v1 Updated

The gateway takes a payment request from your system, sends it to the customer's mobile money operator, and tells you the result. You send one request. We call your server back when the payment is complete.

Important

Payments are asynchronous. A 202 response means we accepted the request, not that the customer has paid. Wait for the FINAL callback, or fetch the transaction, before you release goods or mark an order as paid.

How it works#

  1. You send POST/api/momo with the customer's phone number, an amount and your own reference.
  2. We reply straight away with 202 Accepted. The customer gets a payment prompt on their phone.
  3. When the payment reaches a final status, we send a FINAL callback to your notification_url.

For payments that the customer starts on their own (for example by paying your business number from their phone), we first send a VALIDATE callback so that you can confirm the payment is expected. See Callbacks.

Before you start#

You receive these during onboarding:

Item Where you use it
Base URL The address of the gateway. Every path below is added to it.
X-Client-Id Sent as a header on every request. Identifies your account.
X-Client-Secret Sent as a header on every request.
Signature key Used to compute X-Signature. Never sent. It only lives on your server.
Our IP addresses The addresses we send callbacks from. Whitelist them on your firewall.

All communication uses HTTPS. Requests and responses are JSON (application/json, UTF-8).

Warning

Keep the client secret and the signature key on your server. Never put them in a mobile app, in browser code or in a public repository.

Signing requests#

Every request to the gateway carries an X-Signature header, and so does every callback we send to you. The signature proves that the message came from the other side and was not changed on the way.

Header Value
X-Timestamp The current time in Unix seconds, such as 1792335559.
X-Signature The signature, as described below.

To compute the signature:

  1. Take the X-Timestamp value and the raw JSON body, and join them as text: timestamp + body.
  2. Remove every whitespace character from that text (spaces, tabs and new lines), including spaces inside values. "Airtel Money" becomes "AirtelMoney".
  3. Compute an HMAC with SHA-256, using your signature key as the key.
  4. Encode the result as Base64 (standard alphabet, with padding).

Note

Sign the same bytes you send. If your HTTP library re-formats the JSON after you sign it, the signature will not match. Build the body string once, sign it, then send that exact string.

Use this example to test your code. With the signature key sk_test_example_key, timestamp 1792333239 and this body:

json
{
  "type": "FINAL",
  "data": {
    "reference": "3137804641763844",
    "transaction_reference": "21453994292708209",
    "receipt": null,
    "completed": "2026-10-18T14:20:37.698824Z",
    "status": "FAILED",
    "status_description": "Failed",
    "operator": "Airtel Money"
  }
}

the signature is:

text
FC68dcFwYbpNBT+STbH7ZmE+5Ij4rW+8jz2o72+QOKg=

Each snippet below returns that value for the example above.

Python

python
import base64, hashlib, hmac, re

def sign(timestamp: str, payload: str, secret: str) -> str:
    data = re.sub(r"\s+", "", timestamp + payload)
    digest = hmac.new(secret.encode("utf-8"), data.encode("utf-8"), hashlib.sha256).digest()
    return base64.b64encode(digest).decode("ascii")

JavaScript (Node.js)

javascript
const crypto = require("node:crypto");

function sign(timestamp, payload, secret) {
    const data = (timestamp + payload).replace(/\s+/g, "");
    return crypto.createHmac("sha256", secret).update(data, "utf8").digest("base64");
}

PHP

php
function sign(string $timestamp, string $payload, string $secret): string {
    $data = preg_replace('/\s+/', '', $timestamp . $payload);
    return base64_encode(hash_hmac('sha256', $data, $secret, true));
}

Go

go
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"regexp"
)

var whitespace = regexp.MustCompile(`\s+`)

func sign(timestamp, payload, secret string) string {
	data := whitespace.ReplaceAllString(timestamp+payload, "")
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(data))
	return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}

Rust (crates hmac, sha2 and base64)

rust
use base64::{engine::general_purpose::STANDARD, Engine};
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;

fn sign(timestamp: &str, payload: &str, secret: &str) -> String {
    let data: String = format!("{timestamp}{payload}")
        .chars()
        .filter(|c| !c.is_whitespace())
        .collect();
    let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).expect("any key length works");
    mac.update(data.as_bytes());
    STANDARD.encode(mac.finalize().into_bytes())
}

Java

java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

static String sign(String timestamp, String payload, String secret) throws Exception {
    String data = (timestamp + payload).replaceAll("\\s+", "");
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
}

Create a transaction#

POST/api/momo

Starts a payment. Sign the request as described in Signing requests.

Headers

Header Required Description
X-Client-Id Yes Your client ID.
X-Client-Secret Yes Your client secret.
X-Timestamp Yes Current time in Unix seconds.
X-Signature Yes The request signature.
Idempotency-Key Yes A unique value for this request. Sending the same key again does not create a second payment.
Content-Type Yes application/json

Body

Field Type Description
msisdn string The customer's phone number in international format, without +, such as 255685007797.
reference string Your own reference for this payment. Use it to find the transaction later.
amount number The amount to collect.
type string COLLECTION to collect money from the customer.
notification_url string The HTTPS address on your server where we send callbacks.
bash
curl -X POST "https://GATEWAY_URL/api/momo" \
  -H "X-Client-Id: $CLIENT_ID" \
  -H "X-Client-Secret: $CLIENT_SECRET" \
  -H "X-Timestamp: 1792335559" \
  -H "X-Signature: iv80dq3Qv8WCDaHFUuXgzLje8NM1OfRcNflf1EERbAM=" \
  -H "Idempotency-Key: 3141259380648434" \
  -H "Content-Type: application/json" \
  -d '{"msisdn":"255685007797","reference":"3141259380648434","amount":100000,"type":"COLLECTION","notification_url":"https://shop.example.com/hooks/momo"}'

The signature in this example uses the test key sk_test_example_key. Yours will differ.

Response 202 Accepted

json
{
  "status": true,
  "reference": "3141259380648434",
  "message": "Transaction accepted",
  "data": null
}

reference is the value you sent. The customer now receives a prompt on their phone. The result comes later by callback.

Other responses

HTTP status Meaning What to do
400 Invalid request body. Fix the request. Do not retry it as it is.
403 The request came from an address that is not whitelisted, or your credentials are disabled. Check that you call us from a whitelisted address, then contact support.
404 Credentials not found. Check X-Client-Id and X-Client-Secret.

All error responses use the same JSON shape as the success response, with "status": false and a message that explains the problem.

Get a transaction#

GET/api/momo/{thirdPartyReference}

Returns the latest result of a transaction. {thirdPartyReference} is the reference you sent when you created it.

Use it when a callback does not arrive, or to double-check a payment before you release goods. Do not poll in a tight loop. Callbacks are the main way to learn the result.

Send the same X-Client-Id, X-Client-Secret, X-Timestamp and X-Signature headers. A GET has no body, so you sign only the timestamp.

bash
curl "https://GATEWAY_URL/api/momo/3141259380648434" \
  -H "X-Client-Id: $CLIENT_ID" \
  -H "X-Client-Secret: $CLIENT_SECRET" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIGNATURE"

Response 200 OK

The data field holds the same object that a FINAL callback sends.

json
{
  "status": true,
  "reference": "3141259380648434",
  "message": "Transaction found",
  "data": {
    "type": "FINAL",
    "data": {
      "reference": "3141259380648434",
      "transaction_reference": "21453994292708209",
      "receipt": null,
      "completed": "2026-10-18T14:20:37.698824Z",
      "status": "FAILED",
      "status_description": "Failed",
      "operator": "Airtel Money"
    }
  }
}

Other responses

HTTP status Meaning
403 The request came from an address that is not whitelisted, or your credentials are disabled.
404 Credentials not found, or no transaction with that reference.

Callbacks#

We send callbacks as POST requests to the notification_url you gave in the request. There are two types, shown in the type field.

type When we send it What you do
VALIDATE Before a payment that the customer started themselves is completed. Check the details and answer with success to accept the payment.
FINAL When the transaction reaches its final status. Update your order and answer with success.

Every callback has these headers:

Header Description
X-Signature The signature. Verify it. See Signing requests.
X-Timestamp The time we sent the callback, in Unix seconds.
Idempotency-Key A unique ID for this transaction (the same as transaction_reference). Use it to ignore repeats.
Content-Type application/json

FINAL callback#

json
{
  "type": "FINAL",
  "data": {
    "reference": "3137804641763844",
    "transaction_reference": "21453994292708209",
    "receipt": null,
    "completed": "2026-10-18T14:20:37.698824Z",
    "status": "FAILED",
    "status_description": "Failed",
    "operator": "Airtel Money"
  }
}
Field Type Description
reference string Your own reference, as you sent it.
transaction_reference string Our unique ID for the transaction. Quote it when you contact support.
receipt string or null The operator's receipt number. It is null when the operator gives none, which is usual for failed payments.
completed string When the transaction reached its final status. ISO 8601, in UTC.
status string The final status. See below.
status_description string A short readable explanation of the status.
operator string The mobile money operator, such as Airtel Money.
status Meaning
SUCCESS The customer paid. The money is yours.
FAILED The payment did not go through. For example the customer cancelled, or had too little money.

VALIDATE callback#

json
{
  "type": "VALIDATE",
  "data": {
    "reference": "3137804641763844",
    "transaction_reference": "21453994292708209",
    "receipt": null,
    "operator": "Airtel Money",
    "amount": 1000,
    "business_number": 2121213
  }
}
Field Type Description
reference string The customer's reference for the payment.
transaction_reference string Our unique ID for the transaction.
receipt string The operator's receipt number. Some operators do not send one, so the field may be missing or null.
operator string The mobile money operator.
amount number The amount the customer is paying.
business_number number The business number the customer is paying to.

Check the reference and the amount against your own records. If they are right, answer with success. The payment then continues, and you receive the FINAL callback when it finishes.

Your response#

Answer every callback with HTTP 200 and this body:

json
{
  "status": true,
  "message": "Success"
}

Tip

Callbacks can arrive more than once. Store the Idempotency-Key of each callback you have handled, and when you see the same one again, answer with success without processing it a second time.

Respond quickly. Save the result and do slow work, such as sending emails, after you have replied.

Security checklist#

Caution

Callbacks that are not checked can be forged. Anyone who knows your callback URL can send a fake "payment succeeded" message.

  • Whitelist our IP addresses. Accept callbacks only from the addresses we gave you. Requests to the gateway from other addresses are refused with 403.
  • Use HTTPS. Your notification_url must use HTTPS in production.
  • Verify the signature on every callback. Compute it from the raw body and the X-Timestamp header, and compare it with X-Signature. Reject the callback if they differ. Compare in constant time, with a function such as hmac.compare_digest in Python or MessageDigest.isEqual in Java.
  • Check the timestamp. Reject callbacks whose X-Timestamp is far from your clock, for example by more than 5 minutes. This stops someone from replaying an old message.
  • Check the idempotency key. Ignore callbacks you have already processed.
  • Check the details. Match reference and the amount with your own order before you mark it paid.
  • Protect your secrets. Keep the client secret and the signature key in a secrets manager or environment variables. Do not log them, and tell us at once if you think they leaked.

Getting help#

If you contact support, include the transaction_reference and your own reference. With these two we can trace the payment from request to callback.