Fleeexdocs

Bac à sable

Une clé de test fait tourner tout le pipeline sur un solde fictif et n'appelle aucun fournisseur de modèle : vous pouvez intégrer et reproduire chaque refus sans payer.

Chaque application peut détenir une clé de test à côté de sa clé live. Une clé bac à sable fait tourner tout le pipeline Fleeex, avec le même protocole, la même validation et les mêmes refus, mais elle n'appelle aucun fournisseur de modèle et dépense un solde fictif au lieu du portefeuille de votre utilisateur final.

Rien de ce qu'elle fait n'est facturé, et rien de ce qu'elle fait n'apparaît dans vos métriques d'usage ni sur la facture de qui que ce soit.

Émettre une clé de test

Une clé bac à sable vient de la route de rotation ordinaire avec mode: "sandbox", sous les mêmes règles de propriété qu'une clé live, depuis le compte qui a enregistré l'application :

curl -X POST https://api.fleeex.dev/apps/<appId>/keys \
  -H 'authorization: Bearer <votre jeton de session fleeex>' \
  -H 'content-type: application/json' \
  -d '{"mode":"sandbox"}'
# → { "appId": "…", "keyId": "…", "apiKey": "flx_test_…", "mode": "sandbox" }

Pointez ensuite le client dessus. Rien d'autre ne change :

const client = new FleeexClient({
  apiKey: process.env.FLEEEX_TEST_KEY!, // flx_test_…, un identifiant bac à sable
  userId: "test-user-1",
});
 
const completion = await client.chat.completions.create({
  model: "nova-lite",
  messages: [{ role: "user", content: "Bonjour !" }],
});
// → une réponse Fleeex préenregistrée, pas une réponse de modèle

Une clé de test se voit comme telle (préfixe flx_test_) et ne peut pas être promue. Aucun geste ne change le mode d'une clé : une clé de test fuitée n'achète donc rien.

Une complétion servie en bac à sable porte aussi un en-tête de réponse x-fleeex-mode: sandbox, y compris sur un 402 de bac à sable : vous distinguez donc les deux mondes à la seule lecture de la réponse. C'est un en-tête plutôt qu'un champ du corps, parce que le corps reste iso-OpenAI et qu'un client généré peut échouer sur une propriété inconnue. Seule la route des complétions le pose ; les aides de facturation ne le posent pas, pour la raison exposée dans les deux limites ci-dessous.

Le portefeuille fictif d'un utilisateur de test est approvisionné automatiquement à son premier appel : il n'y a donc aucune étape de préparation avant que le code ci-dessus fonctionne.

Ce qui est identique, et ce qui ne l'est pas

Bac à sable
Le protocole, la surface de paramètres, les erreurs de validationIdentiques.
Les modèles que vous pouvez appelerIdentiques. Un modèle réservé reste un 403 MODEL_NOT_ENTITLED. Le droit de licence n'est pas une question d'argent, et l'intérêt est de rencontrer en test ce que ferait la production.
Les codes et corps de 402Identiques, et déclenchables à la demande (ci-dessous).
Les limites de débitIdentiques. Une clé bac à sable compte dans le seau de votre propre application : un identifiant gratuit n'est donc pas un contournement de la limite.
Les places de flux simultanésComptées dans un espace de noms distinct, pour qu'un flux bac à sable ne consomme jamais les places d'une identité payante.
Le texte de la complétionUne réponse préenregistrée fixe. Aucun fournisseur n'est appelé, aucun quota de modèle n'est consommé.
L'argentUn solde fictif par utilisateur de test, doté d'une allocation au premier appel.
Le topupUrl d'un 402Un substitut sans jeton, voir plus bas.
Les webhooksAucun. Le bac à sable ne touche aucun portefeuille réel : il n'y a donc rien à notifier.
Vos métriques d'usage, factures et rapports de dépenseLe trafic bac à sable n'y apparaît jamais.

Provoquer les parcours que vous devez gérer

Les chemins les plus difficiles à tester sont ceux qui exigent d'être réellement à court d'argent. En bac à sable, vous posez l'état à la place, depuis le plan de contrôle :

provoquer les refus
BASE=https://api.fleeex.dev/apps/<appId>/sandbox
AUTH='authorization: Bearer <votre jeton de session fleeex>'
 
