Aller au contenu

Paiements

Statut pending bloqué, échec sans motif, double débit ou erreur 404 sur un identifiant valide.

Sept symptômes couvrent presque toutes les questions liées aux paiements. Repérez le vôtre et lisez la cause avant la solution. Trois d'entre eux proviennent de la même incompréhension.

Le paiement reste pending sans évolution

Un paiement Mobile Money attend une personne. Wajub a interrogé l'opérateur, qui a envoyé une demande sur un téléphone. Tant que personne ne saisit son code PIN, aucun résultat n'est disponible. pending n'est pas un état bloqué, mais un état d'attente.

Effectuez les vérifications suivantes dans cet ordre.

VérificationMéthode
Le payeur a-t-il ouvert la page ?authorization_url reste valable jusqu'à la fin du paiement et peut être rouvert : traitez-le comme un secret
La demande est-elle arrivée ?L'opérateur l'envoie, pas Wajub. La page hébergée affiche le code USSD en solution de repli
Le paiement est-il simplement trop récent ?En période de pointe, les opérateurs peuvent mettre une minute à envoyer la demande

Le paiement ne reste pas indéfiniment pending. Il expire automatiquement et passe à expired.

Un paiement qui dure 30 minutes au lieu d'une journée
curl https://api.wajub.com/payments \
  -H "Authorization: sk_test.xxxxxxxxxxxxxxxx…" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 5000,
    "currency": "XAF",
    "email": "[email protected]",
    "expires": { "in": 30 }
  }'

Le champ imbriqué est expires.in et sa valeur s'exprime en minutes, de 5 à 43200, soit 30 jours. La valeur par défaut est 1440. Écrire expires_in au premier niveau ne produit aucun effet. La valeur est ignorée et le délai reste fixé à 24 heures.

Le paiement a échoué sans motif visible

Un paiement ne porte aujourd'hui aucun code d'échec : failure_reason est absent du paiement et du webhook payment.failed. C'est status qui distingue les cas.

Lisez le statut
curl https://api.wajub.com/payments/trx_test_CSUGajfv9xh0XQ5wu2lx \
  -H "Authorization: sk_test.xxxxxxxxxxxxxxxx…" \
  | jq '.transaction | { status, channel, amount }'
statusCe qui s'est passé
failedL'opérateur a refusé le débit
expiredLe payeur n'a pas confirmé à temps
cancelledVous avez appelé DELETE /payments/{id}, ou le payeur a cliqué sur Annuler sur la page hébergée

Quand le refus arrive au moment du débit, le code se trouve dans la réponse à POST /payments/{id} : un 402 dont error_code le nomme, insufficient_funds par exemple, et le paiement revient à pending, prêt à être débité de nouveau. Pour les termes exacts de l'opérateur après coup, ouvrez le paiement dans Konsole : sa chronologie montre chaque tentative et la réponse brute du prestataire. Motifs d'échec présente les codes que portent les transferts et les remboursements.

Mon appel a expiré et j'ignore si le paiement existe

Cette situation est dangereuse, car les deux résultats sont possibles et un seul permet d'agir sans risque. Par défaut, le SDK attend 30 secondes par tentative et relance jusqu'à trois fois avec la même clé : l'erreur de connexion arrive donc au bout d'environ deux minutes, et elle n'indique pas ce que le serveur a effectué.

Ne créez pas un second paiement pour le vérifier. Renvoyez la même requête avec la même Idempotency-Key. Wajub renvoie le paiement initial s'il existe ou le crée dans le cas contraire.

Une nouvelle tentative sans risque de double débit
const payment = await wajub.payments.create(params, {
  idempotencyKey: `ORDER-${order.id}`,
});

Sans cette clé, la nouvelle tentative provoque un second débit. Idempotence explique comment construire une clé qui survit au redémarrage de votre processus.

Si la nouvelle tentative arrive pendant que la première requête est encore en cours, l'API répond 409 avec errors.idempotency_key : « A request with this Idempotency-Key is still being processed. » Rien n'a échoué : la réponse de la première requête arrive. Les SDK serveur attendent puis redemandent avec la même clé, après 2 puis 4 secondes (puis 8 en Node.js), et renvoient le paiement d'origine (à partir de Node.js et C# 2.0.0, PHP 2.0.1, Ruby 1.3.0 et 1.2.1 pour les autres). En HTTP brut, attendez quelques secondes et renvoyez la requête. Tout autre 409 est un vrai refus.

