InstafuelInstafuel
FREN
  • Guides
  • Référence
  • Référence API
Guides
  • Démarrage rapide
  • Clés d'API
  • Référence API
Référence
  • Codes d'erreur
  • Glossaire
  • English
Environnement
  • Staging — instafuel-backend-staging.up.railway.app

© Instafuel — Abidjan, Côte d'Ivoire

AccueilDémarrage rapideAuthentificationClés d'APIApporteur d'affairesCrédit walletErreurs et idempotenceLimites et quotasPérimètreWebhooks
powered by Zudoku
Guides

Erreurs et idempotence

Format des erreurs

Toute erreur a la même forme, quel que soit l'endpoint :

{ "error": { "code": "RESELLER_CREDIT_CAP_EXCEEDED", "message": "Montant supérieur au plafond par opération" } }
ChampContrat
codestable. Branchez votre logique dessus.
messagefrançais, destiné à l'affichage. Peut changer sans préavis — ne le parsez jamais.
detailsprésent sur les erreurs de validation : liste de { field, messages[] }.

Une validation échouée détaille chaque champ fautif, et tous les motifs pour chacun :

{ "error": { "code": "VALIDATION_ERROR", "message": "Validation échouée", "details": [ { "field": "phoneNumber", "messages": [ "Numéro ivoirien attendu, format +225XXXXXXXXXX" ] }, { "field": "amountFcfa", "messages": [ "Doit être un entier", "Doit être strictement positif" ] } ] } }

Une gestion correcte, côté client :

