Aller au contenu

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.

GET /payments/{id} · 200 OK
{
"code": 200,
"status": "OK",
"message": "Payment retrieved",
"transaction": {
"id": "trx_test_CSUGajfv9xh0XQ5wu2lx",
"reference": "order-4172",
"status": "succeeded",
"amount": 5000,
"currency": "XAF",
"created_at": "2026-05-24T10:21:08+00:00"
}
}

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.

POST /payments · 201 Created
{
"code": 201,
"status": "Created",
"message": "Payment initiated",
"authorization_url": "https://pay.wajub.com/test_7Yh2Mp4kQ9xL4vR8tB3nW6yC1dF5sJ0aG…",
"authorization_token": "test_7Yh2Mp4kQ9xL4vR8tB3nW6yC1dF5sJ0aG…",
"transaction": {
"id": "trx_test_CSUGajfv9xh0XQ5wu2lx",
"status": "pending",
"amount": 25000,
"currency": "XAF"
}
}

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.

GET /payments · 200 OK
{
"code": 200,
"status": "OK",
"message": "Payments retrieved",
"items": [
{
"id": "trx_test_CSUGajfv9xh0XQ5wu2lx",
"status": "succeeded",
"amount": 5000
},
{
"id": "trx_test_9Wm4Kd2pR7vB5nL6hY1c",
"status": "pending",
"amount": 12000
}
],
"meta": {
"current_page": 1,
"last_page": 4,
"per_page": 25,
"total": 87,
"next_cursor": "Q2t9mVxL4pRw8nZs...",
"has_more": true
}
}

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.

meta, mode curseur
{
"per_page": 25,
"next_cursor": "Hj4kP9wQx2LmV7tRb3NcY6sD",
"prev_cursor": null,
"has_more": true
}

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.

422 Unprocessable Content
{
"code": 422,
"status": "Unprocessable Content",
"message": "The amount field is required. (and 1 more error)",
"errors": {
"amount": [
"The amount field is required."
],
"currency": [
"The selected currency is invalid."
]
}
}

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.

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.

StatutSignification
pendingCréé, en attente d'une action du client.
processingConfirmé auprès du prestataire, règlement en cours.
succeededFonds reçus. Vous pouvez livrer la commande.
failedRefusé par le prestataire ou par le portefeuille.
cancelledAnnulé par le client ou par vous.
expiredLa fenêtre de paiement s'est fermée avant la fin.
partialUne partie du montant a été capturée, sur un paiement fractionné.
refundedEntièrement remboursé après un succès.
partially_refundedPartiellement 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éfixeObjetPartie aléatoire
trx_Paiement20 caractères
po_Transfert sortant20 caractères
rfd_Remboursement20 caractères
dsp_Litige20 caractères
ben_Bénéficiaire20 caractères
acc_Sous-compte Sync20 caractères
cus_Client24 caractères
evt_Événement webhook24 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.

Format des horodatages
"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.

Que pensez-vous de ce contenu ?