Guide d'intégration API
Ce guide décrit le contrat de l'API NguePay tel qu'il est déployé. Tous les exemples visent la sandbox (recette). Version HTML : https://www.nguepay.com/docs/integration.html — ce fichier : https://www.nguepay.com/docs/integration.md
Démarrage
| Élément | Sandbox (recette) | Production |
|---|---|---|
| API REST | https://api.sandbox.nguepay.com/api/v1 |
https://api.live.nguepay.com/api/v1 |
| Page de paiement hébergée | https://pay.sandbox.nguepay.com/pay/{uuid} |
https://pay.live.nguepay.com/pay/{uuid} |
| Dashboard marchand (clés, webhooks) | https://dashboard.sandbox.nguepay.com |
https://dashboard.live.nguepay.com |
Clés API — à générer dans le dashboard, rubrique Clés API. Chaque clé a deux parties :
sk_sandbox_…(clé secrète) : appels serveur à serveur uniquement. Ne jamais l'exposer dans un navigateur ou une application mobile.pk_sandbox_…(clé publique) : appels depuis le navigateur. Elle n'est acceptée que si un domaine autorisé a été déclaré sur la clé dans le dashboard (sinon erreurNGP015, HTTP 403). Elle ne sert qu'à créer une session de paiement.
Les endpoints du dashboard (/api/v1/merchant/webhook, /api/v1/keys, profil…) ne prennent aucune de ces clés : ils exigent la session de connexion au dashboard (valable 12 h ; code de double authentification demandé si elle est activée sur le compte), et répondent 401 NGP001 à une clé API. De même, les coordonnées de notification (notification_email, notification_phone) ne se modifient pas avec une clé API (403 NGP023). Pour gérer vos webhooks par l'API, utilisez /api/v1/webhook-endpoints avec la clé secrète (section 3).
Authentification — un seul en-tête, quelle que soit la clé :
Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Accept: application/json
Clé absente, invalide ou révoquée → HTTP 401, NGP001. Marchand suspendu → HTTP 403, NGP020. Test rapide de la clé : GET /api/v1/ping (réponse pong).
Montants — envoyés en XOF entiers (5000 = 5 000 F CFA), entre 100 et 5 000 000. Dans les réponses de l'API, amount_xof est en XOF entiers et amount en centimes (amount_xof × 100) : utilisez amount_xof.
Enveloppe des réponses — toujours la même forme :
{ "success": true, "data": { … }, "message": "Paiement initié." }
{ "success": false, "message": "Clé API invalide.", "error_code": "NGP001" }
Les erreurs de validation renvoient HTTP 422, "error_code": "NGP_VALIDATION" et le détail par champ dans errors.
1. Créer un paiement
Deux façons de faire, selon qui choisit le moyen de paiement. Dans les deux cas, gardez l'uuid renvoyé : c'est la référence NguePay du paiement.
| A. Page de paiement NguePay (recommandé) | B. Paiement direct par opérateur | |
|---|---|---|
| Qui choisit l'opérateur | Le client, sur la page NguePay | Votre serveur (provider obligatoire) |
| Endpoint | POST /api/v1/checkout/sessions |
POST /api/v1/payments |
| Clé | publique pk_sandbox_… (navigateur, domaine déclaré) ou secrète sk_sandbox_… (depuis votre serveur uniquement, sans contrôle de domaine ; refusée — HTTP 401, NGP001 — si l'appel porte un en-tête Origin, comme tout appel de navigateur : un serveur relais qui retransmet Origin doit le retirer) |
secrète sk_sandbox_… |
| URL à ouvrir | checkout_url = https://pay.sandbox.nguepay.com/pay/{uuid} |
checkout_url = page de l'opérateur (page de paiement Orange Money, ou Stripe Checkout pour la carte) ; en sandbox, souvent la page de simulation NguePay |
| Opérateur lancé | quand le client valide son choix | immédiatement |
A. Page de paiement hébergée — POST /api/v1/checkout/sessions
| Champ | Type | Obligatoire | Règle |
|---|---|---|---|
amount |
entier | oui | XOF entiers, 100 à 5 000 000 |
reference |
chaîne | oui | votre référence de commande, ≤ 150 caractères, unique par marchand |
currency |
chaîne | non | XOF uniquement |
provider |
chaîne | non | orange ou stripe : présélectionne l'opérateur (wave, mtn, pawapay : temporairement indisponibles, refusés avec NGP003) |
phone |
chaîne | non | +225 + 10 chiffres, ex. +2250700000000 |
customer_email |
non | ≤ 180 caractères | |
description |
chaîne | non | ≤ 255 caractères |
return_url |
URL | non | où ramener le client après paiement, ≤ 500 caractères — renvoyée telle quelle (voir « Retour du client ») ; avec la clé publique, même origine que le domaine déclaré sur la clé |
metadata |
objet | non | vos données libres, renvoyées telles quelles ; 20 clés et 4 Ko (JSON encodé) au plus ; avec la clé publique, un metadata.return_url suit la même règle que return_url |
curl -X POST https://api.sandbox.nguepay.com/api/v1/checkout/sessions \
-H "Authorization: Bearer pk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"amount": 5000, "reference": "CMD-1001", "description": "Commande 1001", "return_url": "https://boutique.example/merci"}'
Réponse 201 (ou 200 si la même reference est rejouée à l'identique : vous recevez la session existante) :
{
"success": true,
"data": {
"uuid": "9d3f2b1a-6c4e-4f8a-b2d1-0e7c5a9f1234",
"checkout_url": "https://pay.sandbox.nguepay.com/pay/9d3f2b1a-6c4e-4f8a-b2d1-0e7c5a9f1234",
"expires_at": "2026-10-14T10:30:00+00:00",
"status": "pending"
},
"message": "Session de paiement créée."
}
Redirigez le client vers checkout_url. La session expire 30 minutes après sa création.
Avec la clé publique, un return_url (ou metadata.return_url) d'une autre origine que le domaine déclaré sur la clé — schéma, hôte et port — est refusé : HTTP 422, NGP_VALIDATION, détail sur le champ fautif.
B. Paiement direct — POST /api/v1/payments (serveur uniquement)
Mêmes champs que A, avec ces différences :
providerest obligatoire :orange(Orange Money CI) oustripe(carte).wave,mtnetpawapayrestent dans le contrat mais sont temporairement indisponibles : un paiement qui les demande est refusé (HTTP 422,NGP003).phone(≤ 20 caractères) est recommandé : le client paie sur la page Orange Money ou Stripe ouverte parcheckout_url.emailremplacecustomer_email.webhook_url(optionnel) : URL publique qui recevra les webhooks de ce paiement à la place des destinations du compte. Mêmes règles qu'une destination (section 3, « URL acceptées »).
curl -X POST https://api.sandbox.nguepay.com/api/v1/payments \
-H "Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"amount": 5000, "provider": "orange", "reference": "CMD-1002", "phone": "+2250700000089", "return_url": "https://boutique.example/merci"}'
Réponse 201 (200 si rejouée à l'identique) :
{
"success": true,
"data": {
"transaction_id": "9d3f2b1a-7a10-4c2e-9f51-3b8d6e2c5678",
"uuid": "9d3f2b1a-7a10-4c2e-9f51-3b8d6e2c5678",
"status": "pending",
"provider": "orange",
"provider_code": "orange",
"amount": 500000,
"amount_xof": 5000,
"refunded_amount_xof": 0,
"currency": "XOF",
"reference": "CMD-1002",
"phone": "+225 07 *** ** 89",
"checkout_url": "https://pay.sandbox.nguepay.com/pay/9d3f2b1a-7a10-4c2e-9f51-3b8d6e2c5678/simulate",
"attempts": 0,
"metadata": [],
"failure_reason": null,
"paid_at": null,
"expires_at": "2026-10-14T10:30:00.000000Z",
"created_at": "2026-10-14T10:00:00.000000Z",
"updated_at": "2026-10-14T10:00:01.000000Z",
"links": { "self": "/api/v1/transactions/9d3f2b1a-…", "refund": "/api/v1/transactions/9d3f2b1a-…/refund" }
},
"message": "Paiement initié."
}
Champs de la réponse :
| Champ | Type | Contenu |
|---|---|---|
transaction_id, uuid |
uuid | identifiant NguePay ; transaction_id est un alias historique, toujours égal à uuid |
status |
chaîne | voir section 2 |
provider, provider_code |
chaîne | null | opérateur ; provider est un alias historique ; null pour une session dont le client n'a pas encore choisi |
amount |
entier | montant en centimes (500000 = 5 000 XOF) |
amount_xof |
entier | montant en XOF entiers — celui à afficher |
refunded_amount_xof |
entier | cumul remboursé, en XOF |
currency |
chaîne | XOF |
reference |
chaîne | votre référence |
phone |
chaîne | null | numéro masqué (+225 07 *** ** 89), null si non fourni |
checkout_url |
URL | null | page de l'opérateur ; null pour une validation sur le téléphone, et pour une session tant que le client n'a pas choisi (ce n'est pas l'URL de la page NguePay) |
attempts |
entier | refus récupérables sur la page de paiement (numéro refusé, opérateur indisponible) ; 0 via cet endpoint |
metadata |
objet | vos données, telles qu'envoyées — [] (tableau vide) si vous n'en avez pas envoyé |
failure_reason |
chaîne | null | motif d'échec |
paid_at |
date | null | date d'encaissement |
expires_at, created_at, updated_at |
date | ISO 8601 UTC (…000000Z) |
links |
objet | self et refund : chemins relatifs, sans hôte |
Pour Stripe, la réponse peut aussi contenir merchant (uuid, merchant_code, name de votre compte) ; ce champ n'est pas renvoyé de façon constante, ne l'utilisez pas. GET /api/v1/transactions/{uuid} ajoute logs, l'historique des statuts. Référence complète : objets renvoyés.
Idempotence — la reference est unique par marchand. La rejouer avec le même montant rend le paiement existant (HTTP 200, aucun second débit) ; avec un montant différent, elle est refusée (HTTP 422, NGP005).
Retour du client — le client est renvoyé sur return_url exactement telle que vous l'avez envoyée : NguePay n'y ajoute aucun paramètre (ni statut, ni uuid, ni référence, ni signature), et il n'existe pas de success_url / cancel_url côté marchand — les opérateurs ramènent toujours le client sur les pages NguePay, qui proposent return_url. Mettez-y vous-même ce qui vous permet de retrouver la commande, par exemple https://boutique.example/retour?commande=CMD-1001, puis retrouvez la transaction depuis votre serveur avec GET /api/v1/transactions?reference=CMD-1001 ou GET /api/v1/payments/{uuid}. Sans return_url, metadata.return_url est utilisé s'il existe. Quand checkout_url vaut null (validation sur le téléphone), aucune page NguePay n'est affichée et return_url ne sert pas.
| Issue | Retour vers return_url |
|---|---|
| Paiement réussi | redirection automatique après 5 secondes, et bouton « Retour » |
| Paiement échoué ou expiré | bouton « Retour sur {marchand} » |
| Paiement annulé, ou client qui abandonne sur la page de choix | bouton « Retour au site marchand » |
Ce retour ne prouve pas le paiement, et il arrive aussi après un échec : confirmez toujours par le webhook ou par GET /api/v1/payments/{uuid} (section 2), avec l'uuid de la session — c'est le même identifiant.
2. Consulter le statut d'un paiement
GET /api/v1/payments/{uuid} avec la clé secrète renvoie le paiement complet, au même format que la réponse de création (section 1.B). Un uuid inconnu, ou appartenant à un autre marchand, renvoie HTTP 404.
curl https://api.sandbox.nguepay.com/api/v1/payments/9d3f2b1a-7a10-4c2e-9f51-3b8d6e2c5678 \
-H "Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -H "Accept: application/json"
À lire dans data : status, amount_xof, paid_at, failure_reason, refunded_amount_xof.
status |
Signification | Final ? | À faire côté marchand |
|---|---|---|---|
pending |
Créé, le client n'a pas encore payé | non | attendre |
processing |
Paiement lancé chez l'opérateur, en attente de confirmation | non | attendre (ne pas livrer) |
succeeded |
Argent encaissé (paid_at renseigné) |
oui | livrer la commande |
failed |
Refusé, abandonné ou expiré — motif dans failure_reason (ex. expired, insufficient_funds, initiation_failed, cancelled) |
oui | proposer un nouveau paiement |
expired |
Expiré (statut réservé ; l'expiration automatique produit aujourd'hui failed + failure_reason: "expired") |
oui | idem failed |
canceled |
Annulé par le marchand (POST /api/v1/transactions/{uuid}/cancel, sans webhook) ou par l'opérateur (webhook payment.canceled) |
oui | — |
Sur la page de paiement, un numéro refusé par l'opérateur ou un opérateur momentanément indisponible ne clôt pas le paiement : le client corrige son numéro ou choisit un autre moyen, et le paiement reste pending (aucun webhook). Seul un refus définitif le passe en failed, avec le webhook payment.failed.
Un statut final ne change plus jamais. Un remboursement ne modifie pas status (le paiement reste succeeded) : il augmente refunded_amount_xof. Une confirmation d'opérateur d'un montant différent de celui du paiement n'est jamais créditée.
Annulation — POST /api/v1/transactions/{uuid}/cancel n'accepte qu'un paiement pending. NguePay interroge d'abord l'opérateur et refuse l'annulation (HTTP 422, message explicite) si l'opérateur indique que le client a payé, si une validation est en attente sur le téléphone du client, si l'opérateur est injoignable, ou si sa page de paiement hébergée ne peut pas être fermée. Dans ces cas, réessayez plus tard ou laissez le paiement expirer.
Recommandation : fiez-vous au webhook (section 3) et utilisez cet endpoint en complément — au retour du client, ou si aucun webhook n'est arrivé après quelques minutes.
3. Webhooks
NguePay envoie un POST JSON signé HMAC-SHA256 sur "{timestamp}.{corps brut}", avec le secret de la destination (wh_…). Le résultat est en hexadécimal minuscule dans l'en-tête X-NguePay-Signature.
Déclarer une destination — dans le dashboard (Webhooks), ou par l'API avec la clé secrète : POST /api/v1/webhook-endpoints avec {"url": "https://boutique.example/webhooks/nguepay", "events": ["payment.succeeded", "payment.failed"]}. Sans events, la destination reçoit tout. Jusqu'à 5 destinations par compte. Le secret wh_… n'est affiché qu'une fois, à la création ; POST /api/v1/webhook-endpoints/{uuid}/rotate-secret en génère un nouveau et POST /api/v1/webhook-endpoints/{uuid}/test envoie un événement de test.
URL acceptées — HTTPS obligatoire en production, port standard, aucun identifiant dans l'URL, et un hôte qui résout vers une adresse publique (adresses privées ou locales refusées). Les redirections (3xx) ne sont pas suivies : la livraison échoue. Déclarez l'URL finale.
| Événement | Quand |
|---|---|
payment.succeeded |
paiement encaissé |
payment.failed |
paiement refusé ou expiré |
payment.canceled |
paiement annulé côté opérateur |
refund.succeeded |
remboursement exécuté |
payout.succeeded / payout.failed / payout.canceled |
virement sortant conclu (y compris un virement Orange Money envoyé par l'équipe NguePay, ou refusé lors de la validation NguePay) |
webhook.test |
test déclenché depuis le dashboard ou l'API |
En-têtes envoyés
| En-tête | Contenu |
|---|---|
X-NguePay-Signature |
hex(HMAC_SHA256(secret, timestamp + "." + corps_brut)) |
X-NguePay-Timestamp |
horodatage Unix en secondes de cette tentative, utilisé dans la signature |
X-NguePay-Event |
nom de l'événement, ex. payment.succeeded |
X-NguePay-Delivery |
identifiant unique de la livraison (identique à chaque relance, propre à chaque destination ; = uuid de GET /api/v1/webhooks/{uuid}) |
Content-Type |
application/json |
Exemple de corps — payment.succeeded. Attention : ici amount est en centimes (500000 = 5 000 XOF).
{
"event": "payment.succeeded",
"data": {
"transaction_id": "9d3f2b1a-7a10-4c2e-9f51-3b8d6e2c5678",
"status": "succeeded",
"amount": 500000,
"currency": "XOF",
"reference": "CMD-1002",
"provider": "orange",
"paid_at": "2026-10-14T10:02:41+00:00"
}
}
Le corps ne contient que event et data : pas d'identifiant d'événement ni de date d'émission (ils sont dans les en-têtes). Les webhooks payment.* ne portent ni failure_reason, ni metadata, ni amount_xof — relisez la transaction si besoin. Exception Stripe : payment.succeeded et payment.failed d'un paiement Stripe ne portent que transaction_id, status et reference.
refund.succeeded porte {"transaction_id", "refund_id", "amount"} (amount : ce remboursement, en centimes) ; pour un remboursement fait depuis la console Stripe, s'y ajoutent status (statut de la transaction) et reference. Les événements payout.* (virements sortants, disponibles vers Orange Money ; certains passent d'abord par une validation de l'équipe NguePay — voir Virements sortants) portent payout_id, reference, status, review (null une fois le virement conclu), amount, amount_xof, currency, provider, beneficiary (masqué), failure_reason (dont rejected_by_review et manual_execution_failed), awaiting_operator_funds, processed_at. webhook.test porte message, endpoint (uuid de la destination) et sent_at. Détail : Référence des événements.
Vérifier la signature — sur le corps brut, avant tout JSON.parse, et en comparaison à temps constant :
// Node.js / Express : app.post('/webhooks/nguepay', express.raw({ type: 'application/json' }), handler)
const crypto = require('crypto');
function verifyNguepay(req, secret) {
const signature = req.get('X-NguePay-Signature') || '';
const timestamp = req.get('X-NguePay-Timestamp') || '';
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // anti-rejeu
const expected = crypto.createHmac('sha256', secret)
.update(`${timestamp}.${req.body.toString('utf8')}`)
.digest('hex');
return signature.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// PHP
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_NGUEPAY_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_NGUEPAY_SIGNATURE'] ?? '';
$ok = abs(time() - (int) $ts) <= 300
&& hash_equals(hash_hmac('sha256', $ts.'.'.$raw, $secret), $sig);
Répondre et dédoublonner — répondez 2xx en moins de 10 secondes, puis traitez en tâche de fond. En cas d'erreur réseau, de timeout ou de réponse 5xx, NguePay relance (3 tentatives : immédiate, puis 1 et 5 minutes plus tard ; les échecs restants sont rejoués périodiquement). Une réponse 3xx ou 4xx est définitive : aucune relance. Chaque relance est re-signée : nouvel X-NguePay-Timestamp, nouvelle signature, corps identique — la fenêtre anti-rejeu de 300 s ne rejette donc pas une relance légitime. X-NguePay-Delivery, lui, reste le même. Un même événement peut arriver deux fois : dédoublonnez avec X-NguePay-Delivery, et ne livrez une commande qu'une fois par transaction_id.
Override par paiement — un webhook_url passé à POST /api/v1/payments reçoit les événements de ce paiement à la place des destinations. Il est signé avec le secret du webhook principal du compte, visible dans le dashboard (rubrique Webhooks).
Codes d'erreur, tests sandbox et checklist
| Code | HTTP | Signification |
|---|---|---|
NGP001 |
401 | Clé API absente, invalide ou révoquée, ou clé secrète envoyée depuis un navigateur |
NGP_VALIDATION |
422 | Champs invalides — détail dans errors |
NGP003 |
422 / 502 | Opérateur indisponible |
NGP005 |
422 | Référence déjà utilisée pour un autre montant |
NGP007 |
429 | Trop de requêtes |
NGP008 |
404 | Ressource introuvable |
NGP009 |
422 | Refusé par l'anti-fraude (motif volontairement non détaillé) |
NGP010 |
422 | Opérateur non activé pour ce marchand |
NGP011 |
422 | Plafond mensuel dépassé (compte particulier : 3 000 000 XOF/mois) |
NGP015 |
403 | Clé publique sans domaine déclaré |
NGP020 |
403 | Compte marchand suspendu |
NGP023 |
403 | Action réservée au dashboard (coordonnées de notification modifiées avec une clé API) |
Tests sandbox : quand l'opérateur est simulé en sandbox, checkout_url mène à la page de simulation NguePay (…/pay/{uuid}/simulate), où vous choisissez l'issue — paiement réussi ou échoué — puis recevez le webhook correspondant. Quand la sandbox est branchée sur l'environnement de test de l'opérateur, checkout_url mène à sa page de test (pour la carte : cartes de test Stripe, ex. 4242 4242 4242 4242). Aucun argent réel ne bouge.
Moyens de paiement disponibles (production et sandbox) :
| Moyen | provider |
Statut |
|---|---|---|
| Orange Money CI | orange |
disponible — pas de remboursement par API (refus NGP002, montant rendu) |
| Carte bancaire (Stripe) | stripe |
disponible |
| Wave CI | wave |
bientôt disponible — refusé avec NGP003 en attendant |
| MTN MoMo CI | mtn |
bientôt disponible — refusé avec NGP003 en attendant |
Checklist d'intégration
- Clés sandbox générées ; la clé secrète reste côté serveur ; domaine déclaré sur la clé publique
- Création du paiement avec une
referenceunique par commande ;uuidNguePay enregistré - Destination webhook déclarée, secret
wh_…stocké, signature vérifiée sur le corps brut - Commande livrée uniquement sur
payment.succeeded(oustatus = succeededvia l'API), une seule fois - Test réussi, test refusé et test d'abandon (paiement laissé 30 minutes) joués en sandbox