Fintech

Orange Money Web Payment en Next.js : encaisser un paiement web

Intégrer Orange Money en direct dans Next.js (App Router) : jeton OAuth côté serveur, endpoint /webpayment, redirection, notification vérifiée par notif_token, idempotence et secrets jamais exposés.

📍 Article principal du cluster : Mobile money en backend : Wave, Orange Money, PayDunya, CinetPay. Pour la vue d’ensemble (fournisseur vs agrégateur, cycle de vie, sécurité), lisez d’abord le pilier.

Encaisser via un agrégateur, c’est confortable. Mais quand Orange Money concentre l’essentiel de vos clients et que chaque point de commission compte, l’intégration directe devient rentable. Orange expose pour ça son API Web Payment. Le piège, avec Next.js, c’est de croire qu’on peut appeler cette API depuis le navigateur : ce serait exposer vos clés à la terre entière. Tout se joue côté serveur. À la fin de ce tutoriel, votre application Next.js créera un paiement Orange Money depuis un route handler, redirigera le client vers la page de paiement, et confirmera l’encaissement via une notification serveur — clés jamais exposées.

Ce que vous allez apprendre

  • Obtenir un jeton OAuth2 (client credentials) auprès d’Orange, côté serveur uniquement ;
  • Créer un paiement via l’endpoint /webpayment et récupérer la payment_url ;
  • Rediriger le client puis traiter la notification Orange (notif_url) en vérifiant le notif_token ;
  • Garder vos secrets hors du bundle navigateur — l’erreur n°1 des paiements en Next.js ;
  • Rendre la confirmation idempotente et tester en sandbox.

Ce que vous allez construire

Le fil rouge : un bouton « Payer avec Orange Money » sur une boutique Next.js. Au clic, un route handler /api/orange/pay crée le paiement et renvoie l’URL ; le client y est redirigé. À son retour, la commande est déjà passée « payée » grâce à /api/orange/notify. Concret : un paiement réel validé sans qu’aucune clé Orange ne quitte le serveur.

Prérequis

  • Node 18+ et un projet Next.js 14/15 (au moment d’écrire) en App Router.
  • Un compte Orange Developer avec une application Web Payment : récupérez client_id, client_secret et merchant_key.
  • Le statut marchand Orange Money pour votre pays (Sénégal, Côte d’Ivoire, Mali…).
  • ⏱️ Temps estimé : ~50 minutes. Test express : si vous savez écrire un route handler App Router, vous êtes prêt.

Étape 1 — Mettre les secrets là où le navigateur ne va jamais

En Next.js, toute variable préfixée NEXT_PUBLIC_ est injectée dans le bundle JavaScript envoyé au navigateur. Mettre une clé d’API derrière ce préfixe, c’est la publier. Vos identifiants Orange n’ont donc jamais ce préfixe, et ne sont lus que dans du code serveur (route handlers, fonctions lib/).

# .env.local  (jamais commité, jamais de préfixe NEXT_PUBLIC_)
ORANGE_CLIENT_ID=votre_client_id
ORANGE_CLIENT_SECRET=votre_client_secret
ORANGE_MERCHANT_KEY=votre_merchant_key

Règle mentale simple : si une donnée sert à signer ou à authentifier un appel d’argent, elle vit côté serveur. Le composant client, lui, ne parlera jamais à Orange directement — il parlera à votre API.

Étape 2 — Obtenir le jeton OAuth

L’API Orange utilise OAuth2 en mode « client credentials » : on échange client_id+client_secret contre un jeton d’accès à durée de vie limitée. On encapsule ça dans une fonction serveur réutilisable. Le jeton expire (expires_in) : en production, on le met en cache pour ne pas en redemander un à chaque paiement.

// lib/orange.ts
const BASE = 'https://api.orange.com';