Code
const reponse = await fetch(url, options); if (!reponse.ok) { const { error } = await reponse.json(); switch (error.code) { case "RESELLER_DAILY_CAP_EXCEEDED": return refuserAvecMessageMetier(error.message); case "RATE_LIMIT_EXCEEDED": return replanifier(Number(reponse.headers.get("X-RateLimit-Reset"))); default: // Un code inconnu se traite selon son statut HTTP, jamais en silence throw new ErreurInstafuel(reponse.status, error.code, error.message); } }
Code
reponse = requests.get(url, headers=entetes, timeout=30) if not reponse.ok: erreur = reponse.json()["error"] if erreur["code"] == "RESELLER_DAILY_CAP_EXCEEDED": refuser_avec_message_metier(erreur["message"]) elif erreur["code"] == "RATE_LIMIT_EXCEEDED": replanifier(int(reponse.headers["X-RateLimit-Reset"])) else: raise ErreurInstafuel(reponse.status_code, erreur["code"], erreur["message"])

Codes HTTP

CodeSignification
400requête malformée ou en-tête obligatoire manquant
401clé absente, invalide, expirée ou révoquée
403authentifié, mais hors périmètre, hors droits, ou IP non autorisée
404ressource inexistante, ou fonctionnalité non activée pour vous
409conflit — unicité violée, ou rejeu avec un corps différent
422règle métier violée : plafond dépassé, rôle hors liste blanche, validation échouée
429limite de débit dépassée
5xxerreur serveur

La distinction 400 / 422 est utile : 400 signifie « votre requête est mal formée », 422 signifie « votre requête est correcte mais l'opération est refusée ». Un plafond dépassé est un 422 — inutile de retenter à l'identique.

Idempotence

Toute mutation exige l'en-tête X-Idempotency-Key : un UUID que vous générez.

curl -X POST "https://instafuel-backend-staging.up.railway.app/v1/papi/companies/4832/wallet/credit" \ -H "Authorization: Bearer $INSTAFUEL_API_KEY" \ -H "X-Idempotency-Key: 8f14e45f-ea0c-4f7e-9a1b-3d2c1e0b9a87" \ -H "Content-Type: application/json" \ -d '{ "amountFcfa": 500000, "source": "BANK_TRANSFER", "clientReference": "VIR-2026-0814", "proofIds": [ "11111111-1111-4111-8111-111111111111" ] }'

Ce que fait un second appel

Le premier appel enregistre la demande :

{ "status": "APPROVAL_PENDING", "approvalId": "0193a1f2-7c44-7c1e-9b0a-5f2d1c8e4b60", "amountFcfa": 500000, "requestedAt": "2026-08-31T09:04:12.000Z", "expiresAt": "2026-09-07T09:04:12.000Z" }

Le même appel rejoué avec la même clé et le même corps ne crée pas de seconde demande : il renvoie la réponse mémorisée, à l'identique — même approvalId, même requestedAt — accompagnée de l'en-tête X-Idempotent-Replay: true.

{ "status": "APPROVAL_PENDING", "approvalId": "0193a1f2-7c44-7c1e-9b0a-5f2d1c8e4b60", "amountFcfa": 500000, "requestedAt": "2026-08-31T09:04:12.000Z", "expiresAt": "2026-09-07T09:04:12.000Z" }

La même clé avec un corps différent est en revanche un bug côté appelant, et l'API refuse de trancher à votre place :

{ "error": { "code": "IDEMPOTENCY_CONFLICT", "message": "Cette clé d'idempotence a déjà été utilisée avec un corps différent", "firstSeenAt": "2026-08-31T09:04:12.000Z" } }

Et sans l'en-tête du tout :

{ "error": { "code": "IDEMPOTENCY_KEY_MISSING", "message": "En-tête X-Idempotency-Key obligatoire sur cette opération" } }

La clé est conservée 24 h.

Une clé par opération métier, pas par tentative

Générez la clé au moment où l'opération est décidée — pas au moment de l'envoi — et réutilisez-la pour toutes les tentatives réseau de cette opération.

Générer une nouvelle clé à chaque retry annule toute la protection : chaque tentative devient une opération distincte, et un versement de 500 000 FCFA déclaré trois fois donne trois demandes.

Code
// La clé appartient à l'opération, pas à la requête HTTP const operation = { cle: crypto.randomUUID(), corps: { amountFcfa: 500000 } }; for (let tentative = 1; tentative <= 3; tentative++) { const reponse = await envoyer(operation); // même clé à chaque tour if (reponse.ok || !estRetentable(reponse.status)) return reponse; await attendre(2 ** tentative * 1000); }
Code
<?php // La clé appartient à l'opération : générée une fois, réutilisée à chaque essai $cleIdempotence = bin2hex(random_bytes(16)); for ($tentative = 1; $tentative <= 3; $tentative++) { [$statut, $reponse] = envoyer($cleIdempotence, $corps); if ($statut < 400 || !estRetentable($statut)) { return $reponse; } sleep(2 ** $tentative); }

Deuxième couche, côté wallet

Indépendamment de l'en-tête HTTP, chaque écriture au grand livre porte une clé dérivée du wallet et de la référence du versement, sous contrainte d'unicité en base. Une même clientReference sur un même wallet ne peut donc pas produire deux écritures, même si l'en-tête a été mal géré.

Cette référence est préfixée côté serveur par votre identifiant d'apporteur : deux partenaires utilisant la même référence sur un même wallet n'entrent pas en collision.

Retenter, ou pas

CodeRetenter ?
429, 5xxoui — backoff exponentiel, même clé d'idempotence
408, coupure réseau, délai dépasséoui — même clé : c'est précisément le cas qu'elle couvre
401non, sauf si vous venez de faire tourner la clé
400, 403, 404, 409, 422non — un rejeu à l'identique produira le même résultat

Le silence n'est pas un échec

Si la connexion tombe avant la réponse, l'opération a peut-être abouti. Ne la considérez ni comme réussie ni comme échouée : rejouez-la avec la même clé d'idempotence. Vous récupérerez la réponse mémorisée si elle avait abouti, et l'opération sera exécutée sinon.

403 ou 404 ?

Les deux existent et ne veulent pas dire la même chose :

  • 403 — la ressource est identifiée et se trouve hors de votre périmètre.
  • 404 — la ressource n'existe pas, ou le périmètre est appliqué directement dans la requête, ce qui rend les deux cas indiscernables.

Un 404 sur un identifiant que vous croyez valide signifie donc en général qu'il appartient à quelqu'un d'autre. Voir Périmètre.

Fonctionnalités non activées

Un module non ouvert pour votre compte répond 404 FEATURE_DISABLED, pas 403. Tant qu'une fonctionnalité n'est pas activée, elle est invisible : vous ne pouvez pas déduire son existence en sondant l'API.

{ "error": { "code": "FEATURE_DISABLED", "message": "Fonctionnalité non activée pour ce compte" } }

Si une opération documentée ici renvoie ce code, c'est qu'elle doit être activée côté Instafuel pour votre compte.

Last modified on September 2, 2026
Crédit walletLimites et quotas
On this page
  • Format des erreurs
  • Codes HTTP
  • Idempotence
    • Ce que fait un second appel
    • Deuxième couche, côté wallet
  • Retenter, ou pas
  • 403 ou 404 ?
  • Fonctionnalités non activées
JSON
JSON
Javascript
Javascript
PHP
JSON
JSON
JSON
JSON
Javascript
JSON