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, ou lisez le détail des échéances sur le paiement.
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é, soit par le client sur son téléphone, soit par vous.payment.cancelledProposez de lancer un nouveau paiement.
expiredLe délai de paiement s'est écoulé avant que le client agisse.payment.expired ou payment.failedCréez un nouveau paiement. L'ancien ne pourra jamais aboutir.

Quatre de ces états sont terminaux : succeeded, failed, cancelled et expired. Une fois qu'un paiement en atteint un, plus rien ne le fait bouger, et Wajub horodate ce moment pour décider combien de temps sa page hébergée reste accessible.

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 · cancelled · expired · failed
processingsucceeded · partial · failed · cancelled
partialsucceeded · processing · failed · cancelled

Remarquez ce qui manque : rien ne revient à pending, et un paiement failed, cancelled ou expired ne revient jamais à la vie. Quand il vous faut une autre tentative, vous créez 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 jamais le status du paiement

C'est le point qui piège le plus d'intégrations. Un paiement remboursé, en totalité ou en partie, affiche toujours succeeded sur GET /payments/{id}, et pour toujours. L'API ne réécrit pas le statut, et il n'existe pas d'événement payment.refunded.

Les remboursements ont à la place leur propre famille d'événements : refund.created, 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 peut vous parvenir sous deux noms

Un paiement expiré est annoncé par payment.expired quand la tâche de nettoyage qui ferme les sessions périmées le prend en charge. Un numéro sandbox de délai dépassé aboutit au même statut expired par un autre chemin et s'annonce comme payment.failed.

Les deux signifient la même chose et les deux sont terminaux. C'est précisément pour cela que la règle en haut de la page est d'aiguiller sur status plutôt que sur le nom de l'événement.

partial ne concerne que les paiements fractionnés

Un paiement peut être réglé en deux à quatre échéances. Tant que certaines sont payées et d'autres non, son statut est partial, 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 : un écouteur abonné à la seule famille payment.* ne voit rien entre processing et le résultat final. Abonnez-vous aussi à payment.split.*, et considérez le détail des échéances sur le paiement comme la référence de ce qui a été réglé.

Expiration

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

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. Une tâche planifiée passe sur les sessions périmées toutes les cinq minutes : un paiement dont la date limite vient d'être dépassée peut encore afficher pending un moment, et payment.expired arrive au passage de cette tâche 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 la tâche de nettoyage ne touche que les paiements qui n'ont jamais été crédités.

Ce que vous recevez quand un paiement échoue

Un paiement échoué porte un failure_reason. Sa valeur vient du prestataire qui a traité la tentative, et Wajub route entre de nombreux prestataires.

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.

Que pensez-vous de ce contenu ?