Guide d'intégration API

Version texte, lisible par vos outils : integration.md.

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 :

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 e-mail 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 :

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

Besoin d'aide ?