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 est protégé par la vérification de clé privée. 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.reasonenumobligatoireamountnumberfacultatifcurrencystringfacultatifreferencestringfacultatifmetadataobjectfacultatifreason 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.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…" \
-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
ledger_balance_before, ledger_balance_after et ledger_balance_currency une fois le
remboursement réglé, 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é.
Idempotency-Key, et ce sur quoi porte la clé
La clé est hachée avec le payload, et la réponse est rejouée pendant 24 heures. Réutilisez la même clé avec un montant différent et vous obtenez un nouveau remboursement, pas celui en cache : dérivez donc la clé de ce que vous remboursez plutôt que de la tentative.
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.
Le statut du paiement ne change jamais
C'est le point qui piège tout le monde. Rembourser ne fait pas passer le paiement à refunded ou
partially_refunded. Ces deux valeurs existent dans le vocabulaire de l'API, mais rien ne les écrit
jamais sur un paiement, et un paiement remboursé en totalité affiche toujours succeeded.
Il n'y a donc aucun indicateur à interroger. Pour savoir où en est un paiement, listez ses
remboursements et additionnez ceux qui sont pending 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.
Filtrer les paiements sur un statut remboursé ne renvoie rien
GET /payments?status=refunded et ?status=partially_refunded sont acceptés par l'endpoint et
reviendront toujours vides, car aucun paiement ne porte jamais ces valeurs. Interrogez plutôt les
remboursements.
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à.
Deux conséquences à anticiper. Un remboursement en sandbox ne fait jamais bouger un solde : les chiffres de la sandbox ne vous disent donc rien de votre trésorerie. 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
Quatre événements suivent un remboursement. Abonnez-vous à refund.* pour les recevoir tous.
| Événement | Déclenché quand |
|---|---|
refund.created | Le remboursement a été accepté et enregistré |
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 | Arrêté avant d'être finalisé |
Il n'existe pas de webhook refund.processing
Un remboursement passe bien par un statut processing, mais aucun webhook n'est émis pour ce
statut. S'abonner à refund.processing enregistre un événement qui ne se déclenchera jamais.
Utilisez refund.created si vous avez besoin d'un signal au démarrage, et considérez processing
comme une valeur que vous ne verrez qu'à la lecture.
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 | Échoue |
| 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 échoué porte un failure_reason parmi insufficient_funds,
refund_rejected_by_issuer, transaction_already_reversed, network_error, provider_error ou
unknown, accompagné d'un failure_message lisible.
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.
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 |
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. 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.