MobileWalletPartner Gateway Docs

Authentication & signing

AppId/AppKey, service grants, and the HMAC signature required on money-out actions.

AppId / AppKey (HTTP Basic)

Every request authenticates with HTTP Basic auth:

Authorization: Basic base64(AppId:AppKey)

A few properties worth knowing before you build against this:

  • A wrong AppId and a wrong AppKey fail identically (401) — the response never reveals which half was wrong.
  • AppKey is shown in plaintext exactly once — at credential creation or rotation. It's hashed at rest (PBKDF2) from that point on. If it's lost, ask an admin to rotate your credential (this immediately invalidates the old one).
  • A suspended partner still authenticates (proving the credential itself is valid) but is rejected with 403, distinct from the 401 an unknown/wrong credential gets.
  • Sandbox and Live are separate credentials with separate IP whitelists — the same way a real payment provider gives you separate test/live key pairs. Rotating one never touches the other.

Service grants

Authenticating successfully only proves who you are — it doesn't grant access to anything. Each service (MOMO_DEPOSIT, MOMO_WITHDRAWAL, CRYPTO_BUY, …) must be individually enabled for your partner, each with its own amount/daily limits. Calling an endpoint for a service you haven't been granted returns 403 GW-403-FORBIDDEN — never a 404, which would otherwise leak whether the route exists at all.

IP whitelisting

Live fails closed: zero configured whitelist entries means every Live request is rejected, full stop. Sandbox is open by default so you can start testing immediately. Ask an admin to add your outbound IP/CIDR before sending real traffic.

Rate limits

Each partner/service pair has its own rate limit bucket — traffic on one service never consumes another service's budget.

Request signing (money-out actions)

MOMO_WITHDRAWAL and CRYPTO_SELL — both money leaving the platform to a customer — require a request signature in addition to Basic auth. MOMO_DEPOSIT/CRYPTO_BUY (money coming in) do not.

Two headers, alongside the usual Authorization and JSON body:

X-Timestamp: 2026-10-10T11:00:00Z
X-Signature: <hex-encoded HMAC-SHA256, lowercase>

Signature algorithm:

signature = lowercase_hex( HMAC_SHA256(
  key:     AppKey (the same plaintext key you send in the Authorization header),
  message: "{timestamp}.{raw_json_request_body}"
))

There is no separate signing secret to manage — you prove possession of your AppKey twice in the same request: once for identity (the Authorization header) and once for integrity (the signature). {raw_json_request_body} must be the exact bytes you send — not a re-serialized or re-formatted version — since the gateway verifies the signature against the literal request body it receives.

import hashlib
import hmac
from datetime import datetime, timezone

app_key = "your-app-key"
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
body = '{"partnerOrderId":"wd-001","msisdn":"+237670000001","amount":500,"currency":"XAF"}'

message = f"{timestamp}.{body}".encode()
signature = hmac.new(app_key.encode(), message, hashlib.sha256).hexdigest()
curl -u "$APP_ID:$APP_KEY" -X POST https://gateway.example.com/api/v1/momo/withdraw \
  -H "Content-Type: application/json" \
  -H "X-Timestamp: $TIMESTAMP" \
  -H "X-Signature: $SIGNATURE" \
  -d "$BODY"

A request is rejected (401) if the signature is missing, doesn't match, or the timestamp is more than 5 minutes away from the server's clock in either direction — a timestamp from the future is treated exactly as suspiciously as a stale one.

Money-out actions additionally enforce your partner's configured amount/daily limits, and are rejected if they'd take your ledger balance below zero — both checked before the request ever reaches a provider.

Complete signing examples in Node.js, Python, PHP and C#, and a Postman collection that signs for you, are on Code samples & Postman.

Want to see a real signed request built and sent live? Every endpoint in the API Reference has a Try it console and ready-made code samples in several languages, so you can compare them against your own implementation.

On this page