MobileWalletPartner Gateway Docs

Code samples & Postman

Signed requests and webhook verification in Node.js, Python, PHP and C#, plus a ready-made Postman collection.

Postman collection

Download the Postman collection, import it, and set three collection variables: baseUrl, appId and appKey. Start with a Sandbox credential.

  • Every request authenticates with your AppId and AppKey.
  • Withdrawals and crypto sells are signed automatically by a pre-request script.
  • Each create request gets a fresh partnerOrderId, and the status and callback-replay requests reuse the last one you created.
  • testMsisdn defaults to the Sandbox "approved" number. Sandbox only accepts the test numbers.

Signing a money-out request

Withdrawals and crypto sells need two extra headers. The signature is the lowercase hex HMAC-SHA256 of "{timestamp}.{body}", keyed with your AppKey. Sign the exact body string you send: serialize once, then use that same string for both the signature and the request.

All four samples below send the same Sandbox withdrawal.

Node.js (18+)

import { createHmac } from "node:crypto";

const appId = process.env.APP_ID;
const appKey = process.env.APP_KEY;

const body = JSON.stringify({
  partnerOrderId: `wd-${Date.now()}`,
  msisdn: "+237670000001",
  amount: 500,
  currency: "XAF",
});
const timestamp = new Date().toISOString();
const signature = createHmac("sha256", appKey).update(`${timestamp}.${body}`).digest("hex");

const response = await fetch("https://gateway.example.com/api/v1/momo/withdraw", {
  method: "POST",
  headers: {
    Authorization: "Basic " + Buffer.from(`${appId}:${appKey}`).toString("base64"),
    "Content-Type": "application/json",
    "X-Timestamp": timestamp,
    "X-Signature": signature,
  },
  body, // the same string that was signed
});
console.log(response.status, await response.json());

Python (requests)

import hashlib, hmac, json, os, time
from datetime import datetime, timezone
import requests

app_id, app_key = os.environ["APP_ID"], os.environ["APP_KEY"]

body = json.dumps({
    "partnerOrderId": f"wd-{int(time.time() * 1000)}",
    "msisdn": "+237670000001",
    "amount": 500,
    "currency": "XAF",
}, separators=(",", ":"))
timestamp = datetime.now(timezone.utc).isoformat()
signature = hmac.new(app_key.encode(), f"{timestamp}.{body}".encode(), hashlib.sha256).hexdigest()

response = requests.post(
    "https://gateway.example.com/api/v1/momo/withdraw",
    auth=(app_id, app_key),
    headers={"Content-Type": "application/json", "X-Timestamp": timestamp, "X-Signature": signature},
    data=body.encode(),  # the same bytes that were signed - not json=...
)
print(response.status_code, response.json())

PHP

<?php
$appId = getenv('APP_ID');
$appKey = getenv('APP_KEY');

$body = json_encode([
    'partnerOrderId' => 'wd-' . round(microtime(true) * 1000),
    'msisdn' => '+237670000001',
    'amount' => 500,
    'currency' => 'XAF',
]);
$timestamp = gmdate('Y-m-d\TH:i:s\Z');
$signature = hash_hmac('sha256', $timestamp . '.' . $body, $appKey);

$ch = curl_init('https://gateway.example.com/api/v1/momo/withdraw');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_USERPWD => "$appId:$appKey",
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        "X-Timestamp: $timestamp",
        "X-Signature: $signature",
    ],
    CURLOPT_POSTFIELDS => $body, // the same string that was signed
    CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($ch);

C# (.NET 8)

using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

var appId = Environment.GetEnvironmentVariable("APP_ID")!;
var appKey = Environment.GetEnvironmentVariable("APP_KEY")!;

var body = JsonSerializer.Serialize(new
{
    partnerOrderId = $"wd-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
    msisdn = "+237670000001",
    amount = 500,
    currency = "XAF",
});
var timestamp = DateTimeOffset.UtcNow.ToString("O");
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(appKey));
var signature = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{body}"))).ToLowerInvariant();

using var http = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://gateway.example.com/api/v1/momo/withdraw")
{
    Content = new StringContent(body, Encoding.UTF8, "application/json"), // the same string that was signed
};
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{appId}:{appKey}")));
request.Headers.Add("X-Timestamp", timestamp);
request.Headers.Add("X-Signature", signature);

var response = await http.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");

Deposits, crypto buys and read-only calls use the same Basic auth without the two signing headers.

Verifying a webhook

Callbacks are signed with your callbackSecret over the raw request body (no timestamp). Verify against the raw bytes before parsing the JSON, and compare in constant time. See Webhooks for delivery and retry rules.

Node.js (Express)

import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

const app = express();
app.post("/gateway-callback", express.raw({ type: "application/json" }), (req, res) => {
  const expected = createHmac("sha256", process.env.CALLBACK_SECRET).update(req.body).digest();
  const provided = Buffer.from(req.get("X-Signature") ?? "", "hex");
  if (provided.length !== expected.length || !timingSafeEqual(provided, expected)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.body.toString("utf8"));
  // ... record event.partnerOrderId / event.status idempotently ...
  res.sendStatus(200); // reply within 10 seconds
});

Python (Flask)

import hashlib, hmac, os
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/gateway-callback")
def gateway_callback():
    expected = hmac.new(os.environ["CALLBACK_SECRET"].encode(), request.get_data(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("X-Signature", "")):
        abort(401)
    event = request.get_json()
    # ... record event["partnerOrderId"] / event["status"] idempotently ...
    return "", 200

PHP

<?php
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, getenv('CALLBACK_SECRET'));
if (!hash_equals($expected, $_SERVER['HTTP_X_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}
$event = json_decode($raw, true);
// ... record $event['partnerOrderId'] / $event['status'] idempotently ...
http_response_code(200);

C# (ASP.NET Core)

app.MapPost("/gateway-callback", async (HttpRequest request) =>
{
    using var buffer = new MemoryStream();
    await request.Body.CopyToAsync(buffer);
    var raw = buffer.ToArray();

    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("CALLBACK_SECRET")!));
    var expected = hmac.ComputeHash(raw);
    var provided = Convert.FromHexString(request.Headers["X-Signature"].ToString() is { Length: 64 } s ? s : new string('0', 64));
    if (!CryptographicOperations.FixedTimeEquals(expected, provided))
    {
        return Results.Unauthorized();
    }

    var callback = JsonSerializer.Deserialize<JsonElement>(raw);
    // ... record partnerOrderId / status idempotently ...
    return Results.Ok();
});

On this page