# Forcer le « fonds insuffisants » : l'appel suivant répond 402 PAYMENT_REQUIRED avec son lien.
curl -X POST "$BASE/users/test-user-1/reset" -H "$AUTH" \
  -H 'content-type: application/json' -d '{"balanceMicros":0}'
 
# Forcer le plafond mensuel de dépense : l'appel suivant répond 402 APP_SPEND_CAP_EXCEEDED.
curl -X POST "$BASE/users/test-user-1/reset" -H "$AUTH" \
  -H 'content-type: application/json' -d '{"spendCapMicros":1}'
 
# Forcer un portefeuille suspendu : 402 WALLET_SUSPENDED, et aucun lien de rechargement.
curl -X POST "$BASE/users/test-user-1/reset" -H "$AUTH" \
  -H 'content-type: application/json' -d '{"suspended":true}'
 
# Remettre l'allocation et continuer.
curl -X POST "$BASE/users/test-user-1/reset" -H "$AUTH"
 
# Voir où en est chaque utilisateur de test (solde fictif, réservations, requêtes, tokens).
curl "$BASE" -H "$AUTH"

Une remise à zéro efface aussi les réservations, les compteurs et toute suspension : chaque scénario part donc d'un état que vous avez nommé. WALLET_SUSPENDED est le seul refus qu'une intégration réelle ne pourrait autrement atteindre qu'au travers d'une vraie opposition bancaire. Voir la référence des erreurs pour la signification de chaque code.

Notez l'asymétrie, elle est délibérée : une clé d'API bac à sable ne peut pas se réapprovisionner elle-même. L'approvisionnement est un geste de plan de contrôle réservé au propriétaire de l'application, faute de quoi le « fonds insuffisants » serait un état que votre intégration ne pourrait jamais être amenée à affronter.

Un 402 de bac à sable ne peut pas encaisser

Le corps du 402 garde sa forme documentée : l'intégration testée le traite donc exactement comme elle le fera en production. Mais son topupUrl ne porte aucun jeton : c'est un substitut de bac à sable.

C'est volontaire. Un refus de test ne doit pas pouvoir ouvrir la vraie page d'onboarding, à un saut d'un vrai Checkout Stripe. La redirection est donc exerçable, et le paiement hors d'atteinte.

Le trafic bac à sable ne peut pas fuiter dans les données de production

Ce n'est pas un filtre que quelqu'un doit penser à poser. C'est une séparation structurelle :

  • Une requête bac à sable n'écrit aucun événement d'usage, aucune ligne de grand livre et rien dans le magasin des portefeuilles : aucun agrégat, aucune facture, aucun rapprochement, aucun export fiscal ni aucune métrique de revenu n'a de source pour elle.
  • L'argent fictif vit dans une table différente de l'argent réel, dans son propre espace de partitions : les deux ne peuvent donc pas se mélanger dans un solde ou un agrégat.
  • Un même appId et un même userId sur les deux identifiants sont donc sans danger, et c'est ce qui permet à une application de détenir les deux. Une clé de test ne bouge pas un portefeuille réel approvisionné pour le même couple (app, utilisateur), et une clé live sur un portefeuille non approvisionné répond 402 au lieu de dépenser le solde de bac à sable posé juste à côté.

Les portefeuilles de test sont jetables : un portefeuille abandonné expire, et l'appel suivant en reprovisionne un avec une allocation neuve.

Les deux limites à connaître

⚠ getBalance() et getUsageSummary() ne fonctionnent qu'en live. Elles résolvent la correspondance (app, utilisateur) réelle : avec une clé de test, elles répondent donc le contrat d'onboarding en 402 et un résumé vide, plutôt que le solde fictif. Lisez le solde de bac à sable via GET /apps/<appId>/sandbox.

L'allocation fait office de budget. Un appel en bac à sable n'atteint aucun fournisseur : le servir ne coûte donc rien. L'allocation (5 € par défaut) borne le nombre d'appels qu'un utilisateur de test peut faire avant que le 402 ordinaire ne l'arrête. Réinitialisez pour continuer.

Quelle clé est laquelle

Une clé bac à sable est un identifiant de l'application : gardez-la dans une variable d'environnement distincte et choisissez par environnement plutôt que de faire un branchement dans le code.

export const client = new FleeexClient({
  apiKey: process.env.FLEEEX_API_KEY!, // flx_test_… en recette, flx_… en production
  userId,
});

GET /apps/<appId>/keys indique le mode de chaque clé : vous n'avez donc jamais à analyser une clé pour savoir ce qu'elle fait.