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
| Code | HTTP | Quand | Que faire | Relancer |
|---|---|---|---|---|
validation_error | 422 · 400 | Corps invalide (détail champ par champ dans details), encaissement non remboursable, ou curseur de liste illisible (400). | Corriger la requête. | Non |
idempotency_key_required | 400 | Écriture envoyée sans en-tête Idempotency-Key. | Ajouter l’en-tête, et le signer. | Non |
unauthorized | 401 | En-tête manquant, clé inconnue, horodatage hors fenêtre ou signature invalide. | Lire le message, puis le diagnostic de l’authentification. | Non |
forbidden | 403 | Compte en cours de vérification : encaissements et reversements live indisponibles. | Attendre la vérification du dossier. Le Sandbox reste ouvert. | Non |
not_found | 404 | Ressource introuvable avec cette clé. | Vérifier l’id et l’environnement : un id Sandbox n’existe pas en live. | Non |
idempotency_conflict | 409 | Clé 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_progress | 409 | La première requête portant cette clé est encore en cours. | Attendre Retry-After, puis renvoyer la même requête. | Oui |
conflict | 409 | L’état actuel de la ressource ne permet pas cette action. | Relire la ressource avant d’agir. | Non |
merchant_suspended | 403 | Le 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_blocked | 422 | Opération bloquée par les contrôles anti-fraude. | Ne pas renvoyer. Contacter le support avec le correlation_id. | Non |
rate_limited | 429 | Quota de requêtes atteint. | Attendre Retry-After secondes. | Oui |
internal_error | 500 | Incident de notre côté. | Relancer avec la même Idempotency-Key, en reculant. | Oui |
http_error | autre | Toute 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 :
- Ne relancer que ce qui peut réussir plus tard : une coupure réseau, un
429, un5xx, ouidempotency_in_progress. Une requête invalide le restera. - 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.
- Reculer : respecter
Retry-Afterquand 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));# La MÊME Idempotency-Key à chaque tentative : une relance
# ne crée jamais un second encaissement.
import json, time, urllib.error
def avec_relances(envoyer, tentatives=4):
"""`envoyer` re-signe à chaque appel : l'horodatage change."""
for n in range(tentatives + 1):
try:
with envoyer() as resp:
return json.load(resp)
except urllib.error.HTTPError as e:
try:
erreur = json.load(e)["error"]
except ValueError:
erreur = {} # un 502 de proxy n'est pas du JSON
relancable = (e.code == 429 or e.code >= 500
or erreur.get("code") == "idempotency_in_progress")
if not relancable or n == tentatives:
raise RuntimeError(
f"{e.code} {erreur.get('code')} {erreur.get('correlation_id')}"
) from e
time.sleep(int(e.headers.get("Retry-After") or 2 ** n))
except urllib.error.URLError:
# Réseau coupé : la requête est peut-être arrivée. La relancer
# avec la même clé est sans risque.
if n == tentatives:
raise
time.sleep(2 ** n)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.