export async function getToken() {
  const creds = Buffer
    .from(process.env.ORANGE_CLIENT_ID + ':' + process.env.ORANGE_CLIENT_SECRET)
    .toString('base64');

  const res = await fetch(BASE + '/oauth/v3/token', {
    method: 'POST',
    headers: {
      Authorization: 'Basic ' + creds,
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: 'grant_type=client_credentials',
    cache: 'no-store',
  });

  const data = await res.json();
  return data.access_token as string;
}

Buffer.from(...).toString('base64') encode les identifiants pour l’en-tête Basic. Le cache: 'no-store' empêche Next.js de mettre la réponse en cache — on ne met jamais un jeton d’authentification en cache partagé. Si l’appel renvoie un 401, c’est que vos identifiants sont faux ou que l’app Orange n’est pas activée pour le Web Payment.

Un détail Next.js qui coûte des heures : ces appels doivent tourner sur le runtime Node, pas Edge, car Buffer n’existe pas dans l’environnement Edge. Si vous avez basculé un route handler en Edge, ajoutez export const runtime = 'nodejs'; en tête du fichier. Sinon, vous verrez une erreur « Buffer is not defined » au moment d’encoder les identifiants.

Étape 2 bis — La couche persistance

Les fonctions savePayment, findPaymentByOrder et markPaid utilisées plus loin encapsulent votre base. Peu importe l’ORM ; l’essentiel est de stocker la référence de commande, les deux jetons Orange et le statut. Voici une implémentation minimale avec Prisma.

// lib/payments.ts
import { prisma } from '@/lib/prisma';

export function savePayment(p: {
  orderId: string; payToken: string; notifToken: string; amount: number; status: string;
}) {
  return prisma.payment.create({ data: p });
}

export function findPaymentByOrder(orderId: string) {
  return prisma.payment.findFirst({ where: { orderId }, orderBy: { createdAt: 'desc' } });
}

export function markPaid(orderId: string) {
  return prisma.payment.updateMany({ where: { orderId }, data: { status: 'paid' } });
}

Le orderBy par date décroissante gère le cas où un client réessaie : on raisonne toujours sur la tentative la plus récente. Le statut reste une simple chaîne (pending, paid, failed), suffisante pour la machine à états décrite dans le pilier.

Étape 3 — Créer le paiement et rediriger

Le route handler reçoit l’identifiant de commande, récupère un jeton, puis appelle /webpayment. Orange répond avec une payment_url, un pay_token et un notif_token. Ces deux jetons sont précieux : on les stocke en base pour vérifier plus tard que la notification est authentique. Le montant, lui, est un entier, et il vient du serveur — jamais du corps de la requête cliente sans recontrôle.

// app/api/orange/pay/route.ts
import { NextResponse } from 'next/server';
import { getToken } from '@/lib/orange';
import { savePayment } from '@/lib/payments';

export async function POST(request: Request) {
  const { orderId, amount } = await request.json();
  const token = await getToken();

  const res = await fetch('https://api.orange.com/orange-money-webpay/dev/v1/webpayment', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + token,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      merchant_key: process.env.ORANGE_MERCHANT_KEY,
      currency: 'OUV',        // 'XOF' en production
      order_id: orderId,
      amount: amount,         // entier XOF
      return_url: 'https://maboutique.sn/paiement/retour',
      cancel_url: 'https://maboutique.sn/paiement/annule',
      notif_url: 'https://maboutique.sn/api/orange/notify',
      lang: 'fr',
      reference: 'Boutique Diallo',
    }),
    cache: 'no-store',
  });

  const data = await res.json();

  await savePayment({
    orderId,
    payToken: data.pay_token,
    notifToken: data.notif_token,
    amount,
    status: 'pending',
  });

  return NextResponse.json({ paymentUrl: data.payment_url });
}

Notez currency: 'OUV' : c’est la devise du bac à sable Orange ; en production, vous passez 'XOF'. Et le chemin /dev/ dans l’URL devient le code pays (/ci/, /sn/…) une fois en production. Côté client, le bouton ne fait qu’appeler ce route handler puis suivre l’URL retournée.

// composant client (bouton de paiement)
async function payer(orderId: string) {
  const res = await fetch('/api/orange/pay', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ orderId, amount: 5000 }),
  });
  const { paymentUrl } = await res.json();
  window.location.href = paymentUrl; // direction Orange Money
}

