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