Aller au contenu

Cycle de vie d'un paiement

Chaque état d'un paiement, le webhook qui l'annonce, et ce qu'il faut faire dans chacun.

Un paiement ne réussit ni n'échoue au moment où vous le créez. Entre ces deux issues, il passe par des états qui reflètent ce qui se passe sur le téléphone du client et chez l'opérateur Mobile Money. Votre intégration doit se comporter différemment dans chacun.

Deux de ces états signifient « attendez », et il est important de les distinguer : l'un signifie que Wajub n'a encore contacté aucun opérateur, l'autre que le client reçoit la demande de confirmation en ce moment même. Faire une nouvelle tentative pendant le second crée un deuxième débit.

Les états

Aiguillez sur `status`, jamais sur le message

Le champ message est rédigé pour des humains et peut changer sans préavis. La valeur status fait partie du contrat de l'API, et c'est sur elle que votre code doit se baser. Aiguillez sur elle plutôt que sur l'événement reçu, car plusieurs événements peuvent annoncer le même état.

statusSignificationWebhookQue faire
pendingLe paiement existe. Aucun opérateur n'a encore été contacté.payment.createdRedirigez le client, ou faites l'appel de débit. Rien à livrer.
processingEnvoyé à l'opérateur. Le client reçoit la demande de confirmation sur son téléphone.payment.processingAttendez. Ne faites pas de nouvelle tentative et ne créez pas de second paiement.
partialCertaines échéances d'un paiement fractionné sont réglées, d'autres non.payment.split.*Attendez. amount_paid sur le paiement indique ce qui est réglé jusqu'ici.
succeededFonds capturés.payment.succeededLivrez la commande.
failedLa tentative n'a pas abouti.payment.failedLisez failure_reason, puis décidez si une nouvelle tentative a du sens.
cancelledAnnulé par vous avec DELETE /payments/{id}, ou par le client avec le bouton Annuler de la page hébergée. Un refus sur le téléphone n'est pas une annulation.payment.cancelledProposez de lancer un nouveau paiement.
expiredLe délai de paiement s'est écoulé avant que le client agisse.payment.expiredCréez un nouveau paiement. L'ancien ne pourra jamais aboutir.

Quatre de ces états mettent fin à la tentative : succeeded, failed, cancelled et expired. Wajub horodate ce moment pour décider combien de temps la page hébergée reste accessible. succeeded ne bouge plus jamais, sauf par les remboursements : depuis la version d'API 2026-10-11, un paiement remboursé se lit refunded ou partially_refunded.

Les transitions possibles

Wajub ne fait évoluer un paiement que selon les chemins ci-dessous. Un statut inattendu après un autre est un bug de notre côté, pas un état que vous devez gérer.

DepuisPeut devenir
pendingprocessing · partial · succeeded · failed · cancelled · expired
processingpending · partial · succeeded · failed · cancelled · expired
partialprocessing · succeeded · failed · cancelled
failed · cancelled · expiredsucceeded uniquement

Deux de ces chemins surprennent. processing revient à pending quand l'opérateur refuse un débit immédiatement, avec un 402, pour que le même paiement puisse être débité de nouveau ; payment.attempt_failed en informe votre serveur. Et un paiement failed, cancelled ou expired peut encore devenir succeeded quand l'opérateur confirme l'argent en retard : livrez sur payment.succeeded, quel que soit le moment où il arrive. Il le devient aussitôt quand votre propre prestataire a encaissé. Quand c'est un rail Wajub, ou quand une autre tentative avait déjà encaissé le paiement, Wajub examine d'abord la confirmation tardive : soit il crédite le paiement, et payment.succeeded suit, soit il rembourse le payeur et le paiement reste tel quel. Rien d'autre ne fait sortir de ces trois états, donc une nouvelle tentative après eux passe par un nouveau paiement.

Quatre points qui surprennent

processing signifie que le téléphone du client sonne

C'est l'état dans lequel votre intégration passera le plus de temps, et celui qui a le plus de chances d'être mal géré. En Mobile Money, processing signifie que l'opérateur a envoyé une demande de confirmation sur le téléphone du client et attend qu'il saisisse son code PIN. Cela peut prendre quelques secondes, ou le temps qu'il faut à quelqu'un pour retrouver son téléphone.

Un remboursement ne modifie le status du paiement qu'à partir de la version 2026-10-11

C'est le point qui piège le plus d'intégrations, et il dépend de votre version d'API. Depuis 2026-10-11, un paiement se lit partially_refunded dès que des remboursements réussis en couvrent une partie, et refunded quand ils le couvrent en entier. Sur une version antérieure, il affiche toujours succeeded, en totalité ou en partie, pour toujours. Dans les deux cas, c'est une lecture des remboursements : le paiement lui-même ne change pas, et il n'existe pas d'événement payment.refunded. Ce que dit le statut du paiement

Les remboursements ont à la place leur propre famille d'événements : refund.created, refund.processing (en live uniquement), refund.succeeded, refund.failed et refund.cancelled. Pour savoir quelle part d'un paiement a été restituée, listez ses remboursements avec GET /payments/{id}/refunds et additionnez ceux qui ont réussi.

expired s'annonce par payment.expired