Le client ne voit jamais vos clés : il ne connaît que votre route /api/orange/pay. Tout le secret reste sur le serveur Next.js.

Un mot sur le montant : dans l’exemple, le composant client envoie amount: 5000, mais en pratique on ne fait jamais confiance à cette valeur. Le route handler doit recharger la commande depuis la base à partir de orderId et recalculer le total côté serveur — exactement comme dans le pilier. Le montant envoyé par le client n’est qu’une indication d’affichage, jamais la source de vérité de ce qu’on encaisse.

Étape 4 — La notification : vérifier le notif_token

Une fois le paiement validé (ou échoué), Orange appelle votre notif_url avec un statut. Comme pour tout webhook, on ne fait pas confiance au corps tel quel : Orange y joint le notif_token reçu à l’initialisation, et c’est lui qu’on compare à celui qu’on a stocké. S’ils ne correspondent pas, l’appel n’est pas authentique et on l’ignore.

// app/api/orange/notify/route.ts
import { findPaymentByOrder, markPaid, markFailed } from '@/lib/payments';

export async function POST(request: Request) {
  const body = await request.json();
  // body : { status, notif_token, txnid, order_id, ... }

  const payment = await findPaymentByOrder(body.order_id);
  if (!payment) return new Response('', { status: 200 });

  // 1) Authenticité : le notif_token doit correspondre.
  if (body.notif_token !== payment.notifToken) {
    return new Response('', { status: 200 });
  }
  // 2) Idempotence : déjà payé ? on s'arrête.
  if (payment.status === 'paid') {
    return new Response('', { status: 200 });
  }
  // 3) Statut réel.
  if (body.status === 'SUCCESS') {
    await markPaid(payment.orderId);
  } else if (body.status === 'FAILED') {
    await markFailed(payment.orderId);
  }
  return new Response('', { status: 200 });
}

On répond toujours 200 pour qu’Orange cesse de réémettre la notification. Le notif_token joue ici le rôle de signature partagée : sans lui, n’importe quel curieux pourrait poster un faux SUCCESS sur votre endpoint. Pour aller plus loin, Orange propose aussi un endpoint /webpayment/transactionstatus (avec order_id, amount et pay_token) pour interroger le statut à la demande — utile comme filet si une notification se perd.

Pensez enfin à journaliser chaque notification reçue (statut, order_id, horodatage) avant de la traiter. Le jour où un client conteste un paiement, ou quand vous rapprochez vos transactions avec le relevé Orange en fin de mois, ces journaux sont votre seule preuve. Une notification non journalisée est une transaction qu’on ne saura pas défendre.

Étape 5 — Pages de retour et OTP

Le client est renvoyé sur return_url (paiement tenté) ou cancel_url (abandon). Ces pages, comme dans le pilier, n’encaissent rien : elles lisent l’état de la commande dans votre base. Pensez aussi à l’expérience : sur Orange Money, le client valide en générant un code à usage unique (OTP) via le menu USSD Orange. Si votre interface ne l’explique pas, beaucoup abandonnent à cette étape précise — un simple texte « validez avec le code reçu par USSD » réduit nettement les abandons.

Point d’étape — Au clic « Payer », une ligne pending avec notif_token doit apparaître en base, puis basculer en paid après la notification. Si elle reste pending, vérifiez que notif_url est publique et en HTTPS, et que le notif_token comparé est bien celui stocké.

Étape 6 — Tester en sandbox

Avec la devise OUV et le chemin /dev/, vous êtes en bac à sable : aucun argent réel ne bouge. Déroulez le cycle complet — clic, redirection, validation simulée, notification — et observez la commande passer « payée ». Exposez votre app locale via un tunnel HTTPS pour que la notif_url soit joignable. Ne passez en XOF et au chemin pays qu’une fois ce cycle vert de bout en bout.

🐞 Pièges fréquents