Le client a été débité deux fois

Ce problème provient presque toujours de l'une de deux causes.

La première est une nouvelle tentative sans clé d'idempotence, présentée ci-dessus. La seconde vient de l'idée que reference évite les doublons. Ce n'est pas le cas. reference est une chaîne libre sans règle d'unicité. Envoyer deux fois la même valeur crée deux paiements qui débitent tous les deux.

Si le problème s'est déjà produit, remboursez le doublon au lieu de l'annuler. Un paiement succeeded ne peut plus être annulé, seulement remboursé. Consultez Remboursements.

GET /payments/{something} renvoie 404 pour mon identifiant

Il existe trois causes, de la plus fréquente à la plus rare.

Vous avez transmis votre propre référence. L'endpoint accepte uniquement l'uid Wajub. Un paiement créé avec "reference": "ORDER-4172" n'est pas accessible depuis /payments/ORDER-4172 et répond avec 404 Payment Not Found. Enregistrez le champ id de la réponse de création avec votre commande, puis utilisez-le pour la recherche.

Valeur à conserver et valeur à ne pas utiliser pour la recherche
{
"code": 201,
"status": "Created",
"message": "Payment initiated",
"authorization_url": "https://pay.wajub.com/…",
"transaction": {
"id": "trx_test_CSUGajfv9xh0XQ5wu2lx",
"reference": "ORDER-4172"
}
}

id permet de retrouver le paiement. reference vous appartient et sert à vos propres recherches.

Vous utilisez le mauvais environnement. Une clé de la sandbox ne voit aucun paiement live, et inversement. Les deux ne partagent aucune donnée. La réponse est donc 404, pas une erreur d'autorisation. Le préfixe test_ de l'identifiant indique l'environnement concerné.

Le paiement appartient à une autre équipe. Les identifiants ne passent pas d'une équipe à l'autre. La réponse reste volontairement 404 plutôt que 403.

Le montant est refusé avec une réponse 422

Trois règles s'appliquent. Les utilisateurs provenant d'une autre plateforme rencontrent souvent la première.

Les montants utilisent les unités principales. 5000 signifie cinq mille francs, pas cinquante. Aucun centime ne doit être converti. XAF et XOF ne possèdent aucune sous-unité.

Chaque devise possède des limites. Le serveur les applique et la réponse 422 indique le champ concerné. Lisez errors.amount au lieu de deviner. Pour XAF et XOF, le minimum est faible et le maximum assez élevé. Vous les rencontrerez généralement après avoir envoyé une valeur dans la mauvaise unité.

Le Mobile Money possède son propre plafond, et un débit unique au-delà est refusé. Au-dessus de 500 000 XAF ou XOF, ou de l'équivalent dans une autre devise, la charge répond 422, en live comme en sandbox. Réglez-le en tranches avec POST /payments/{id}/splits : le paiement reste dans partial tant qu'une partie seulement est encaissée, et ne passe à succeeded qu'après la dernière tranche ou quand vous le clôturez. Considérez partial comme « pas encore payé », jamais comme un signal de livraison.

Tout fonctionne dans la sandbox, mais rien en live

Deux causes sont possibles.

Vos clés live peuvent exister alors que votre compte n'est pas encore activé. Les paiements live exigent la validation du KYC. Passer en live contient la checklist.

Vous utilisez peut-être aussi des numéros de test de la sandbox auprès de vrais opérateurs. Dans la sandbox, les six derniers chiffres du numéro déterminent le résultat. En live, ils identifient une personne. +237670000002 provoque un échec dans la sandbox, mais appartient à quelqu'un en production.

Suffixe de la sandboxRésultat
000000Réussite
000001La charge répond 422 pour fonds insuffisants, le paiement reste pending
000002Échec
000003Délai expiré
000004Annulé par le payeur
000009Paiement réussi, mais tous ses remboursements échouent

Que pensez-vous de ce contenu ?