Remboursements
Restituer des fonds sur un paiement réussi, puis suivre le remboursement jusqu'à son propre résultat.
Un remboursement est un objet à part entière, avec son propre id, son propre statut et ses propres
webhooks. Ce n'est pas un état dans lequel passe le paiement. La distinction compte plus qu'il n'y
paraît : le paiement que vous remboursez reste succeeded pour toujours, et tout ce que vous voulez
savoir sur l'argent qui revient se trouve sur le remboursement.
Vous pouvez restituer le montant total ou une partie, et cumuler plusieurs remboursements partiels jusqu'à épuisement du montant d'origine.
Les remboursements exigent la clé secrète
POST /refunds exige une clé secrète. Un appel qui porte une clé publique
est refusé avec 406 Not Acceptable et le message Private Key Required. Cet endpoint a sa place
sur votre serveur, jamais dans un navigateur ni dans une application mobile.
Ce qui peut être remboursé
Quatre conditions sont vérifiées avant l'enregistrement d'un remboursement, et chacune échoue avec
son propre 422.
| Condition | Ce que vous recevez en cas d'échec |
|---|---|
Le paiement est succeeded | Transaction must be completed to be refunded. Current status: … |
| Il reste un montant remboursable | Transaction has already been fully refunded. |
| Le montant tient dans ce qui reste | Refund amount exceeds refundable amount (…) |
| La devise correspond à celle du paiement | Refunds must be in the transaction currency (XAF). |
Le montant remboursable est le montant du paiement moins tout ce qui a déjà été remboursé, en
comptant les remboursements encore pending comme ceux qui ont réussi. Un remboursement en attente
réserve donc sa part immédiatement : c'est ce qui empêche deux appels simultanés de rembourser deux
fois le même argent.
Un remboursement ne peut pas convertir une devise
currency est accepté, mais la seule valeur possible est la devise du paiement lui-même. Le champ
existe pour que vous puissiez être explicite, pas pour rembourser en XOF un paiement en XAF. Si
vous l'omettez, Wajub utilise de toute façon la devise du paiement.
Créer un remboursement
Le paiement est désigné par son id Wajub. Votre propre reference sur le paiement n'est pas
acceptée ici, car la recherche porte sur l'id du paiement et rien d'autre.
https://api.wajub.com/refundspaymentstringobligatoireid du paiement, par exemple trx_test_CSUGajfv9xh0XQ5wu2lx. Votre reference marchand n'est pas acceptée.reasonenumobligatoireamountnumberfacultatifcurrencystringfacultatiffees_bearerenumfacultatifmerchant ou customer : qui supporte les frais Wajub du paiement pour la part remboursée. Par défaut : vos réglages de remboursement, sinon celui qui a payé les frais au paiement. Voir plus bas.referencestringfacultatifmetadataobjectfacultatifdispute_settlement marque le remboursement que Wajub crée quand vous acceptez un litige, et elle est retirée si vous l'envoyez.reason est obligatoire et sa liste est fermée. Toute valeur hors de cette liste donne un 422 :
choisissez la plus proche plutôt que d'en inventer une.
| Motif | Quand l'utiliser |
|---|---|
requested_by_customer | Le client l'a demandé, et vous avez accepté |
duplicate | La même commande a été payée deux fois |
fraudulent | Vous pensez que le paiement n'a pas été fait par le titulaire de la carte ou du compte |
product_not_received | La marchandise n'est jamais arrivée |
service_not_delivered | Un service a été payé mais pas fourni |
wrong_amount | Vous avez débité plus que prévu |
customer_complaint | Une réclamation réglée sans désigner de responsable |
merchant_error | Votre propre erreur : mauvais article, mauvais prix, mauvais client |
network_error | Le paiement est passé de votre côté mais pas du leur |
transaction_error | Une panne technique dans le paiement lui-même |
reconciliation | Correction d'un écart trouvé lors d'un rapprochement |
Envoyez l'appel depuis votre serveur, avec une Idempotency-Key si le remboursement peut être
déclenché par un processus qui retente ses appels.
curl https://api.wajub.com/refunds \
-H "Authorization: sk_test.xxxxxxxxxxxxxxxx…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: refund-order-4172" \
-d '{
"payment": "trx_test_CSUGajfv9xh0XQ5wu2lx",
"amount": 2000,
"reason": "product_not_received",
"reference": "rma-881"
}'Wajub répond 201 Created. Rien n'a encore quitté votre solde : le remboursement a été accepté et
mis en file, et status vous indique où il en est.
Le paiement est porté par transaction, pas par payment. Les lectures renvoient aussi un détail
tax quand une taxe s'applique, une timeline des changements de statut, et failure_reason avec
failure_message quand le remboursement a échoué. Un remboursement qui a pris de l'argent sur
votre solde renvoie en plus ledger_balance_currency, ledger_balance_before et
ledger_balance_after : votre solde juste avant le remboursement et juste après, si bien que la
différence est ce qu'il a pris, part des frais du paiement comprise quand elle a bougé avec lui. En
sandbox, ce sont ceux de votre solde sandbox. refund.succeeded part une fois le remboursement sorti
de votre solde : son contenu les porte aussi, et il peut arriver quelques minutes après le passage
du remboursement à succeeded. Un remboursement qui
n'a rien pris, parce qu'un litige perdu l'avait déjà fait, n'a aucun des trois.
Idempotency-Key, et ce sur quoi porte la clé
La clé est conservée sur le remboursement lui-même et n'expire jamais : la même clé désigne
toujours le même remboursement, et n'en crée jamais un second. Pendant 24 heures, la première
réponse est rejouée telle quelle, et tout changement du corps est refusé avec un 422. Ensuite,
le remboursement d'origine revient dans un nouveau 201, sauf si la clé sert pour un autre
paiement ou un autre amount, ce qui donne aussi un 422. Une clé est consommée pour toujours :
dérivez-la de ce remboursement précis, une commande et un numéro de remboursement, pas de la
commande seule. Un second remboursement du même montant ne ferait que vous rendre le premier.
Le remboursement a son propre cycle de vie
Cinq états, dont trois terminaux. Un remboursement ne revient jamais à un état précédent.
Le délai ne dépend pas de Wajub. Sur une carte, c'est l'émetteur qui décide, et plusieurs jours sont normaux. Sur Mobile Money, le crédit arrive généralement en quelques minutes, parfois en quelques heures. Rien dans votre intégration n'y change quoi que ce soit : annoncez au client une fourchette plutôt qu'une heure précise.
Un remboursement ne démarre pas dans le même état selon l'environnement
En production, un remboursement est créé pending et passe à processing quand il atteint le
prestataire. En sandbox, il n'y a pas de prestataire : il est donc créé directement en processing
et se résout quelques secondes plus tard. Écrivez votre handler sur les états terminaux et vous
ne verrez pas la différence ; si vous testez pending, vos tests en sandbox n'entreront jamais dans
cette branche.
Qui envoie un remboursement live
En production, un remboursement est envoyé de l'une de trois façons. execution_mode sur le
remboursement dit laquelle, une fois que Wajub l'a planifié, et manual_reason dit pourquoi quand il
n'est pas envoyé automatiquement.
execution_mode | Qui l'envoie |
|---|---|
automatic | Wajub, sur le numéro mobile money du payeur, en quelques minutes |
merchant | Vous : le paiement a été encaissé sur votre propre compte prestataire, depuis lequel Wajub ne peut pas rembourser |
manual | Le support Wajub |
manual_reason est un objet avec un code stable sur lequel brancher votre logique et un message
que vous pouvez afficher.
code | Signification |
|---|---|
refund_from_your_provider | Encaissé sur votre propre compte prestataire : remboursez-le là-bas. Il se met à jour ici quand le prestataire confirme |
card_or_bank_payment | Les remboursements automatiques couvrent le mobile money. L'équipe Wajub envoie celui-ci |
insufficient_balance | Votre solde ne couvrait pas le remboursement au moment de l'envoyer |
payer_number_missing | Le numéro mobile money du payeur n'est pas connu |
requested_over_24h | Demandé il y a plus d'un jour : l'équipe Wajub vérifie d'abord qu'il n'a pas été remboursé autrement |
outcome_being_confirmed | Envoyé, et la réponse de l'opérateur n'est pas définitive : l'équipe Wajub confirme que l'argent est arrivé |
automatic_attempt_failed | L'opérateur l'a refusé et aucun argent n'est parti : le support Wajub le traite |
wajub_review | Tout autre cas que l'équipe Wajub doit d'abord examiner |
Un remboursement peut changer de voie sans changer de statut. Un remboursement pas encore envoyé
reste pending quand l'équipe Wajub le prend en charge. Un remboursement déjà transmis à l'opérateur
reste processing quand l'opérateur le refuse et que le support Wajub le traite
(automatic_attempt_failed) : il ne revient pas à pending. Dans les deux cas il se termine
succeeded ou failed. Chaque changement de voie envoie refund.updated avec le remboursement tel
qu'il est désormais. Les remboursements sandbox n'ont aucun des deux champs.
Ce que dit le statut du paiement
Cela dépend de votre version de l'API, et c'est le point qui piège tout le monde.
À partir de la version 2026-10-11, un paiement affiche partially_refunded dès que des
remboursements réussis couvrent une partie de son montant, et refunded dès qu'ils le couvrent
en entier. Un remboursement pending, processing ou failed ne change rien. Sur une version
antérieure, le paiement continue d'afficher succeeded, quel que soit le montant remboursé.
Gestion des versions indique celle sur laquelle votre compte est fixé.
Le statut est une lecture, et rien d'autre ne bouge avec lui : aucun événement de paiement n'est
émis, et le paiement n'est pas modifié par ailleurs. Pour savoir ce qui reste remboursable, quelle
que soit la version, listez les remboursements du paiement et additionnez ceux qui sont pending,
processing ou succeeded.
https://api.wajub.com/payments/{id}/refundsSoustrayez ce total du montant du paiement et vous obtenez ce qui reste remboursable, c'est-à-dire exactement le chiffre que l'API vérifie quand vous envoyez le remboursement suivant.
Le filtre de statut suit votre version
À partir de 2026-10-11, GET /payments?status=refunded et ?status=partially_refunded listent
ces paiements, et ?status=succeeded ne les inclut plus. Sur une version antérieure, les deux
filtres sont acceptés et reviennent vides, et succeeded liste tous les paiements réussis,
remboursés ou non.
Qui supporte les frais
Wajub garde les frais d'un paiement que vous remboursez. Ce que vous décidez, c'est qui les
supporte au final, pour la part des frais qui correspond au montant remboursé : vous, ou le client.
C'est fees_bearer.
| Les frais ont été payés par | fees_bearer: merchant | fees_bearer: customer |
|---|---|---|
Vous (bearer: merchant sur le paiement) | Le client récupère le montant. Les frais que vous avez payés ne vous sont pas rendus | La part des frais est déduite de ce que le client récupère, et vous êtes débité d'autant en moins |
Le client, en plus (bearer: customer) | Le client récupère le montant et la part des frais, et vous êtes débité des deux | Le client récupère le montant, pas les frais |
Sans ce champ, les frais restent à celui qui les a payés : c'est la première et la dernière case du
tableau, et c'est ainsi que les remboursements ont toujours fonctionné. Pour changer cela pour tous
les remboursements, réglez-le une fois dans Settings, Refunds ; fees_bearer sur un
remboursement prend le dessus.
Le remboursement vous dit ce qui a été appliqué : fee_amount est la part des frais, et
payer_amount ce que le client reçoit. Votre solde est débité de payer_amount, ni plus ni moins.
Un remboursement partiel prend la même proportion des frais, et le remboursement qui solde le
paiement prend ce qu'il en reste : les parts s'additionnent exactement. Dans une devise sans
subdivision, comme le XAF, une part est un nombre entier, et payer_amount aussi.
Un paiement encaissé sur votre propre compte prestataire est différent. Wajub ne détient rien de
son argent : votre solde n'est pas débité, vous envoyez le remboursement vous-même depuis ce
prestataire, et le montant à envoyer est payer_amount. Avec bearer: merchant, Wajub n'a facturé
aucuns frais sur ce paiement : fee_amount vaut 0 et payer_amount est égal à amount. Avec
bearer: customer, le payeur a payé les frais en plus à votre prestataire, et le tableau ci-dessus
s'applique.
L'argent bouge au succès, pas à la création
Créer un remboursement réserve le montant sur ce qui est remboursable, mais ne touche à aucun solde.
Le débit a lieu quand le remboursement atteint succeeded, et seulement à ce moment-là, pour
payer_amount : le montant, plus ou moins la part des frais.
Deux conséquences à anticiper. En sandbox, un remboursement réussi débite votre solde sandbox du montant remboursé, comme un remboursement live débite votre solde. Un remboursement qui rembourse un litige que vous avez accepté le débite même s'il échoue, puisque le litige est perdu dans tous les cas. Et un remboursement émis sur un paiement déjà perdu dans un litige est plafonné au montant principal qui n'a pas déjà été repris : le même argent ne peut pas sortir deux fois par deux portes différentes.
Ce que devient ensuite le solde, et comment se comporte un solde négatif, est expliqué dans Solde et règlements.
Webhooks
Six événements suivent un remboursement. Abonnez-vous à chacun par son nom : un abonnement ne
reconnaît que des noms exacts, et refund.* ne correspond à rien.
| Événement | Déclenché quand |
|---|---|
refund.created | Le remboursement a été accepté et enregistré |
refund.processing | Le remboursement a atteint le prestataire. En live uniquement |
refund.succeeded | Fonds restitués. C'est sur celui-ci qu'il faut faire le rapprochement |
refund.failed | Le prestataire l'a refusé ou n'a pas pu le finaliser, lisez failure_reason |
refund.cancelled | Wajub a arrêté un remboursement live avant de l'envoyer, avec cancelled_by_wajub : rien n'a quitté votre solde. Envoyé une fois la version d'API 2026-10-12 devenue courante, en refund.failed jusque-là |
refund.updated | Un remboursement live a changé de voie : execution_mode ou manual_reason a changé. Live uniquement |
refund.processing n'arrive jamais en sandbox
En live, un remboursement passe de pending à processing quand il atteint le prestataire, et
refund.processing est envoyé. En sandbox, un remboursement est créé directement en processing :
cet événement n'arrive donc jamais. Utilisez refund.created si vous avez besoin d'un signal au
démarrage dans les deux environnements.
refund.succeededévénementDédupliquez sur X-Wajub-Delivery-Id comme partout ailleurs. L'enregistrement et la vérification de
signature sont dans Webhooks.
Tester un remboursement en échec dans la sandbox
Les résultats en sandbox sont déterministes, et ils dépendent du moyen de paiement du paiement d'origine, pas de ce que vous envoyez sur le remboursement. Payez avec le bon numéro, remboursez ensuite, et vous obtenez le résultat voulu.
Pour un paiement fait par Mobile Money, ce sont les six derniers chiffres du numéro de téléphone qui décident.
| Suffixe sur le paiement d'origine | Résultat du remboursement |
|---|---|
000000 | Réussit |
000009 | Le paiement réussit, le remboursement échoue toujours. C'est celui à utiliser pour vos tests |
000001, 000002, 000003, 000004 | Rien à rembourser : le paiement lui-même n'aboutit jamais, donc l'appel de remboursement répond 422 |
| Tout autre suffixe | Réussit |
Pour un paiement par carte, 4000000000005423 débite normalement puis fait échouer chaque
remboursement. Toutes les autres cartes de test se remboursent sans problème. Les paiements en
cryptomonnaies et les paiements bancaires se remboursent toujours avec succès, car il n'existe pas
d'adresse d'échec pour les tester.
Un remboursement sandbox échoué porte un failure_reason tiré au hasard parmi
insufficient_funds, refund_rejected_by_issuer, transaction_already_reversed, network_error,
provider_error et provider_declined, accompagné d'un failure_message lisible. Ce sont des codes
qu'utilise aussi un remboursement live, listés dans
Pourquoi un remboursement a échoué.
Le seul cas aléatoire
Quand le paiement d'origine n'a aucun moyen de paiement rattaché, la sandbox se rabat sur un tirage au sort pondéré pour réussir environ sept fois sur dix. Si vos tests sont instables, voici pourquoi : payez avec un vrai numéro de test pour que le résultat soit choisi et non tiré au hasard.
Pourquoi un remboursement a échoué
Un remboursement failed porte toujours un failure_reason, pris dans cette liste en production
comme en sandbox, et un failure_message à côté : les mots du fournisseur quand il en a envoyé.
failure_reason | Ce qu'il signifie |
|---|---|
insufficient_funds | La réserve du fournisseur ne couvrait pas le remboursement |
refund_rejected_by_issuer | Un émetteur de carte l'a refusé |
transaction_already_reversed | Le paiement était déjà annulé ou remboursé chez le fournisseur |
invalid_number | Le numéro ou le compte du payeur ne peut pas le recevoir, par exemple fermé ou non abonné. Live uniquement |
limit_exceeded | Un plafond du compte destinataire ou du fournisseur a été atteint. Live uniquement |
network_error | Le fournisseur n'a pas pu être joint à temps |
provider_error | Le fournisseur était indisponible, ou a refusé l'appel lui-même |
provider_declined | Le fournisseur l'a refusé sans dire pourquoi |
ops_failed | Le support Wajub n'a pas pu mener à bien un remboursement qu'il avait pris en charge, sans raison connue : celui que le prestataire avait refusé garde la raison du prestataire, prise dans cette liste. failure_message porte sa note. Live uniquement |
Un remboursement échoué ne prend rien sur votre solde, à une exception près en sandbox : le remboursement d'un litige que vous avez accepté, qui tient lieu de perte du litige (voir plus haut). Une nouvelle tentative est un nouveau remboursement.
Lister les remboursements
Les remboursements sont renvoyés du plus récent au plus ancien, tous paiements confondus.
https://api.wajub.com/refunds| Paramètre de requête | Accepte |
|---|---|
per_page | De 1 à 100, 25 par défaut |
status | pending, processing, succeeded, failed, cancelled. Avant la version d'API 2026-10-12, un remboursement annulé se lit failed et failed le liste |
search | Texte libre sur la référence et l'id du remboursement |
date_from, date_to | Dates, date_to jamais antérieure à date_from |
cursor | Passe en pagination par curseur, pour les exports |
La récupération d'un remboursement se fait par son id.
https://api.wajub.com/refunds/{id}Dans le Dashboard
Un remboursement peut être émis sans écrire de code. Ouvrez un paiement dans Payments, utilisez
son action de remboursement, et le résultat est le même objet que celui que crée l'API. Le
formulaire demande qui supporte les frais quand le paiement en portait, et affiche ce que le client
recevra. L'action
exige la permission refund_payments, distincte de manage_payments : un membre de l'équipe peut
ainsi gérer les commandes sans pouvoir rendre de l'argent.
Refunds les liste tous avec leur statut, et les exporte en CSV ou Excel en production.
Refund requests est le côté entrant. Un payeur qui utilise Pocket peut demander le remboursement d'un paiement qu'il a effectué, et la demande y attend jusqu'à ce que quelqu'un l'approuve ou la rejette. Rien n'est remboursé avant l'approbation, et l'approbation crée le remboursement exactement comme le ferait l'API.
Pages associées
- Gérer les remboursementsLe parcours complet, rapprochement compris.
- LitigesQuand l'argent est repris plutôt que rendu.
- Solde et règlementsOù arrive un remboursement une fois qu'il a réussi.
- Cycle de vie d'un paiementPourquoi le paiement reste succeeded pendant tout ce temps.
- Mode sandboxLes numéros et cartes de test, et ce que chacun déclenche.
- Référence API des remboursementsTous les champs de la ressource, et les trois endpoints.