What you'll build

A complete Orange Money payment integration for West and Central Africa. By the end of this tutorial you will have:

Orange Money operates in Senegal, Cote d'Ivoire, Mali, Burkina Faso, Guinea, and Cameroon. All amounts are in XOF (West African CFA franc) — always integers, no decimals.

Get your API key

  1. Go to developer.orange.com and create an account
  2. Create a new application and subscribe to the Orange Money Webpay API
  3. From your app's credentials page, copy the Client ID and Client Secret
  4. Contact Orange or your aggregator to obtain your Merchant Key — this identifies your business in payment requests

Add these to your .env file:

ORANGE_CLIENT_ID=your_client_id
ORANGE_CLIENT_SECRET=your_client_secret
ORANGE_MERCHANT_KEY=your_merchant_key
ORANGE_BASE_URL=https://api.orange.com

Orange provides a sandbox environment for testing. Check your developer portal for the sandbox base URL and test credentials. Switch to the production URL only when you are ready to accept real payments.

Build the payment endpoint

Orange Money uses OAuth2 client_credentials to issue short-lived access tokens. You must fetch a token before every payment request (or cache it until expiry).

BlocWeave prompt

Create an Orange Money payment integration for an Express app. 1. src/services/orange.service.ts — export two functions: - getAccessToken(): POST to ${ORANGE_BASE_URL}/oauth/v3/token with header Authorization: Basic <base64(ORANGE_CLIENT_ID:ORANGE_CLIENT_SECRET)> and body grant_type=client_credentials (Content-Type: application/x-www-form-urlencoded). Return the access_token from the response. - createPayment({ orderId, amount, returnUrl, cancelUrl, notifUrl }): Call getAccessToken() first. Then POST to ${ORANGE_BASE_URL}/orange/payment/v1/webpayment with Bearer token and JSON body: { merchant_key: ORANGE_MERCHANT_KEY, currency: "XOF", order_id: orderId, amount (integer), return_url: returnUrl, cancel_url: cancelUrl, notif_url: notifUrl }. Return the full response body which includes payment_url. 2. src/controllers/payments.controller.ts — export createOrangePayment(req, res, next): - Validate req.body has amount (positive integer) and order_id (string) - Call createPayment with the order details, using your server's base URL for return_url, cancel_url, and notif_url - Save the order_id and amount to your database with status "pending" - Return 201 with { paymentUrl: response.payment_url, orderId } 3. src/routes/payments.router.ts — POST /orange → createOrangePayment. Mount under /payments in index.ts.

The createPayment response includes a payment_url. Redirect the customer to that URL — Orange Money renders a payment page where the customer enters their phone number and confirms with their PIN.

// Example response from Orange
{
  "payment_url": "https://webpayment.orange.com/page/xxxxx",
  "pay_token": "abc123",
  "notif_token": "xyz789"
}

After the customer pays (or cancels), Orange redirects them to your return_url or cancel_url.

Handle the webhook / verify payment

Orange Money sends a notification to your notif_url when payment completes, but it does not include an HMAC signature. Never trust the notification payload alone — always verify by calling the transaction status API.

BlocWeave prompt

Add Orange Money webhook handling and transaction verification. 1. src/services/orange.service.ts — add: - verifyTransaction(orderId): Call getAccessToken(). GET ${ORANGE_BASE_URL}/orange/payment/v1/transactionstatus/${orderId} with Bearer token. Return the response body. 2. src/controllers/payments.controller.ts — add: - orangeWebhook(req, res, next): - Immediately return 200 { received: true } so Orange does not retry - Extract the order_id from the notification body - Call verifyTransaction(order_id) - If status === "SUCCESS": update the order in your database to "paid" and record the transaction details - If status !== "SUCCESS": log the discrepancy but do not update the order - orangeReturn(req, res, next): - This handles the redirect when the customer returns from the payment page - Extract order_id from query params - Call verifyTransaction(order_id) to get the current status - Redirect to your frontend with the payment result: /payment/success?order=ORDER_ID or /payment/failed?order=ORDER_ID 3. src/routes/payments.router.ts — add POST /orange/webhook → orangeWebhook, GET /orange/return → orangeReturn, GET /orange/cancel → orangeCancel (redirect to frontend cancel page).

// Verification response
{
  "status": "SUCCESS",    // or "FAILED", "PENDING"
  "order_id": "ORD-12345",
  "amount": 5000,
  "currency": "XOF"
}

No HMAC means verify everything. Because Orange Money webhooks are unsigned, the only way to confirm a payment is to call the transactionstatus API yourself. Never mark an order as paid based on the webhook body alone. An attacker who discovers your webhook URL could send fake success notifications.

Test it

  1. Set your environment variables and start the server:
npm run dev
  1. Create a payment:
curl -X POST http://localhost:3000/payments/orange \
  -H "Content-Type: application/json" \
  -d '{"order_id": "ORD-TEST-001", "amount": 5000}'
  1. Open the returned paymentUrl in a browser — you should see the Orange Money payment page

  2. In sandbox mode, complete the test payment using the test phone number and PIN provided in your Orange developer account

  3. After payment, Orange redirects to your return_url — your server calls verifyTransaction and updates the order status

  4. Check your database — the order should now be marked as paid

  5. To test the webhook separately, use ngrok to expose your local server:

ngrok http 3000
# Update your notif_url to the ngrok URL

Project structure after this tutorial

src/
  controllers/
    payments.controller.ts
  routes/
    payments.router.ts
  services/
    orange.service.ts