Un paiement expiré est annoncé par payment.expired, que sa fenêtre se soit fermée ou qu'un numéro sandbox de délai dépassé y ait mis fin. Un délai dépassé en sandbox s'annonçait comme payment.failed ; ce n'est plus le cas. Aiguillez tout de même sur status : c'est à cela que sert la règle en haut de la page.

partial ne concerne que les paiements fractionnés

Un paiement peut être réglé en deux à quatre échéances, en sandbox et, sur le Mobile Money, en live (Paiements fractionnés). Tant que certaines sont payées et d'autres non, son statut est partial, processing pendant qu'une échéance attend le payeur, et chaque échéance rend compte d'elle-même via payment.split.processing, payment.split.succeeded et payment.split.failed.

Il n'existe pas d'événement payment.partial, et un abonnement ne reconnaît que des noms exacts : payment.* ne correspond à rien. Listez les trois événements payment.split par leur nom, ou abonnez-vous à *. Le paiement ne porte pas la liste de ses échéances : amount_paid est la référence de ce qui a été réglé, et chaque événement payment.split.* indique quelle échéance a bougé.

Un paiement partial reste ouvert jusqu'au paiement de sa dernière échéance : rien ne l'expire ni ne l'annule. Pour l'arrêter avant, clôturez-le avec POST /payments/{id}/close : il devient succeeded pour ce qui a été encaissé, que vous gardez ou remboursez (outcome). DELETE /payments/{id} refuse un paiement dont des échéances sont déjà payées.

Expiration

Chaque paiement porte une date limite, fixée avec expires.in à la création, en minutes, et relue dans son expires_at :

Minimum5 minutes
Par défaut1 440 minutes (24 heures)
Maximum43 200 minutes (30 jours)

Le plancher est de cinq minutes, car une demande de confirmation Mobile Money a besoin de temps pour atteindre le téléphone et être confirmée. Des fenêtres plus courtes expirent avant que le client puisse payer.

L'expiration n'est pas instantanée : elle peut suivre la date limite de quelques minutes. Un paiement dont la date limite vient d'être dépassée peut encore afficher pending un moment, et payment.expired arrive un peu après la date limite plutôt qu'à la seconde près.

Ce délai est voulu, ce n'est pas une erreur d'arrondi. Ne calculez pas l'expiration vous-même pour décider qu'une commande est morte : un opérateur peut encore confirmer un paiement dans les dernières secondes de sa fenêtre, et l'expiration ne s'applique qu'aux paiements qui n'ont jamais été crédités. Elle ne s'applique pas non plus à un paiement réglé en tranches dès que l'une d'elles est payée : cet argent vient du payeur, et le paiement reste ouvert pour la tranche suivante. Un paiement partial n'expire jamais : il reste ouvert jusqu'à être soldé ou annulé.

Ce que vous recevez quand un paiement échoue

Un paiement échoué porte un failure_reason, et failure_message le dit en une phrase. La valeur est le code du prestataire qui a refusé la dernière tentative, et Wajub route entre de nombreux prestataires, ou payment_failed quand cette tentative a échoué sans code du prestataire. Voir Motifs d'échec.

Aiguillez sur status pour le déroulement du code. Utilisez failure_reason pour expliquer à un humain ce qui s'est passé, et pour décider manuellement si une catégorie d'échec mérite une nouvelle tentative.

Annuler un paiement

Un paiement pending, processing ou partial peut être annulé depuis votre serveur, quand le client abandonne la commande de votre côté.

DELETEhttps://api.wajub.com/payments/{id}

Le paiement passe en cancelled, payment.cancelled est envoyé, et sa page n'accepte plus de paiement. Tout autre statut répond 400 This payment cannot be cancelled (already completed or expired). Une confirmation que l'opérateur envoie après l'annulation fait quand même passer le paiement en succeeded, comme décrit plus haut.

Lister les paiements par état

GEThttps://api.wajub.com/payments
Paramètre de requêteValeurs acceptées
statuspending, processing, partial, succeeded, failed, cancelled, expired
referenceExactement la reference envoyée à la création. Elle n'est pas unique : chaque paiement qui la porte est renvoyé
searchTexte libre sur l'id du paiement, votre reference et la description
date_from, date_toDates, comparées à la date de création
amount_min, amount_maxMontants dans l'unité principale de la devise
per_pageDe 1 à 100, 25 par défaut
cursorPasse à la pagination par curseur

status accepte aussi refunded et partially_refunded. À partir de la version 2026-10-11 de l'API, ils listent les paiements remboursés en totalité ou en partie, que succeeded laisse alors de côté. Sur une version antérieure, ils renvoient une liste vide. Ce que dit le statut du paiement

Pour voir les paiements qui attendent encore un client, filtrez sur pending :

curl -G https://api.wajub.com/payments \
  -H "Authorization: $WAJUB_API_KEY" \
  -d status=pending \
  -d per_page=50

Le reçu d'un paiement réussi

Une fois le paiement réussi, il porte receipt_url : le reçu du payeur, une page hébergée sur votre domaine de factures, dans la langue du payeur. Le lien est signé, et c'est la signature de son paramètre s qui l'ouvre : transmettez l'URL telle quelle.

Le même document existe sous deux autres formes. Ajoutez /pdf au chemin pour un PDF, ou /data pour le reçu en JSON, en gardant la chaîne de requête.

Que pensez-vous de ce contenu ?