Aller au contenu

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.

Ce qui peut être remboursé

Quatre conditions sont vérifiées avant l'enregistrement d'un remboursement, et chacune échoue avec son propre 422.

ConditionCe que vous recevez en cas d'échec
Le paiement est succeededTransaction must be completed to be refunded. Current status: …
Il reste un montant remboursableTransaction has already been fully refunded.
Le montant tient dans ce qui resteRefund amount exceeds refundable amount (…)
La devise correspond à celle du paiementRefunds 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.

POSThttps://api.wajub.com/refunds
paymentstringobligatoire
L'id du paiement, par exemple trx_test_CSUGajfv9xh0XQ5wu2lx. Votre reference marchand n'est pas acceptée.
reasonenumobligatoire
La raison du remboursement. Les valeurs acceptées sont listées plus bas.
amountnumberfacultatif
Montant dans l'unité principale, décimales autorisées et arrondies à deux. Omettez-le pour rembourser en totalité ce qui reste.
currencystringfacultatif
Doit être égale à la devise du paiement, qui est la valeur par défaut.
fees_bearerenumfacultatif
merchant 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.
referencestringfacultatif
Votre propre référence pour ce remboursement, jusqu'à 255 caractères. Renvoyée à la lecture, et elle reste la vôtre : Wajub n'y écrit jamais l'identifiant du remboursement chez le prestataire.
metadataobjectfacultatif
Données clé-valeur libres, renvoyées telles quelles sur l'objet remboursement. Une clé est réservée : dispute_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.

MotifQuand l'utiliser
requested_by_customerLe client l'a demandé, et vous avez accepté
duplicateLa même commande a été payée deux fois
fraudulentVous pensez que le paiement n'a pas été fait par le titulaire de la carte ou du compte
product_not_receivedLa marchandise n'est jamais arrivée
service_not_deliveredUn service a été payé mais pas fourni
wrong_amountVous avez débité plus que prévu
customer_complaintUne réclamation réglée sans désigner de responsable
merchant_errorVotre propre erreur : mauvais article, mauvais prix, mauvais client
network_errorLe paiement est passé de votre côté mais pas du leur
transaction_errorUne panne technique dans le paiement lui-même
reconciliationCorrection 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.

Réponse · 201 Created
{
"code": 201,
"status": "Created",
"message": "Refund created successfully",
"refund": {
"id": "rfd_test_9xh0XQ5wu2lxCSUGajfv",
"transaction": "trx_test_CSUGajfv9xh0XQ5wu2lx",
"reference": "rma-881",
"amount": 2000,
"currency": "XAF",
"status": "processing",
"reason": "product_not_received",
"metadata": {
},
"sandbox": true,
"created_at": "2026-09-11T10:30:00+00:00"
}
}

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.

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_modeQui l'envoie
automaticWajub, sur le numéro mobile money du payeur, en quelques minutes
merchantVous : le paiement a été encaissé sur votre propre compte prestataire, depuis lequel Wajub ne peut pas rembourser
manualLe support Wajub

manual_reason est un objet avec un code stable sur lequel brancher votre logique et un message que vous pouvez afficher.

codeSignification
refund_from_your_providerEncaissé sur votre propre compte prestataire : remboursez-le là-bas. Il se met à jour ici quand le prestataire confirme
card_or_bank_paymentLes remboursements automatiques couvrent le mobile money. L'équipe Wajub envoie celui-ci
insufficient_balanceVotre solde ne couvrait pas le remboursement au moment de l'envoyer
payer_number_missingLe numéro mobile money du payeur n'est pas connu
requested_over_24hDemandé il y a plus d'un jour : l'équipe Wajub vérifie d'abord qu'il n'a pas été remboursé autrement
outcome_being_confirmedEnvoyé, et la réponse de l'opérateur n'est pas définitive : l'équipe Wajub confirme que l'argent est arrivé
automatic_attempt_failedL'opérateur l'a refusé et aucun argent n'est parti : le support Wajub le traite
wajub_reviewTout 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.

GEThttps://api.wajub.com/payments/{id}/refunds

Soustrayez 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.

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 parfees_bearer: merchantfees_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 rendusLa 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 deuxLe 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énementDéclenché quand
refund.createdLe remboursement a été accepté et enregistré
refund.processingLe remboursement a atteint le prestataire. En live uniquement
refund.succeededFonds restitués. C'est sur celui-ci qu'il faut faire le rapprochement
refund.failedLe prestataire l'a refusé ou n'a pas pu le finaliser, lisez failure_reason
refund.cancelledWajub 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.updatedUn 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énement
Un remboursement total ou partiel est terminé sur le paiement.
Payload
{
"id": "evt_Lm2Xc94Rf8Qp7Tz3Wk5Yb1Nd",
"event": "refund.succeeded",
"created": "2026-09-11T11:02:41+00:00",
"livemode": true,
"data": {
"id": "rfd_9xh0XQ5wu2lxCSUGajfv",
"transaction": "trx_CSUGajfv9xh0XQ5wu2lx",
"reference": "rma-881",
"amount": 2000,
"currency": "XAF",
"status": "succeeded",
"reason": "product_not_received"
}
}

Dé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'origineRésultat du remboursement
000000Réussit
000009Le paiement réussit, le remboursement échoue toujours. C'est celui à utiliser pour vos tests
000001, 000002, 000003, 000004Rien à rembourser : le paiement lui-même n'aboutit jamais, donc l'appel de remboursement répond 422
Tout autre suffixeRé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_reasonCe qu'il signifie
insufficient_fundsLa réserve du fournisseur ne couvrait pas le remboursement
refund_rejected_by_issuerUn émetteur de carte l'a refusé
transaction_already_reversedLe paiement était déjà annulé ou remboursé chez le fournisseur
invalid_numberLe numéro ou le compte du payeur ne peut pas le recevoir, par exemple fermé ou non abonné. Live uniquement
limit_exceededUn plafond du compte destinataire ou du fournisseur a été atteint. Live uniquement
network_errorLe fournisseur n'a pas pu être joint à temps
provider_errorLe fournisseur était indisponible, ou a refusé l'appel lui-même
provider_declinedLe fournisseur l'a refusé sans dire pourquoi
ops_failedLe 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.

GEThttps://api.wajub.com/refunds
Paramètre de requêteAccepte
per_pageDe 1 à 100, 25 par défaut
statuspending, processing, succeeded, failed, cancelled. Avant la version d'API 2026-10-12, un remboursement annulé se lit failed et failed le liste
searchTexte libre sur la référence et l'id du remboursement
date_from, date_toDates, date_to jamais antérieure à date_from
cursorPasse en pagination par curseur, pour les exports

La récupération d'un remboursement se fait par son id.

GEThttps://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.

Que pensez-vous de ce contenu ?