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èleUne 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 validation | Identiques. |
| Les modèles que vous pouvez appeler | Identiques. 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 402 | Identiques, et déclenchables à la demande (ci-dessous). |
| Les limites de débit | Identiques. 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és | Compté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étion | Une réponse préenregistrée fixe. Aucun fournisseur n'est appelé, aucun quota de modèle n'est consommé. |
| L'argent | Un solde fictif par utilisateur de test, doté d'une allocation au premier appel. |
Le topupUrl d'un 402 | Un substitut sans jeton, voir plus bas. |
| Les webhooks | Aucun. 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épense | Le 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 :
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
appIdet un mêmeuserIdsur 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épond402au 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()etgetUsageSummary()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 en402et un résumé vide, plutôt que le solde fictif. Lisez le solde de bac à sable viaGET /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
402ordinaire 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.