Paiements
Créer un paiement, suivre son statut, rejouer une requête sans doublon et traiter une issue inconnue.
Un paiement est une demande d’encaissement adressée à un client, sur un moyen
de paiement donné. Il est enregistré à created, passe à pending dès que
l’opérateur a répondu (la réponse de création porte déjà pending), puis se
termine en confirmed, failed ou expired.
Créer un paiement
curl -X POST "$API_URL/v1/payments" \
-H "Authorization: Bearer $API_KEY" \
-H "Idempotency-Key: commande-1042" \
-H "Content-Type: application/json" \
-d '{
"amount": "5000",
"currency": "XOF",
"rail": "sn_wave",
"merchant_reference": "commande-1042",
"return_url": "https://boutique.example/paiement/retour",
"customer_phone": "77 123 45 67"
}'| Champ | Requis | Description |
|---|---|---|
amount | oui | Montant en XOF : chaîne de chiffres, au moins le min_amount du moyen de paiement ("5000", pas 5000). |
currency | oui | Toujours "XOF". |
rail | oui | Identifiant du moyen de paiement : sn_wave, ci_orange… Voir Moyens de paiement et pays. |
merchant_reference | oui | Référence de la commande : 1 à 128 caractères parmi lettres, chiffres et . _ : / # -. Unique par projet. |
return_url | oui | URL https:// où revient le client après le paiement. Une URL http://, y compris http://localhost, est refusée (400 INVALID_REQUEST, champ return_url). |
customer_phone | mode réel | Numéro mobile du client, valide dans le pays du moyen de paiement. Format local (77 123 45 67) ou E.164 (+221771234567) ; renvoyé au format E.164. Sert aussi aux remboursements. |
customer_otp | ci_orange | Code à usage unique du client, 4 à 8 chiffres. Voir Moyens de paiement et pays, section « Code OTP ». |
customer_device | non | sn_orange : appareil du client, mobile (défaut) ou desktop. mobile ouvre directement l’application Max it ; desktop renvoie un QR code (next_action.type = qr, valable 5 minutes) que le client scanne avec son téléphone. Le numéro customer_phone reste requis en mode réel, aussi avec desktop. Les pages de paiement 221 Pay choisissent seules selon l’appareil. |
offer_code | non | Code promo du projet. amount est alors réduit de la remise ; la réponse ajoute offer_code, discount_amount et original_amount. |
Un champ absent de ce tableau est refusé : 400 INVALID_REQUEST, avec
fields.<nom du champ>. Les champs de la réponse, comme
payment_link_description, ne s’envoient pas.
Réponse
201 à la création, 200 pour une requête rejouée avec la même
Idempotency-Key et le même corps.
{
"id": "8d1f0c6e-3b7a-4c55-9a51-2f0e5c1d7a90",
"status": "pending",
"amount": "5000",
"currency": "XOF",
"rail": "sn_wave",
"merchant_reference": "commande-1042",
"checkout_url": "https://…",
"next_action": { "type": "redirect", "url": "https://…" },
"created_at": "2026-10-07T10:00:00Z",
"withdrawal_eligibility": "not_confirmed",
"fee": null,
"net": null,
"livemode": false
}Rediriger le client vers checkout_url. Sur Wave, next_action est une
redirection (type redirect) vers la même page. livemode vaut false
avec une clé de test, true avec une clé live. fee (frais) et net
(montant crédité au marchand) sont renseignés à la confirmation.
Un paiement fait sur un lien de paiement porte en
plus payment_link_description, la description du lien, à afficher à la place
de merchant_reference (de la forme link:<lien>:<paiement>). Le champ est
absent des autres paiements.
Action client : next_action
Selon l’opérateur, la réponse porte next_action, l’étape à accomplir par le
client. checkout_url vaut alors null quand l’opérateur ne fournit pas de
page.
type | Champ utile | Traitement |
|---|---|---|
redirect | url | Rediriger le client vers url. |
qr | qr_code | Afficher l’image qr_code (PNG en URI data:, utilisable telle quelle comme source d’une image). Le client la scanne avec son application mobile money. |
instruction | message | Afficher message tel quel : le client suit l’instruction sur son téléphone. |
L’issue arrive ensuite par webhook ou par relecture du paiement.
Statuts
| Statut | Signification | Événement webhook |
|---|---|---|
created | Paiement enregistré. | Aucun |
pending | Demande transmise, paiement du client attendu. | Aucun |
confirmed | Fonds reçus, fee et net renseignés, montant net crédité au solde disponible. | payment.succeeded |
failed | Paiement refusé ou abandonné. error_code : provider_failed. | payment.failed |
expired | Délai de paiement dépassé. error_code : expired. | payment.expired |
unknown | Issue incertaine après un incident. Voir « Issue inconnue ». | Aucun |
confirmed est définitif. withdrawal_eligibility passe alors à confirmed :
le montant net peut être retiré.
Relire et lister
curl "$API_URL/v1/payments/$PAYMENT_ID" -H "Authorization: Bearer $API_KEY"
curl "$API_URL/v1/payments?status=confirmed,failed&limit=20" -H "Authorization: Bearer $API_KEY"GET /v1/payments/{id} ajoute au paiement error_code, error_message
(dans la langue de Accept-Language), customer.phone et refunds.
Filtres de GET /v1/payments :
| Paramètre | Description |
|---|---|
status | Un ou plusieurs statuts, répétés ou séparés par des virgules. |
rail | Moyen de paiement. |
q | Identifiant du paiement, ou merchant_reference exacte ou par préfixe. |
customer_phone | Numéro du client. |
from, to | Plage de created_at (RFC 3339) : from inclus, to exclu. |
limit | 1 à 100, 20 par défaut. |
cursor | Valeur de next_cursor de la page précédente ; null en dernière page. |
La réponse contient data, next_cursor et total_count.
Rejouer sans doublon : Idempotency-Key
L’en-tête Idempotency-Key est obligatoire sur POST /v1/payments (1 à 128
caractères). Une clé désigne une tentative logique, par exemple une commande.
- Même clé, même corps : le même paiement est renvoyé (
200), jamais un second. - Même clé, corps différent :
409 IDEMPOTENCY_CONFLICT, rien n’est créé. - Une clé ne vaut que pour la route où elle a servi.
Après un délai d’attente ou une coupure réseau, renvoyer exactement la même
requête avec la même clé. merchant_reference est unique par projet : une
référence déjà utilisée avec une autre clé renvoie
409 DUPLICATE_MERCHANT_REFERENCE.
Issue inconnue
Une coupure peut rendre l’issue d’un paiement incertaine. 221 Pay ne redemande jamais un paiement de lui-même : un double encaissement est exclu.
| Situation | Conduite |
|---|---|
Aucune réponse à POST /v1/payments | Rejouer la même requête avec la même Idempotency-Key. |
202 avec seulement id | Relire GET /v1/payments/{id} jusqu’à un statut final. |
Statut unknown | Garder la commande en attente, sans second paiement. Le webhook ou la relecture donne l’issue dès la réponse de l’opérateur. |
503 CHANNEL_UNAVAILABLE | Proposer un autre moyen de paiement ou réessayer plus tard. |
Un paiement unknown ne crédite le solde qu’une fois confirmé.
Erreurs fréquentes
| Statut | code | Cause |
|---|---|---|
| 400 | INVALID_REQUEST | Champs invalides, un message par champ dans fields. |
| 403 | MERCHANT_SUSPENDED | Compte marchand suspendu. |
| 403 | MERCHANT_LIMIT_EXCEEDED | Plafond journalier ou mensuel d’encaissement atteint. |
| 409 | IDEMPOTENCY_CONFLICT | Clé déjà utilisée avec un autre corps. |
| 409 | DUPLICATE_MERCHANT_REFERENCE | merchant_reference déjà utilisée dans le projet. |
| 503 | CHANNEL_UNAVAILABLE | Moyen de paiement non activé sur le projet ou momentanément fermé. |
Table complète : Erreurs.