Symptôme Cause probable Correctif
Clé visible dans le navigateur (onglet Sources) Variable préfixée NEXT_PUBLIC_ Retirer le préfixe ; lire la clé uniquement en code serveur
401 sur /oauth/v3/token Identifiants faux ou app non activée Vérifier client_id/secret et l’activation Web Payment
Commande jamais « payée » notif_url non publique Tunnel HTTPS ; tester l’URL
Faux SUCCESS accepté notif_token non vérifié Comparer au token stocké avant d’agir
Paiement rejeté en prod Devise restée OUV / chemin /dev/ Passer à XOF et au code pays

🌍 Adaptation au contexte ouest-africain

L’intégration directe Orange Money a du sens là où Orange domine — c’est le cas dans une grande partie de la zone UEMOA. Le montant se manipule en entier (le XOF n’a pas de centimes), et la validation par OTP USSD est une spécificité à expliquer clairement à l’utilisateur. Côté réseau, prévoyez le filet habituel : si la notification tarde, un appel à transactionstatus depuis un job planifié rattrape les commandes restées en attente. Pour héberger les route handlers, n’importe quelle plateforme Node ou un VPS en HTTPS fait l’affaire — l’important est que notif_url soit publiquement joignable.

Reste la question du « pourquoi en direct plutôt qu’un agrégateur ». La réponse est économique : si Orange Money pèse la majorité de vos encaissements, économiser la marge de l’agrégateur sur chaque transaction finit par compter. Si vos clients se répartissent entre plusieurs wallets, revenez à un agrégateur — le tutoriel CinetPay du cluster vous y attend.

✅ Récapitulatif

Vous avez intégré Orange Money en direct, proprement : un jeton OAuth obtenu côté serveur, un paiement créé via /webpayment avec redirection, et une notification vérifiée par notif_token avant toute validation idempotente. Le fil conducteur du pilier reste intact — le navigateur n’encaisse jamais, le serveur vérifie tout — et vos clés ne quittent jamais Next.js.

🧾 Aide-mémoire

Élément Rôle
POST /oauth/v3/token Obtenir le jeton (Basic auth, grant_type=client_credentials)
POST /orange-money-webpay/{pays}/v1/webpayment Créer le paiement → payment_url, pay_token, notif_token
notif_token Jeton à comparer pour authentifier la notification
currency OUV en sandbox, XOF en production
NEXT_PUBLIC_ À NE JAMAIS utiliser pour une clé de paiement

💪 À vous de jouer

Mettez le jeton OAuth en cache mémoire avec son expires_in pour éviter d’en demander un à chaque paiement, et ajoutez un repli : si la notification n’est pas arrivée au bout de 5 minutes, interrogez transactionstatus.

Voir une piste

Stockez { token, expiresAt } dans une variable de module ; dans getToken(), renvoyez le token en cache s’il reste valide (avec une marge de 60 s), sinon redemandez-en un. Attention : en serverless, le cache mémoire ne survit pas entre invocations — préférez alors un petit cache partagé (Redis) ou acceptez un appel de jeton par requête.

Tutoriels frères

Pour aller plus loin

FAQ

Puis-je appeler l’API Orange directement depuis un composant client ?
Non. Cela exposerait client_secret et merchant_key dans le navigateur. L’appel se fait toujours depuis un route handler serveur.

Quelle différence entre pay_token et notif_token ?
Le pay_token identifie la session de paiement (utile pour transactionstatus) ; le notif_token authentifie la notification entrante. On stocke les deux.

Le paiement marche en sandbox mais échoue en prod.
Vérifiez la devise (XOF, pas OUV), le chemin pays dans l’URL, et que votre statut marchand est actif pour ce pays.

Mots-clés : Orange Money Web Payment, Next.js paiement, API Orange Money, OAuth Orange, notif_token, mobile money Next.js.

Malick Diallo

Rédaction SenTur

Contributeur SenTur — passionné de tech et de transmission.

Aucun commentaire pour l'instant — lancez la discussion !

Laisser un commentaire

Votre adresse email ne sera pas publiée. La discussion est modérée — restez courtois et constructif.