Passer MTN MoMo en production est une porte d’entrée massive vers les paiements mobiles dans une quinzaine de pays africains (Ghana, Côte d’Ivoire, Cameroun, Bénin, Uganda, Rwanda, Zambie, Liberia, Guinée Conakry, Congo, Afrique du Sud, Eswatini notamment — MTN n’opère cependant pas comme MNO au Sénégal, où il faut se tourner vers Orange Money, Wave ou Mixx by Yas), mais sa documentation publique ne couvre que partiellement le moment le plus délicat du cycle de vie : la transition d’un sandbox auto-géré vers un environnement de production réglementé. Ce guide reprend cette transition étape par étape, avec les commandes exactes, les pièges classiques et les vérifications opérationnelles qui sécurisent un go-live sans incident.
Le périmètre couvert ici suppose que vous avez déjà un prototype fonctionnel en sandbox : initiation d’un requestToPay, réception d’un callback, gestion basique des erreurs. Si ce n’est pas le cas, commencez par bâtir cette boucle avant d’attaquer la mise en production, car les concepts fondamentaux (X-Reference-Id, Bearer token, subscription keys) sont identiques en sandbox et en prod, ils sont juste émis différemment.
Prérequis pour passer MTN MoMo en production
- Une intégration sandbox fonctionnelle sur
sandbox.momodeveloper.mtn.com - Un dossier entreprise complet : registre de commerce (RC), numéro fiscal local (IFU au Bénin, NIF au Cameroun, équivalent pays), statuts, attestation de domiciliation bancaire
- Une URL HTTPS stable pour recevoir les callbacks production (jamais une URL ngrok ou de tunnel local)
- Un serveur avec une IP publique fixe ou une plage CIDR connue
- Comptez quatre à six semaines de délai administratif
Étape 1 — Comprendre les six différences sandbox vs production
La transition n’est pas un simple changement de variable d’environnement. Six paramètres concrets diffèrent et doivent être anticipés dans votre code dès la phase sandbox pour éviter une refonte au moment du go-live.
Le premier paramètre est l’hôte. Sandbox : sandbox.momodeveloper.mtn.com. Production : proxy.momoapi.mtn.com. Le second est le portail. Sandbox : momodeveloper.mtn.com. Production : momoapi.mtn.com avec sous-domaine pays. Le troisième est le header d’environnement X-Target-Environment : valeur sandbox en test, libellé pays-spécifique en prod (mtnghana, mtnuganda, mtnivorycoast, mtncameroon, mtnbenin, mtncongo, mtnzambia, mtnliberia, mtnguineaconakry, mtnswaziland, mtnsouthafrica). Le quatrième est la devise : sandbox accepte EUR, prod impose la devise locale (GHS pour Ghana, UGX pour Uganda, XAF pour Cameroun et Congo, XOF pour Côte d’Ivoire/Bénin/Guinée Conakry, LRD pour Liberia, ZAR pour Afrique du Sud, ZMW pour Zambie, RWF pour Rwanda). Le cinquième est la génération des credentials : auto-service en sandbox, validation humaine en prod. Le sixième est la gestion des numéros : numéros fictifs acceptés en sandbox, numéros d’abonnés réels obligatoires en prod.
Un code qui factorise ces six paramètres dans une configuration externe (variables d’environnement, fichier de config chargé au boot) est portable d’un environnement à l’autre. Un code qui hardcode l’hôte sandbox au milieu d’un service métier vous oblige à grep-replace au moment le plus critique.
Étape 2 — Préparer le dossier KYC business
Avant d’ouvrir le portail production, rassemblez le dossier KYC complet. Les pièces récurrentes demandées par MTN d’un pays à l’autre sont les suivantes : copie certifiée du registre de commerce, pièce d’identité du représentant légal, attestation fiscale en cours de validité, RIB du compte de versement (le compte bancaire vers lequel MTN versera vos collectes), modèle d’opération (description courte de votre cas d’usage), et estimation du volume mensuel et de la valeur moyenne par transaction.
Cette estimation est importante : elle conditionne les plafonds que MTN appliquera à votre compte marchand. Sous-estimer vous expose à des blocages dès les premiers jours de forte activité ; sur-estimer ralentit l’instruction du dossier. Visez une projection réaliste avec une marge de sécurité de 30 %.
Préparez aussi un document décrivant votre architecture de sécurité : modèle d’authentification utilisateur, isolation des credentials, journal d’audit, plan de réponse aux incidents. MTN n’exige pas formellement ces documents dans tous les pays mais leur disponibilité accélère l’instruction si un compliance officer pose des questions.
Étape 3 — Ouvrir le compte sur le portail pays
Une fois le dossier prêt, contactez votre account manager MTN local. Si vous n’en avez pas, le formulaire de contact sur momoapi.mtn.com et le support du portail sandbox redirigent vers la personne du pays. Vous recevrez par email un identifiant initial et un mot de passe temporaire pour momoapi.mtn.com ou son sous-domaine pays.
La première connexion vous demande de définir un mot de passe robuste et de configurer une authentification OTP par SMS sur un numéro MTN du pays. Cette double authentification est obligatoire : vous ne pourrez pas consulter ni régénérer vos clés sans accès au numéro enregistré. Choisissez un numéro pro qui ne risque pas de changer (idéalement attribué à l’équipe et non à une personne).
Une fois connecté, vous arrivez sur un dashboard partenaire similaire au sandbox mais avec des sections supplémentaires : Subscription Keys (vos clés par produit), API User (votre identifiant API), Callback Configuration (URLs autorisées), Reports (rapports financiers), IP Whitelist (selon les pays).
Étape 4 — Configurer le callback en production
Dans la section API Access du portail, cliquez sur Create API User. Le formulaire demande une Callback Host (typiquement le domaine racine, par exemple api.votre-domaine.com) et une Payment Server URL (l’endpoint exact de réception des callbacks, par exemple https://api.votre-domaine.com/payments/mtn/webhook).
Le serveur de callback doit déjà être fonctionnel en HTTPS valide (certificat délivré par une autorité reconnue, pas un certificat auto-signé). Testez avant de valider le formulaire : MTN effectue un ping côté serveur pour vérifier la joignabilité. Une URL injoignable au moment de la validation bloque la création.
Le portail génère ensuite votre API_User (UUID v4 unique pour votre compte) et une API_Key statique. Stockez ces deux valeurs immédiatement dans votre coffre à secrets — la API_Key n’est plus affichée en clair après la première vue. Si vous la perdez, vous devrez la régénérer depuis le portail, ce qui invalide la précédente.
Étape 5 — Souscrire aux produits et récupérer les subscription keys
MTN MoMo Open API se compose de trois produits indépendants : Collection pour encaisser un paiement client, Disbursement pour verser à un bénéficiaire, Remittance pour les transferts internationaux. Chaque produit a sa propre paire de clés Primary et Secondary. Le portail propose une vue Products où vous cliquez sur chaque produit que vous utilisez et confirmez l’abonnement.
Une fois abonné, la clé Primary s’affiche dans la section My Subscriptions. Cette clé doit être envoyée dans chaque appel API via le header Ocp-Apim-Subscription-Key. La clé Secondary permet une rotation sans interruption : vous remplacez Primary par Secondary côté code, puis régénérez la nouvelle Primary, puis vous revenez sur Primary. Cette mécanique est précieuse en cas de fuite suspectée.
Étape 6 — Adapter le code pour la production
Le code applicatif est presque identique à celui du sandbox, mais quatre paramètres changent en runtime. Voici un exemple Python minimaliste qui résume la configuration à externaliser :
import os
import uuid
import base64
import requests
MOMO_HOST = os.environ["MOMO_HOST"] # proxy.momoapi.mtn.com en prod
MOMO_ENV = os.environ["MOMO_ENV"] # mtnghana, mtnuganda, etc.
MOMO_SUB_KEY = os.environ["MOMO_COLLECTION_KEY"]
MOMO_API_USER = os.environ["MOMO_API_USER"]
MOMO_API_KEY = os.environ["MOMO_API_KEY"]
MOMO_CURRENCY = os.environ["MOMO_CURRENCY"] # GHS, UGX, XAF, etc.
def get_access_token():
basic = base64.b64encode(
f"{MOMO_API_USER}:{MOMO_API_KEY}".encode()
).decode()
r = requests.post(
f"https://{MOMO_HOST}/collection/token/",
headers={
"Authorization": f"Basic {basic}",
"Ocp-Apim-Subscription-Key": MOMO_SUB_KEY,
"Content-Type": "application/json",
},
json={}, # MTN exige un body JSON même vide
timeout=10,
)
r.raise_for_status()
return r.json()["access_token"]
def request_to_pay(amount, payer_msisdn, external_id, payer_message):
token = get_access_token()
ref = str(uuid.uuid4())
r = requests.post(
f"https://{MOMO_HOST}/collection/v1_0/requesttopay",
headers={
"Authorization": f"Bearer {token}",
"X-Reference-Id": ref,
"X-Target-Environment": MOMO_ENV,
"Ocp-Apim-Subscription-Key": MOMO_SUB_KEY,
"Content-Type": "application/json",
},
json={
"amount": str(amount),
"currency": MOMO_CURRENCY,
"externalId": external_id,
"payer": {"partyIdType": "MSISDN", "partyId": payer_msisdn},
"payerMessage": payer_message,
"payeeNote": "",
},
timeout=15,
)
r.raise_for_status() # MTN renvoie 202 Accepted, la transaction est mise en file
return ref
Deux précisions importantes sur les paramètres. Le payer_msisdn doit être au format E.164 sans le + initial — par exemple "233241234567" pour un numéro ghanéen, "237671234567" pour un numéro camerounais. Inclure le + donne une 400. Le champ amount est une chaîne ; pour les devises sans décimales (XOF, XAF) passez un entier converti en chaîne, pour les devises avec décimales (GHS, UGX en cents) formatez avec deux chiffres après la virgule (par exemple f"{amount:.2f}") — str(100.50) renvoie "100.5" et fait sauter une décimale. Cette fonction renvoie le X-Reference-Id que vous stockez en base comme clé d’idempotence. Si vous re-déclenchez la même initiation pour cause de timeout réseau, réutilisez le même X-Reference-Id : MTN renvoie alors un 409 que vous interprétez comme « transaction déjà en cours » plutôt que de créer un doublon.
Le statut s’obtient ensuite via un GET sur /collection/v1_0/requesttopay/{ref}. La réponse JSON contient un champ status qui prend les valeurs PENDING, SUCCESSFUL ou FAILED. Le callback envoyé par MTN à l’URL configurée arrive en parallèle ; les deux mécanismes (poll et webhook) sont complémentaires, pas redondants.
Intégrer MTN MoMo dans d’autres stacks
Le code de l’étape 6 est en Python, mais la mécanique se transpose à n’importe quelle stack capable de faire un POST HTTPS avec deux entêtes (Authorization + Ocp-Apim-Subscription-Key) et un body JSON. Voici la même logique requestToPay portée vers Node.js, Laravel et Django, plus un client Angular qui appelle votre backend (jamais MTN directement, les credentials ne quittent pas le serveur).
Node.js / Express (axios)
import express from 'express';
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
const app = express();
app.use(express.json());
const {
MOMO_HOST, MOMO_ENV, MOMO_COLLECTION_KEY,
MOMO_API_USER, MOMO_API_KEY, MOMO_CURRENCY,
} = process.env;
async function getAccessToken() {
const basic = Buffer.from(`${MOMO_API_USER}:${MOMO_API_KEY}`).toString('base64');
const { data } = await axios.post(
`https://${MOMO_HOST}/collection/token/`,
{},
{
headers: {
Authorization: `Basic ${basic}`,
'Ocp-Apim-Subscription-Key': MOMO_COLLECTION_KEY,
'Content-Type': 'application/json',
},
timeout: 10000,
}
);
return data.access_token;
}
async function requestToPay({ amount, msisdn, externalId, message }) {
const token = await getAccessToken();
const ref = uuidv4();
await axios.post(
`https://${MOMO_HOST}/collection/v1_0/requesttopay`,
{
amount: String(amount),
currency: MOMO_CURRENCY,
externalId,
payer: { partyIdType: 'MSISDN', partyId: msisdn },
payerMessage: message,
payeeNote: '',
},
{
headers: {
Authorization: `Bearer ${token}`,
'X-Reference-Id': ref,
'X-Target-Environment': MOMO_ENV,
'Ocp-Apim-Subscription-Key': MOMO_COLLECTION_KEY,
'Content-Type': 'application/json',
},
timeout: 15000,
}
);
return ref;
}
app.post('/payments/initiate', async (req, res) => {
try {
const ref = await requestToPay({
amount: req.body.amount,
msisdn: req.body.msisdn,
externalId: req.body.orderId,
message: `Commande ${req.body.orderId}`,
});
res.json({ ok: true, reference: ref });
} catch (e) {
res.status(502).json({ ok: false, error: e.response?.data || e.message });
}
});
app.listen(3000);
Ce module Express expose un endpoint POST /payments/initiate qui prend amount + msisdn + orderId, déclenche le push USSD côté abonné, et retourne le reference pour suivre la transaction. Le mapping amount → String(amount) conserve la décimale exigée par MTN pour les devises avec sub-unités (GHS, UGX en cents).
Laravel (PHP 8.2+ avec Http facade)
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
class MomoCollectionService
{
private string $host;
private string $env;
private string $subKey;
private string $apiUser;
private string $apiKey;
private string $currency;
public function __construct()
{
$this->host = config('momo.host');
$this->env = config('momo.env');
$this->subKey = config('momo.collection_key');
$this->apiUser = config('momo.api_user');
$this->apiKey = config('momo.api_key');
$this->currency = config('momo.currency');
}
private function token(): string
{
$basic = base64_encode("{$this->apiUser}:{$this->apiKey}");
return Http::withHeaders([
'Authorization' => "Basic {$basic}",
'Ocp-Apim-Subscription-Key' => $this->subKey,
'Content-Type' => 'application/json',
])->timeout(10)
->post("https://{$this->host}/collection/token/", new \stdClass())
->throw()
->json('access_token');
}
public function requestToPay(string $amount, string $msisdn, string $externalId, string $message): string
{
$token = $this->token();
$ref = (string) Str::uuid();
Http::withHeaders([
'Authorization' => "Bearer {$token}",
'X-Reference-Id' => $ref,
'X-Target-Environment' => $this->env,
'Ocp-Apim-Subscription-Key' => $this->subKey,
'Content-Type' => 'application/json',
])->timeout(15)->post("https://{$this->host}/collection/v1_0/requesttopay", [
'amount' => $amount,
'currency' => $this->currency,
'externalId' => $externalId,
'payer' => ['partyIdType' => 'MSISDN', 'partyId' => $msisdn],
'payerMessage' => $message,
'payeeNote' => '',
])->throw();
return $ref;
}
}
Le service Laravel MomoCollectionService s’injecte dans un controller via le container ; les credentials viennent de config/momo.php alimenté par les variables d’environnement. La méthode throw() de la facade Http convertit les 4xx/5xx MTN en exceptions Laravel RequestException que vous attrapez dans un middleware métier.
Django REST framework
# settings.py
MOMO = {
'HOST': os.environ['MOMO_HOST'],
'ENV': os.environ['MOMO_ENV'],
'SUB_KEY': os.environ['MOMO_COLLECTION_KEY'],
'API_USER': os.environ['MOMO_API_USER'],
'API_KEY': os.environ['MOMO_API_KEY'],
'CURRENCY': os.environ['MOMO_CURRENCY'],
}
# payments/services.py
import base64
import uuid
import requests
from django.conf import settings
class MomoService:
def _token(self) -> str:
cfg = settings.MOMO
basic = base64.b64encode(f"{cfg['API_USER']}:{cfg['API_KEY']}".encode()).decode()
r = requests.post(
f"https://{cfg['HOST']}/collection/token/",
headers={
'Authorization': f'Basic {basic}',
'Ocp-Apim-Subscription-Key': cfg['SUB_KEY'],
'Content-Type': 'application/json',
},
json={}, timeout=10,
)
r.raise_for_status()
return r.json()['access_token']
def request_to_pay(self, amount, msisdn, external_id, message) -> str:
cfg = settings.MOMO
token = self._token()
ref = str(uuid.uuid4())
r = requests.post(
f"https://{cfg['HOST']}/collection/v1_0/requesttopay",
headers={
'Authorization': f'Bearer {token}',
'X-Reference-Id': ref,
'X-Target-Environment': cfg['ENV'],
'Ocp-Apim-Subscription-Key': cfg['SUB_KEY'],
'Content-Type': 'application/json',
},
json={
'amount': str(amount), 'currency': cfg['CURRENCY'],
'externalId': external_id,
'payer': {'partyIdType': 'MSISDN', 'partyId': msisdn},
'payerMessage': message, 'payeeNote': '',
},
timeout=15,
)
r.raise_for_status()
return ref
# payments/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .services import MomoService
class InitiatePayment(APIView):
def post(self, request):
try:
ref = MomoService().request_to_pay(
amount=request.data['amount'],
msisdn=request.data['msisdn'],
external_id=request.data['order_id'],
message=f"Commande {request.data['order_id']}",
)
return Response({'ok': True, 'reference': ref})
except requests.HTTPError as e:
return Response({'ok': False, 'error': str(e)}, status=status.HTTP_502_BAD_GATEWAY)
Le service MomoService est isolé dans payments/services.py pour rester testable ; les tests Django mocquent requests.post avec responses ou httpretty. La vue InitiatePayment reste fine et déléguée. Pour le webhook côté Django, exposez une vue CallbackView en POST sur l’URL configurée côté portail MTN, validez le payload et mettez à jour le statut de la commande en transaction.
Spring Boot 3.2+ (Java 17, RestClient)
// application.yml
momo:
host: ${MOMO_HOST} # proxy.momoapi.mtn.com en prod
env: ${MOMO_ENV}
sub-key: ${MOMO_COLLECTION_KEY}
api-user: ${MOMO_API_USER}
api-key: ${MOMO_API_KEY}
currency: ${MOMO_CURRENCY}
// MomoProperties.java
@ConfigurationProperties(prefix = "momo")
public record MomoProperties(
String host, String env, String subKey,
String apiUser, String apiKey, String currency
) {}
// MomoCollectionService.java
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.UUID;
@Service
public class MomoCollectionService {
private final MomoProperties cfg;
private final RestClient restClient;
public MomoCollectionService(MomoProperties cfg, RestClient.Builder builder) {
this.cfg = cfg;
this.restClient = builder.build();
}
private String getAccessToken() {
String basic = Base64.getEncoder().encodeToString(
(cfg.apiUser() + ":" + cfg.apiKey()).getBytes(StandardCharsets.UTF_8)
);
TokenResponse t = restClient.post()
.uri("https://{host}/collection/token/", cfg.host())
.header("Authorization", "Basic " + basic)
.header("Ocp-Apim-Subscription-Key", cfg.subKey())
.contentType(MediaType.APPLICATION_JSON)
.body(Map.of())
.retrieve()
.body(TokenResponse.class);
return t.accessToken();
}
public String requestToPay(BigDecimal amount, String msisdn,
String externalId, String message) {
String token = getAccessToken();
String ref = UUID.randomUUID().toString();
var payload = new RequestToPayPayload(
amount.toPlainString(), cfg.currency(), externalId,
new Payer("MSISDN", msisdn), message, ""
);
restClient.post()
.uri("https://{host}/collection/v1_0/requesttopay", cfg.host())
.header("Authorization", "Bearer " + token)
.header("X-Reference-Id", ref)
.header("X-Target-Environment", cfg.env())
.header("Ocp-Apim-Subscription-Key", cfg.subKey())
.contentType(MediaType.APPLICATION_JSON)
.body(payload)
.retrieve()
.toBodilessEntity(); // MTN renvoie 202 Accepted
return ref;
}
public record TokenResponse(@JsonProperty("access_token") String accessToken) {}
public record RequestToPayPayload(
String amount, String currency, String externalId,
Payer payer, String payerMessage, String payeeNote
) {}
public record Payer(String partyIdType, String partyId) {}
}
// PaymentController.java
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.Map;
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
private final MomoCollectionService momo;
public PaymentController(MomoCollectionService momo) {
this.momo = momo;
}
@PostMapping("/initiate")
public Map<String, Object> initiate(@RequestBody InitiateRequest req) {
String ref = momo.requestToPay(
req.amount(), req.msisdn(), req.orderId(),
"Commande " + req.orderId()
);
return Map.of("ok", true, "reference", ref);
}
public record InitiateRequest(BigDecimal amount, String msisdn, String orderId) {}
}
// MomoApplication.java — enable @ConfigurationProperties
@SpringBootApplication
@EnableConfigurationProperties(MomoProperties.class)
public class MomoApplication {
public static void main(String[] args) {
SpringApplication.run(MomoApplication.class, args);
}
}
Cette implémentation Spring Boot 3.2+ utilise RestClient — l’API HTTP synchrone moderne introduite avec Spring 6.1 qui remplace RestTemplate (en maintenance) et reste plus simple que WebClient pour un appel POST classique. Les credentials sont injectés via @ConfigurationProperties typé sous forme de record Java 17, ce qui garantit l’immutabilité et la validation au démarrage. Le format Map.of() sérialise le body {} exigé par le endpoint /token/, et toBodilessEntity() ignore le corps de réponse vide du 202 Accepted renvoyé par MTN sur un requestToPay réussi. Pour la gestion d’erreurs, ajoutez un .onStatus(HttpStatusCode::is4xxClientError, ...) avant retrieve() qui mappe les 4xx MTN vers vos exceptions métier (MomoAuthException, MomoConflictException, etc.).
Bonnes pratiques transversales
Quel que soit le langage, trois règles s’appliquent. Idempotence stricte : l’X-Reference-Id est généré côté backend et stocké en base avec l’ID de commande avant l’appel — un retry réseau réutilise le même UUID, MTN renverra alors 409 que vous traitez comme « déjà en cours ». Timeouts agressifs : 10 s pour le token, 15 s pour le requestToPay, 30 s pour le webhook handler côté votre serveur. Logs structurés : chaque appel sortant logge le X-Reference-Id, l’externalId, le statut HTTP et le code d’erreur MTN — sans cela le débogage en production avec un MTN qui répond 500 sans corps explicite devient impossible.
Étape 7 — Tester en production avec un compte interne
Une fois les credentials production en place, ne testez jamais sur un client réel comme premier essai. Créez un compte MoMo interne avec un solde minimal sur un téléphone réservé à cet usage, puis exécutez un cycle complet sandbox-équivalent en prod. Vérifiez que vous recevez le push USSD, que vous saisissez le PIN, que le statut bascule à SUCCESSFUL, que le callback est reçu, que votre logique métier applique l’effet attendu, et que le rapport MTN affiche la transaction.
Exécutez ce cycle pour les trois cas de figure : succès, refus du PIN, expiration du push (l’utilisateur ne répond pas). Chacun mène à un état distinct dans votre base et chacun doit être géré explicitement. Un état non géré crée des transactions zombies qui polluent vos rapports et vos réconciliations.
# Test rapide d'authentification depuis le serveur de production
curl -X POST "https://proxy.momoapi.mtn.com/collection/token/" \
-H "Authorization: Basic $(echo -n "$MOMO_API_USER:$MOMO_API_KEY" | base64 | tr -d '\n')" \
-H "Ocp-Apim-Subscription-Key: $MOMO_COLLECTION_KEY" \
-H "Content-Type: application/json" \
-d '{}'
Cette commande doit renvoyer un JSON contenant access_token, token_type: "access_token" et expires_in en secondes (typiquement 3600). Une 401 indique un problème de credentials ou de subscription key, une 403 signale plus souvent un problème de whitelisting IP, et un timeout pointe vers un blocage réseau côté pare-feu.
Étape 8 — Surveiller le go-live les premières 72 heures
Les pannes opérationnelles arrivent rarement le jour 1 ; elles arrivent au jour 3 ou 7, quand le volume monte et que des cas limites se manifestent. Instrumentez quatre alertes dès le go-live : taux d’erreur HTTP par endpoint, latence p95 sur /collection/token/ et /collection/v1_0/requesttopay/, taux de transactions restées en PENDING plus de cinq minutes, écart entre votre journal interne et le rapport quotidien MTN.
Le job de réconciliation horaire est un filet de sécurité essentiel. Toute transaction restée en PENDING depuis plus de cinq minutes doit être interrogée via GET. Si l’opérateur l’a finalisée à SUCCESSFUL mais que vous n’avez pas reçu le callback, vous appliquez l’effet métier sur la base de la valeur lue. Si elle reste PENDING plus d’une heure, considérez la transaction comme expirée et déclenchez votre flux d’annulation côté applicatif.
Erreurs fréquentes
| Erreur | Cause probable | Solution |
|---|---|---|
| 401 Unauthorized sur /token/ | API_Key périmée ou subscription_key invalide | Régénérer dans le portail, mettre à jour le coffre à secrets |
| 403 Forbidden inattendu | Callback Host non whitelisté ou IP serveur inconnue | Vérifier la configuration dans le portail et l’IP sortante effective |
| 400 Bad Request sur requesttopay | X-Target-Environment incorrect ou devise non supportée | Vérifier le libellé pays exact et utiliser la devise locale |
| 409 Conflict | X-Reference-Id réutilisé pour une transaction différente | Générer un nouvel UUID v4 par transaction métier |
| Webhook jamais reçu | HTTPS invalide ou callback URL bloquée | Vérifier la chaîne TLS côté curl, désactiver IPv6 si suspect |
| Statut PENDING permanent | Payer n’a pas répondu au push USSD | Considérer la transaction comme expirée après 60 secondes |
FAQ
Peut-on continuer à utiliser le sandbox après le go-live ? Oui, et c’est recommandé. Gardez deux jeux de credentials et un flag d’environnement. Les nouvelles features se valident d’abord en sandbox.
La rotation des clés casse-t-elle l’API en cours ? Si vous utilisez la Secondary key pour basculer puis régénérer la Primary, vous évitez toute coupure. Évitez de régénérer les deux clés simultanément.
Doit-on s’abonner aux trois produits dès le départ ? Non. Souscrivez uniquement aux produits effectivement utilisés. Collection seul suffit pour la majorité des cas e-commerce.
Que faire si MTN refuse le dossier KYC ? La cause est presque toujours un document manquant ou une incohérence entre RIB, registre de commerce et nom du représentant. Reprenez le dossier point par point avec l’account manager.
Ressources officielles
- Portail développeur sandbox — momodeveloper.mtn.com
- Portail production — momoapi.mtn.com
- Documentation API publique — api-documentation
- Guide d’authentification — RFC 7235 HTTP Authentication
Article de référence : Intégrer les APIs Mobile Money en production : Wave, Orange Money, MTN MoMo, Moov. Tutoriels frères : Wave Business API en production, Orange Money Web Payment en production.
À combiner avec : connecter Orange Money à votre site marchand pour offrir un parcours d’achat sans friction.