Aller au contenu

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.

Un échec de validation
{
"code": 422,
"status": "Unprocessable Content",
"message": "The given data was invalid.",
"errors": {
"amount": [
"The amount must be at least 0.01."
],
"customer.email": [
"The customer.email field must be a valid email address."
]
}
}

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.

CodeSignificationAction
400La requête est bien formée, mais la ressource est dans un état incompatibleLisez l'état, ne réessayez pas
401La clé est inconnue, révoquée, inactive ou a dépassé expires_atCorrigez l'identifiant
402L'argent du payeur n'a pas été transféréAffichez le motif et proposez un autre canal
403Une clé restreinte n'a pas le scope requis ou l'IP est absente de sa liste d'IP autoriséesCorrigez la clé, pas l'appel
404La ressource n'existe pas pour l'équipe et l'environnement de cette cléVérifiez l'identifiant et l'environnement
405Le verbe est incorrect sur un chemin valideCorrigez l'appel
406La route exige une clé privée, mais vous avez envoyé une clé publiqueDéplacez l'appel vers votre serveur
422Un champ est incorrect ou une clé d'idempotence entre en conflitCorrigez le payload, ne réessayez pas
429Un plafond a été atteintAppliquez une attente progressive, voir Gestion des limites de requêtes
500Le problème vient de WajubRé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'

Seuls trois types d'échecs méritent une nouvelle tentative

ÉchecNouvelle tentativeRaison
429Oui, après le délai indiqué par la réponseLe plafond se réinitialise
5xxOui, avec une attente progressiveLe problème vient de Wajub et reste généralement bref
Délai expiré ou connexion interrompueOui, avec la même clé d'idempotenceVous ne savez pas si la requête a abouti
4xx autre que 429JamaisLa 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.

Réessayez avec une attente exponentielle et une part d'aléa
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é.

La même stratégie sans l'écrire
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.

ChampDestinataireUtilisation
error_codeVotre codeAdaptez le traitement, enregistrez-le et comptez-le. Il est stable et traduisible
payer_messageLe payeurAffichez-le tel quel, il est déjà rédigé pour lui
messageVos logsLe 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.

Que pensez-vous de ce contenu ?