Gestion des erreurs
Identifiez les échecs à réessayer, ceux à présenter au payeur et ceux à ne jamais répéter.
« L'appel a échoué » peut désigner plusieurs situations. Une carte refusée, un payload incorrect,
une clé sans autorisation et une connexion interrompue arrivent tous sous forme d'exception dans le
même bloc catch, mais nécessitent des réactions opposées. Réessayer un payload incorrect gaspille
votre quota. Ne pas réessayer après une interruption de connexion peut vous faire perdre une vente
qui allait aboutir.
Cette page vous aide à les distinguer. Le catalogue des codes se trouve dans Erreurs et celui des causes de refus dans Motifs d'échec.
Toutes les erreurs ont la même structure
Quel que soit le problème, le corps JSON contient toujours les trois mêmes clés, auxquelles s'ajoute
errors lorsqu'un champ est en cause. status contient la phrase associée au statut HTTP, jamais la
chaîne littérale error.
Les champs imbriqués utilisent la notation par points. Ainsi, customer.email dans la réponse
désigne directement customer.email dans les données envoyées.
Signification de chaque code
Deux de ces codes sont souvent mal interprétés, avec des conséquences financières réelles.
| Code | Signification | Action |
|---|---|---|
400 | La requête est bien formée, mais la ressource est dans un état incompatible | Lisez l'état, ne réessayez pas |
401 | La clé est inconnue, révoquée, inactive ou a dépassé expires_at | Corrigez l'identifiant |
402 | L'argent du payeur n'a pas été transféré | Affichez le motif et proposez un autre canal |
403 | Une clé restreinte n'a pas le scope requis ou l'IP est absente de sa liste d'IP autorisées | Corrigez la clé, pas l'appel |
404 | La ressource n'existe pas pour l'équipe et l'environnement de cette clé | Vérifiez l'identifiant et l'environnement |
405 | Le verbe est incorrect sur un chemin valide | Corrigez l'appel |
406 | La route exige une clé privée, mais vous avez envoyé une clé publique | Déplacez l'appel vers votre serveur |
422 | Un champ est incorrect ou une clé d'idempotence entre en conflit | Corrigez le payload, ne réessayez pas |
429 | Un plafond a été atteint | Appliquez une attente progressive, voir Gestion des limites de requêtes |
500 | Le problème vient de Wajub | Réessayez avec une attente progressive et déclenchez une alerte si le problème persiste |
La première confusion concerne 400. Ce n'est pas le code de validation : un montant incorrect ou
une devise absente renvoie 422. Un code 400 signifie par exemple « ce paiement ne peut pas être
annulé, car il est déjà terminé ». Vous devez donc examiner la ressource, jamais renvoyer le même
corps.
La seconde concerne 406. Une clé publique utilisée sur /balance, /transfers, /refunds,
/disputes ou /beneficiaries renvoie 406 Private Key Required, et non 403. Si votre code
attend 403 pour détecter une clé incorrecte, cette branche ne s'exécutera jamais.
Intercepter l'erreur
Chaque SDK lève des erreurs typées. Vous pouvez donc adapter le traitement à leur type plutôt qu'à
un nombre. Dans Node, le statut HTTP se trouve dans httpStatus et les erreurs de champs dans
errors.
# Print the body, then the status on its own line
curl -s -o /tmp/body.json -w '%{http_code}\n' \
https://api.wajub.com/payments \
-H "Authorization: sk_test.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…" \
-H "Content-Type: application/json" \
-d '{ "amount": 5000, "currency": "XAF", "email": "buyer@example.com" }'
cat /tmp/body.json | jq '.code, .message, .errors'`err.status` vaut undefined dans Node
La propriété correcte est httpStatus. Une condition de nouvelle tentative fondée sur
err.status compare undefined à un nombre. Elle est toujours fausse et aucune requête n'est
relancée.
Seuls trois types d'échecs méritent une nouvelle tentative
| Échec | Nouvelle tentative | Raison |
|---|---|---|
429 | Oui, après le délai indiqué par la réponse | Le plafond se réinitialise |
5xx | Oui, avec une attente progressive | Le problème vient de Wajub et reste généralement bref |
| Délai expiré ou connexion interrompue | Oui, avec la même clé d'idempotence | Vous ne savez pas si la requête a abouti |
4xx autre que 429 | Jamais | La même requête produit la même réponse |
La troisième ligne nécessite la clé. Un délai expiré sur POST /payments crée une situation
ambiguë : le paiement peut exister ou non, sans que votre code puisse le savoir. En réutilisant la
même Idempotency-Key, les deux situations aboutissent à un seul paiement. Sans elle, la nouvelle
tentative provoque un second débit. Idempotence explique comment
construire cette clé.
Par défaut, le SDK abandonne une requête après 30 secondes et lève une erreur de connexion. Réduisez ce délai sur une page de checkout où le client regarde un indicateur de chargement.
import { WajubRateLimitError, WajubConnectionError, WajubError } from '@wajub/node';
const isRetryable = (err) =>
err instanceof WajubRateLimitError ||
err instanceof WajubConnectionError ||
(err instanceof WajubError && (err.httpStatus ?? 0) >= 500);
export async function withRetry(fn, { maxAttempts = 4, baseDelay = 500 } = {}) {
for (let attempt = 1; ; attempt++) {
try {
return await fn();
} catch (err) {
if (!isRetryable(err) || attempt >= maxAttempts) throw err;
const hinted = err instanceof WajubRateLimitError ? err.retryAfter : undefined;
const backoff = baseDelay * 2 ** (attempt - 1);
const delay = hinted != null ? hinted * 1000 : backoff * (0.5 + Math.random());
await new Promise((r) => setTimeout(r, delay));
}
}
}
const payment = await withRetry(() =>
wajub.payments.create(params, { idempotencyKey: `ORDER-${order.id}`, timeout: 8000 }),
);La part d'aléa n'est pas accessoire. Sans elle, tous les workers qui échouent au même moment réessaient simultanément. La seconde vague devient alors aussi importante que la première.
Pour un script shell ou un job cron, cURL applique déjà la même stratégie. Il ne relance que les
échecs temporaires et laisse les réponses 4xx de côté.
curl --retry 4 --retry-delay 2 --retry-max-time 60 \
https://api.wajub.com/payments/trx_CSUGajfv9xh0XQ5wu2lx \
-H "Authorization: sk_test.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…"Un refus n'est pas une erreur à réessayer
402 Payment Required signifie que l'argent n'a pas été transféré : solde insuffisant, portefeuille
bloqué ou client ayant laissé la demande expirer. Réessayer un débit identique produit le même refus.
Sur certains canaux, le payeur reçoit aussi une nouvelle notification qu'il n'a pas demandée.
Le corps contient deux champs importants, destinés à des publics différents.
| Champ | Destinataire | Utilisation |
|---|---|---|
error_code | Votre code | Adaptez le traitement, enregistrez-le et comptez-le. Il est stable et traduisible |
payer_message | Le payeur | Affichez-le tel quel, il est déjà rédigé pour lui |
message | Vos logs | Le texte brut du prestataire, toujours en anglais |
Proposez au client un autre canal ou un autre numéro. Il s'agit d'un nouveau paiement avec une nouvelle clé d'idempotence, et non d'une nouvelle tentative du paiement précédent.
Les échecs de webhook sont réessayés par Wajub
Votre endpoint a une priorité : répondre rapidement avec 200. Toutes les opérations potentiellement
lentes, y compris celles sur votre base de données, doivent avoir lieu après cet accusé de réception.
Wajub accorde 10 secondes à chaque livraison et effectue cinq tentatives, espacées de 30 secondes,
1 minute, 5 minutes, 10 minutes et 1 heure. Toute réponse autre que 2xx, ainsi qu'un délai expiré,
compte comme un échec et déclenche ce calendrier.
C'est pourquoi l'échec d'un handler ne doit pas affecter la réponse. Si la livraison de la commande
lève une erreur et que vous répondez 500, Wajub renvoie l'événement, votre handler échoue de
nouveau et un même bug se reproduit cinq fois. Répondez 200, placez l'événement dans une file et
gérez vous-même les nouvelles tentatives.
Le seul cas qui doit produire une réponse autre que 2xx est une signature invalide. Renvoyez 400
et arrêtez le traitement : cette requête ne vient pas de Wajub et rien ne doit être livré à nouveau.
Disjoncteurs en cas de panne prolongée d'un prestataire
L'attente progressive gère une interruption brève. Elle ne suffit pas lorsqu'une panne dépasse la durée tolérée par votre file. Chaque job réessaie, échoue et recommence jusqu'à ce que la file ne contienne plus que des échecs.
Un disjoncteur arrête les appels après une série d'échecs, les refuse immédiatement pendant une période de récupération, puis laisse passer une seule requête pour vérifier le retour du service. Utilisez une implémentation éprouvée plutôt que d'en écrire une : opossum avec Node, ou son équivalent dans votre stack.
Son emplacement est essentiel. Encadrez l'appel attendu par un client afin que le checkout affiche un message clair plutôt qu'un indicateur de chargement pendant 30 secondes. Laissez le rapprochement en arrière-plan en dehors du disjoncteur, car ce traitement peut être lent et doit continuer à réessayer.
Pages associées
- ErreursTous les codes HTTP et le corps exact de chaque réponse.
- Motifs d'échecLa signification de chaque code de refus et les cas où une nouvelle tentative peut réussir.
- IdempotenceLa clé qui sécurise une nouvelle tentative après l'expiration d'un délai.
- Gestion des limites de requêtesLisez le délai dans une réponse 429 au lieu de l'estimer.