Créer un paiement
L'appel serveur-à-serveur qui ouvre une transaction, et tout ce qui peut lui arriver ensuite.
Créer un paiement
/v1/paymentsAuthentification par clé secrète. Le provider est obligatoire : c'est votre serveur qui décide de l'opérateur. Pour laisser le choix au client, utilisez la session de checkout.
| Champ | Type | Requis | Description |
|---|---|---|---|
amount | entier | ✅ | Montant en XOF entiers. Minimum 100, maximum 5 000 000. |
provider | chaîne | ✅ | orange (Orange Money CI) ou stripe (carte). wave, mtn et pawapay sont temporairement indisponibles : refusés avec NGP003 (422). |
reference | chaîne (≤ 150) | ✅ | Votre identifiant de commande. C'est la clé d'idempotence. |
phone | chaîne (≤ 20) | ❌ | Numéro du payeur, +225 + 10 chiffres. |
email | ❌ | Adresse du client, pour le reçu. | |
description | chaîne (≤ 255) | ❌ | Libellé affiché au client. |
webhook_url | URL (≤ 255) | ❌ | Destination unique pour cette transaction. Elle remplace vos destinations déclarées. Mêmes règles qu'une destination déclarée : voir URL acceptées. |
return_url | URL (≤ 500) | ❌ | Où ramener le client après paiement. Sans rapport avec webhook_url. |
metadata | objet | ❌ | Vos données libres, rendues telles quelles. 20 clés au plus, 4 Ko au plus une fois encodé en JSON. |
curl -X POST https://api.live.nguepay.com/api/v1/payments \
-H "Authorization: Bearer sk_live_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d '{
"amount": 15000,
"provider": "orange",
"reference": "cmd_10452",
"phone": "+2250700000000",
"description": "Commande #10452"
}'La réponse
{
"success": true,
"data": {
"transaction_id": "9f1c0c2e-2b4a-4f0d-9a1b-6d2f7c3e4a55",
"uuid": "9f1c0c2e-2b4a-4f0d-9a1b-6d2f7c3e4a55",
"status": "pending",
"provider": "orange",
"provider_code": "orange",
"amount": 1500000,
"amount_xof": 15000,
"refunded_amount_xof": 0,
"currency": "XOF",
"reference": "cmd_10452",
"phone": "+225 07 *** ** 00",
"checkout_url": "URL de la page de paiement Orange Money",
"attempts": 0,
"metadata": [],
"failure_reason": null,
"paid_at": null,
"expires_at": "2026-09-17T22:30:00.000000Z",
"created_at": "2026-09-17T22:00:00.000000Z",
"updated_at": "2026-09-17T22:00:01.000000Z",
"links": {
"self": "/api/v1/transactions/9f1c0c2e-2b4a-4f0d-9a1b-6d2f7c3e4a55",
"refund": "/api/v1/transactions/9f1c0c2e-2b4a-4f0d-9a1b-6d2f7c3e4a55/refund"
}
},
"message": "Paiement initié."
}Chaque champ est décrit dans l'objet Transaction. À retenir : transaction_id et provider sont des alias historiques de uuid et provider_code ; phone revient masqué ; metadata vaut [] (tableau vide) quand vous n'en envoyez pas ; links contient des chemins relatifs. Pour Stripe, la réponse peut porter en plus un objet merchant (uuid, merchant_code, name de votre compte) : ne vous y fiez pas, il n'est pas renvoyé de façon constante.
checkout_url est la page de l'opérateur (ici la page de paiement Orange Money) : ouvrez-la sur le téléphone du client, qui y valide le paiement. Pour la page de paiement hébergée NguePay, créez plutôt une session (POST /checkout/sessions).amount_xof est le montant que vous avez demandé ; amount est le même montant dans l'unité de stockage (centimes). Affichez amount_xof à vos utilisateurs.Qui choisit le moyen de paiement
Les moyens proposés à votre client viennent de votre compte NguePay, pas de votre code : activez-les dans l'écran Moyens de paiement de votre dashboard. Rien à configurer côté intégration.
| Appel | provider | Qui choisit |
|---|---|---|
POST /v1/payments | obligatoire | Vous — le client arrive directement chez l'opérateur. |
POST /v1/checkout/sessions | facultatif | Le client, sur la page NguePay, parmi vos moyens activés. |
Omettez provider sur une session de checkout et la première page affiche vos moyens activés, au choix du client. Un moyen que vous n'avez pas activé n'y figure pas, et ne peut pas être atteint en devinant l'URL.
provider non activé sur votre compte, l'appel est refusé avec NGP010. Pour l'instant, seuls orange et stripe sont ouverts : wave, mtn et pawapay sont temporairement indisponibles et refusés avec NGP003 (422).Ramener le client sur votre site
return_url et webhook_url répondent à deux besoins qui ne se recouvrent pas, et il faut les deux.
return_url | webhook_url | |
|---|---|---|
| Destinataire | Le navigateur du client | Votre serveur |
| Rôle | Confort : ramener le client chez vous | Faire foi : confirmer le paiement |
| Si absent | Le client reste sur la page NguePay | Vous n'apprenez jamais le paiement |
| Si le client ferme l'onglet | Rien ne se passe | La notification part quand même |
curl -X POST https://api.live.nguepay.com/api/v1/payments \
-H "Authorization: Bearer sk_live_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d '{
"amount": 15000,
"provider": "orange",
"reference": "cmd_10452",
"return_url": "https://votre-boutique.ci/commande/10452",
"webhook_url": "https://votre-boutique.ci/webhooks/nguepay"
}'Ce que contient l'URL de retour
NguePay renvoie le client sur votre return_url exactement telle que vous l'avez envoyée : aucun paramètre n'y est ajouté — ni uuid, ni statut, ni référence, ni signature. Il n'existe pas non plus de success_url ou de cancel_url côté marchand : les opérateurs (Orange Money, Stripe) ramènent toujours le client sur les pages NguePay (/pay/{uuid}/success, /failed, /cancelled, /waiting), et ce sont elles qui proposent votre return_url.
| Page NguePay | Ce qui mène à return_url |
|---|---|
| Paiement réussi | Redirection automatique après 5 secondes, et bouton de retour. Dans l'iframe du SDK, pas de redirection automatique : c'est votre page qui reprend la main (onSuccess). |
| Paiement échoué ou expiré | Bouton de retour. |
| Paiement annulé | Bouton de retour. |
| Choix du moyen de paiement | Bouton « Retour au site marchand ». |
Le retour arrive donc aussi après un échec ou un abandon. Sans return_url, NguePay utilise metadata.return_url s'il existe ; une valeur qui ne commence pas par http est ignorée. Quand checkout_url vaut null (validation sur le téléphone), aucune page NguePay n'est affichée et return_url ne sert pas.
Pour retrouver la transaction au retour, placez vous-même votre identifiant dans l'URL (https://votre-boutique.ci/commande/10452), puis interrogez l'API depuis votre serveur : GET /transactions?reference=cmd_10452, ou GET /payments/{uuid} avec l'uuid conservé à la création. N'accordez aucune valeur à un paramètre de statut que vous auriez ajouté vous-même : le client peut le modifier. Le webhook reste la confirmation qui fait foi.
Le montant est toujours en XOF entiers
Quel que soit l'opérateur, vous envoyez des XOF entiers : 1000 pour 1 000 F. N'appliquez aucune conversion avant l'appel — ni multiplication par cent, ni division.
NguePay convertit ensuite vers l'unité attendue par chaque opérateur, et elle n'est pas la même partout : Orange Money raisonne en francs, Stripe dans la plus petite unité de la devise — qui, pour le XOF, est également le franc, parce que le XOF est une devise zero-decimal. Ces différences sont notre affaire, pas la vôtre.
Dans les réponses, deux champs coexistent : amount_xof est ce que vous avez demandé, amount le même montant dans notre unité de stockage (centimes). Affichez amount_xof.
Idempotence : la référence fait foi
Une même reference ne peut être payée qu'une fois. Si vous rejouez l'appel — double clic, retry réseau, worker relancé — NguePay renvoie la transaction existante avec un 200 au lieu d'un 201, et n'encaisse pas deux fois.
Conséquence pratique : dérivez la référence de votre commande (cmd_10452), jamais d'un horodatage ou d'un aléatoire. Un identifiant aléatoire vous ferait perdre exactement la protection que vous cherchez.
Cycle de vie
| Statut | Signification | Terminal |
|---|---|---|
pending | Créée, en attente d'action du client. | non |
processing | L'opérateur traite l'encaissement. | non |
succeeded | Encaissé. Le seul statut qui autorise la livraison. | oui |
failed | Refusé (solde, code erroné, refus opérateur). | oui |
canceled | Annulé par vous (POST /transactions/{uuid}/cancel, sans webhook) ou signalé annulé par l'opérateur (webhook payment.canceled). | oui |
expired | Délai de paiement écoulé. | oui |
Une transaction remboursée reste en succeeded : le remboursement est un objet distinct, et refunded_amount_xof suit le cumul.
payment.succeeded pour un montant que vous n'avez pas demandé.Lire, lister, annuler
# Lire une transaction
curl https://api.live.nguepay.com/api/v1/payments/{uuid} \
-H "Authorization: Bearer sk_live_VOTRE_CLE"
# Lister avec filtres
curl "https://api.live.nguepay.com/api/v1/transactions?status=succeeded&per_page=50" \
-H "Authorization: Bearer sk_live_VOTRE_CLE"
# Annuler une transaction non terminale
curl -X POST https://api.live.nguepay.com/api/v1/transactions/{uuid}/cancel \
-H "Authorization: Bearer sk_live_VOTRE_CLE"Quand l'annulation est refusée
Seule une transaction en pending s'annule. Avant de la clore, NguePay interroge l'opérateur — canceled est définitif, et un paiement validé juste après serait encaissé sans jamais vous être crédité. L'annulation est donc refusée en 422, avec un message explicite, lorsque :
- l'opérateur indique que le client a déjà payé — la confirmation va suivre ;
- une demande de validation est en attente sur le téléphone du client — il peut valider dans les secondes qui suivent ;
- l'opérateur est injoignable, ou n'a pas encore répondu à la création du paiement — son sort n'est pas connu ;
- la page de paiement hébergée chez l'opérateur ne peut pas être fermée — elle expirera d'elle-même.
Dans les deux derniers cas, réessayez plus tard ou laissez le paiement expirer. Une annulation acceptée renvoie la transaction en canceled.
Ne faites pas de la relecture une boucle d'attente : les webhooks existent pour ça. Réservez-la aux vérifications avant livraison et à la réconciliation.
Erreurs à prévoir
| Code | HTTP | Quand |
|---|---|---|
NGP002 | 422 | Montant hors bornes ou non entier. |
NGP003 | 422 / 502 | Opérateur indisponible — réessayer ou proposer un autre moyen. |
NGP004 | 422 | Numéro de téléphone invalide. |
NGP005 | 422 | Référence déjà utilisée pour un autre montant. |
NGP009 | 422 | Refus anti-fraude. Le motif n'est jamais détaillé. |
NGP010 | 422 | Opérateur non activé sur votre compte. |
NGP011 | 422 | Plafond mensuel d'un compte particulier atteint. |
Plafond des comptes particuliers
Un compte ouvert en tant que particulier peut encaisser jusqu'à 3 000 000 XOF par mois calendaire. Le cumul compte les paiements réussis et ceux encore en cours (pending, processing) : un paiement parti chez l'opérateur engage le plafond même s'il n'est pas encore confirmé.
Au-delà, la création est refusée en 422 avec NGP011 et le message « Plafond mensuel atteint pour un compte particulier (3 000 000 XOF/mois). ». Les comptes entreprise n'ont pas de plafond mensuel.