Structure des réponses
L'enveloppe JSON, le format des listes, la forme des erreurs et les identifiants communs à tous les endpoints.
Chaque endpoint répond avec la même enveloppe JSON, en cas de succès comme d'échec. Trois clés sont toujours présentes : un seul handler de réponses couvre donc toute l'API, et vous ne distinguez que les parties qui changent.
Les trois clés toujours présentes
code répète le statut HTTP dans le corps, pour les clients qui lisent difficilement les en-têtes.
status est le libellé HTTP de ce code (OK, Created, Not Found, Unprocessable Content), et
jamais la chaîne littérale error. message est une courte phrase qui décrit ce qui s'est passé,
destinée à vos logs plutôt qu'à vos clients.
Tout le reste du corps dépend de ce que vous avez demandé.
Une ressource
Quand vous récupérez ou créez un seul objet, il arrive sous une clé qui porte le nom de son type. Un
paiement revient sous transaction, un client sous customer, un transfert sous transfer. Il n'y
a pas d'enveloppe générique data à déballer.
Un champ sans valeur n'est pas toujours envoyé à null. Les paiements, clients, remboursements,
bénéficiaires et litiges l'omettent ; les transferts, factures, liens et comptes l'envoient à
null. Traitez chaque champ facultatif comme pouvant être absent ou null, et lisez-le avec une
valeur par défaut.
Un endpoint peut ajouter ses propres clés de premier niveau à côté de la ressource quand elles n'en
sont pas des propriétés. La création d'un paiement est le premier cas que vous rencontrerez : l'URL
de la page hébergée décrit la session que vous venez d'ouvrir, pas le paiement lui-même. Elle se
place donc à côté de la ressource plutôt qu'à l'intérieur. Le jeton n'a pas de préfixe de type :
c'est une chaîne aléatoire, précédée de test_ dans la sandbox.
Plusieurs ressources
Les endpoints de liste dérogent volontairement à la règle de nommage. Le tableau s'appelle items,
quel que soit son contenu, et un objet meta porte l'état de la pagination. Écrire une seule fonction
de liste qui marche pour les paiements, les clients, les transferts et les autres ressources vaut donc
la peine. Seule exception : GET /events, dont le tableau s'appelle events et qui ne pagine que par
numéro.
meta existe sous deux formes, et celle que vous recevez dépend de votre requête. Par défaut, vous
paginez par numéro de page et meta vous indique où vous en êtes par rapport au total. Sur les
listes qui prennent en charge les curseurs, il porte aussi next_cursor et has_more. Renvoyez ce
next_cursor dans le paramètre cursor et l'API passe en pagination par curseur : c'est ce qu'il
vous faut pour un gros export ou une reprise de données, car elle reste correcte pendant que de
nouveaux objets arrivent.
La taille de page vaut 25 par défaut et plafonne à 100. Pagination explique comment parcourir une collection du début à la fin.
Erreurs
Une erreur garde les trois mêmes clés. Le code HTTP est dans la plage 4xx ou 5xx, status est le
libellé de ce code, et message dit ce qui n'a pas marché. Comme l'enveloppe ne change pas, un appel
en échec se lit avec le même code qu'un appel réussi.
Deux classes d'erreurs ajoutent un champ à traiter explicitement. Un 422 contient errors, un
objet indexé par nom de champ dont les valeurs sont des tableaux de messages : un champ peut donc
échouer sur plusieurs règles à la fois ; un refus qui ne vise aucun champ précis range son message
sous errors.request. Le message de premier niveau est le premier d'entre eux, suivi de
(and 1 more error) s'il y en a d'autres. Une réponse bloquée par la limite de requêtes contient
retry_after, le nombre de secondes à attendre, ce qui vous évite de deviner un délai.
Un 409 Conflict qui contient errors.idempotency_key signifie que vous avez réutilisé une
Idempotency-Key dont la première requête est encore en cours : renvoyez-la un instant plus tard
pour obtenir la réponse enregistrée. Une reference n'est jamais contrôlée pour son unicité : la
réutiliser ne provoque aucun conflit. Les autres 409 disent que la ressource n'est pas dans un
état qui permet l'appel : un débit déjà en cours sur le paiement (error_code: payment_in_progress),
un paiement préparé déjà activé, annulé ou expiré, un paiement dont des tranches ont été payées, et
quelques cas Sync et CLI décrits sur leurs pages. Attendre n'y change rien : relisez la ressource,
puis décidez.
Un 404 vous indique quelle ressource manquait, Payment Not Found plutôt qu'un message générique :
le champ message suffit donc pour aiguiller l'erreur sans examiner l'URL appelée. Le catalogue
complet des codes se trouve dans la référence API.
N'analysez pas le message
message est du texte libre, et il change. Basez vos décisions sur code, et sur les clés de
errors quand vous avez besoin du détail par champ. Quelques échecs côté serveur renvoient code
et status sans aucun message : lisez-le donc avec prudence.
Statuts de paiement
Le status d'une transaction prend l'une de neuf valeurs. Les transitions entre elles, et celles qui
sont finales, sont décrites dans Concepts clés.
| Statut | Signification |
|---|---|
pending | Créé, en attente d'une action du client. |
processing | Confirmé auprès du prestataire, règlement en cours. |
succeeded | Fonds reçus. Vous pouvez livrer la commande. |
failed | Refusé par le prestataire ou par le portefeuille. |
cancelled | Annulé par le client ou par vous. |
expired | La fenêtre de paiement s'est fermée avant la fin. |
partial | Une partie du montant a été capturée, sur un paiement fractionné. |
refunded | Entièrement remboursé après un succès. |
partially_refunded | Partiellement remboursé après un succès. |
Les deux derniers se lisent à partir de la version 2026-10-11 de l'API. Sur une version
antérieure, un paiement remboursé continue d'afficher succeeded.
Identifiants
Chaque objet porte un id construit de la même façon : un préfixe qui nomme le type, puis test_
quand l'objet vit dans la sandbox, puis une chaîne aléatoire. Le préfixe vous dit ce que vous avez
entre les mains sans rien consulter, et le segment test_ empêche de confondre un identifiant
sandbox avec un identifiant live dans un log ou un ticket de support.
| Préfixe | Objet | Partie aléatoire |
|---|---|---|
trx_ | Paiement | 20 caractères |
po_ | Transfert sortant | 20 caractères |
rfd_ | Remboursement | 20 caractères |
dsp_ | Litige | 20 caractères |
ben_ | Bénéficiaire | 20 caractères |
acc_ | Sous-compte Sync | 20 caractères |
cus_ | Client | 24 caractères |
evt_ | Événement webhook | 24 caractères |
Ainsi, un paiement sandbox s'écrit trx_test_CSUGajfv9xh0XQ5wu2lx et son jumeau en production
trx_CSUGajfv9xh0XQ5wu2lx. Stockez les identifiants en texte de longueur illimitée, jamais dans une
colonne fixe de 24 ou 32 caractères. Les factures et les liens de paiement suivent leurs propres
règles, listées dans Concepts clés.
Dates
Chaque horodatage est au format ISO 8601 en UTC, avec un décalage explicite plutôt qu'un suffixe Z.
"created_at": "2026-05-24T10:21:08+00:00"Les deux écritures désignent le même instant et tout parseur ISO 8601 les gère. Mais si vous comparez des horodatages sous forme de chaînes, ce que vous ne devriez pas faire, c'est ce décalage que vous recevrez réellement.
Montants
Les montants sont exprimés dans l'unité principale de la devise. 5000 dans une réponse signifie
5 000 XAF, et non 50 XAF, soit l'inverse de ce que font la plupart des API de paiement. Le franc CFA
n'a pas de subdivision utilisée au quotidien : une convention en unité mineure inventerait une
coupure qui n'existe pas.
Les valeurs reviennent sous forme de nombres arrondis à deux décimales. Ce que vous pouvez débiter
est borné par devise : de 25 à 2 000 000 en XAF et XOF, à partir de 100 en NGN, à partir de 0,50 en
USD et EUR. Un montant hors de ces bornes est refusé avec un 422 dont le message les indique. Lisez
le champ currency avant tout calcul, car un même chiffre signifie des choses très différentes en
XAF et en USD.