INOPAS
Fondamentaux

Erreurs

Toutes les erreurs ont la même forme : un code stable sur lequel votre code peut s’appuyer, un message lisible par un humain, et un identifiant de corrélation qui permet au support de retrouver l’appel exact.

La forme d’une erreur

code
Stable. C’est sur lui que votre code décide.
message
Pour un humain, en français. Il peut être reformulé : ne le comparez jamais dans votre code.
correlation_id
Le même que l’en-tête X-Correlation-ID. Journalisez-le avec chaque erreur.
details
Présent sur les erreurs de validation : une entrée par champ fautif, avec son chemin (field) et le problème (issue).
Réponse du Sandbox
HTTP/2 422

{
  "error": {
    "code": "validation_error",
    "message": "Validation de la requête échouée.",
    "correlation_id": "c066e5d59c02489c828973115a6be1ab",
    "details": [
      { "field": "body.amount_minor", "issue": "Input should be greater than 0" },
      { "field": "body.currency", "issue": "Input should be 'CDF' or 'USD'" }
    ]
  }
}

Les codes

Codes d’erreur de l’API /v1
CodeHTTPQuandQue faireRelancer
validation_error422 · 400Corps invalide (détail champ par champ dans details), encaissement non remboursable, ou curseur de liste illisible (400).Corriger la requête.Non
idempotency_key_required400Écriture envoyée sans en-tête Idempotency-Key.Ajouter l’en-tête, et le signer.Non
unauthorized401En-tête manquant, clé inconnue, horodatage hors fenêtre ou signature invalide.Lire le message, puis le diagnostic de l’authentification.Non
forbidden403Compte en cours de vérification : encaissements et reversements live indisponibles.Attendre la vérification du dossier. Le Sandbox reste ouvert.Non
not_found404Ressource introuvable avec cette clé.Vérifier l’id et l’environnement : un id Sandbox n’existe pas en live.Non
idempotency_conflict409Clé d’idempotence déjà utilisée pour une requête différente, ou encaissement déjà remboursé.Nouvelle opération : nouvelle clé. Relance : renvoyer le corps d’origine à l’identique.Non
idempotency_in_progress409La première requête portant cette clé est encore en cours.Attendre Retry-After, puis renvoyer la même requête.Oui
conflict409L’état actuel de la ressource ne permet pas cette action.Relire la ressource avant d’agir.Non
merchant_suspended403Le compte marchand est suspendu : paiements réels et reversements sont bloqués.Ne pas renvoyer. Contacter le support : seule la levée de la suspension débloque.Non
fraud_blocked422Opération bloquée par les contrôles anti-fraude.Ne pas renvoyer. Contacter le support avec le correlation_id.Non
rate_limited429Quota de requêtes atteint.Attendre Retry-After secondes.Oui
internal_error500Incident de notre côté.Relancer avec la même Idempotency-Key, en reculant.Oui
http_errorautreToute autre erreur HTTP, par exemple une méthode non permise (405).Vérifier la méthode et la route dans la référence.Non

Relancer sans débiter deux fois

Trois règles suffisent :

  1. Ne relancer que ce qui peut réussir plus tard : une coupure réseau, un 429, un 5xx, ou idempotency_in_progress. Une requête invalide le restera.
  2. Garder la même Idempotency-Key d’une tentative à l’autre, et re-signer à chaque fois (l’horodatage change). Si la première tentative avait abouti, vous recevez sa réponse au lieu d’un second encaissement.
  3. Reculer : respecter Retry-After quand il est présent, doubler l’attente sinon.
Relances avec recul
// La MÊME Idempotency-Key à chaque tentative : une relance
// ne crée jamais un second encaissement.
async function avecRelances(envoyer, tentatives = 4) {
  for (let n = 0; ; n++) {
    let res;
    try {
      res = await envoyer(); // re-signe à chaque appel : l'horodatage change
    } catch (coupure) {
      // Réseau coupé : la requête est peut-être arrivée. La relancer
      // avec la même clé est sans risque.
      if (n >= tentatives) throw coupure;
      await attendre(2 ** n);
      continue;
    }
    if (res.ok) return res.json();
    const corps = await res.json().catch(() => null); // un 502 de proxy n'est pas du JSON
    const code = corps?.error?.code;
    const relancable =
      res.status === 429 || res.status >= 500 || code === "idempotency_in_progress";
    if (!relancable || n >= tentatives) {
      throw new Error(`${res.status} ${code} ${corps?.error?.correlation_id}`);
    }
    await attendre(Number(res.headers.get("Retry-After")) || 2 ** n);
  }
}

const attendre = (s) => new Promise((r) => setTimeout(r, s * 1000));

Joindre le support

Donnez le correlation_id, la route appelée et l’heure de l’appel : c’est tout ce qu’il faut pour retrouver la requête de bout en bout. Ne transmettez jamais votre secret. Nous écrire.

Un seul branchement, tous les paiements.