Cette page t'a-t-elle aidé ?
Cette page t'a-t-elle aidé ?
Intégrer
Une API minimale pour piloter Gilbert depuis votre propre backend : créer des utilisateurs finaux, dialoguer avec eux, lire leur mémoire persistante, la supprimer. Pensée pour un chatbot embarqué dans votre produit, sans UI Gilbert.
Dernière mise à jour : 22 octobre 2026
| Endpoint | Description |
|---|---|
| POST /api/headless/v1/users | Find-or-create d'un utilisateur final à partir de votre identifiant. |
| POST /api/headless/v1/chat | Envoie un message, reçoit une réponse en stream NDJSON, avec sortie structurée optionnelle. |
| GET /api/headless/v1/memory | Liste la mémoire persistante d'un utilisateur final (100 souvenirs les plus récents). |
| POST /api/headless/v1/documents | Dépose un document dans l'espace pour l'indexation documentaire (RAG), en fichier ou texte déjà extrait. Jusqu'à 4 Mio d'enveloppe. |
| POST /api/headless/v1/documents/uploads | Temps 1 du dépôt d'un document volumineux : valide tout ce qui se valide sans les octets, et rend une URL d'envoi signée avec un billet. |
| POST /api/headless/v1/documents/register | Temps 2 du dépôt d'un document volumineux : confronte les octets déposés au billet, puis enregistre le document (jusqu'à 64 Mio). |
| POST /api/headless/v1/documents/read | Lit un document (PDF) et rend ce qui en est extrait, validé contre votre schéma. |
| POST /api/headless/v1/text/read | Lit un texte que vous avez déjà extrait et rend ce qui en est tiré, validé contre votre schéma. |
| GET /api/headless/v1/documents/capabilities | Dit, par couple (fournisseur, palier), ce que Gilbert sait de la capacité à lire un document. |
| POST /api/headless/v1/workspaces | Ouvre l'espace d'un client (idempotent par external_ref), avec la clé de conversation de ce nouvel espace client. |
| GET /api/headless/v1/workspaces | Liste tout le sous-arbre des espaces clients ouverts par votre clé, paginé par curseur. |
| PATCH /api/headless/v1/workspaces/{id} | Suspend ou réactive un espace client de votre sous-arbre. |
| GET /api/headless/v1/workspaces/{id} | Lit un espace client de votre sous-arbre, avec l'état de son repli sur la clé centrale de Solve et la dépense du mois. |
| PATCH /api/headless/v1/workspaces/{id}/central-fallback | Réservé au payeur effectif de l'arbre de cet espace : fixe le budget mensuel d'un espace de son arbre sur la clé centrale de Solve, et ouvre ou ferme le repli d'un espace ouvert par un revendeur. |
| POST /api/headless/v1/workspaces/{id}/close | Ferme définitivement un espace client : révoque ses clés, sans supprimer ses données. |
| POST /api/headless/v1/workspaces/{id}/chat-key/rotate | Réémet la clé de conversation d'un espace client que vous avez ouvert vous-même, en cas de perte. |
| GET /api/headless/v1/workspaces/{id}/task-routing | Dit quel fournisseur et quel palier servent chaque famille de tâches de cet espace client, et si la clé du fournisseur réglé est présente. |
| PUT /api/headless/v1/workspaces/{id}/task-routing | Règle le fournisseur et le palier d'une ou plusieurs familles. Les familles non citées ne bougent pas. |
| GET /api/headless/v1/workspaces/{id}/panel-theme | Lit les couleurs du panneau intégré de cet espace client : les siennes, et celles qui s'appliquent réellement. |
| PUT /api/headless/v1/workspaces/{id}/panel-theme | Règle les couleurs du panneau intégré de cet espace client, en clair et en sombre. |
| DELETE /api/headless/v1/workspaces/{id}/panel-theme | Retire les couleurs propres de cet espace client : il reprend celles de l'espace au-dessus de lui. |
| GET /api/headless/v1/workspaces/{id}/product-connection | Lit les connexions de votre produit sur un espace client que vous avez ouvert, avec l'empreinte de leur secret et leurs outils de panneau. |
| PUT /api/headless/v1/workspaces/{id}/product-connection | Branche votre serveur MCP sur le panneau intégré d'un espace client que vous avez ouvert. Rejouable. |
| DELETE /api/headless/v1/workspaces/{id}/product-connection | Débranche cette connexion et retire ses outils du panneau, même sur un espace client suspendu. |
| POST /api/headless/v1/workspaces/{id}/product-connection/rotate-secret | Renouvelle le secret de la connexion. Rejouable avec le même identifiant de demande (demande_id). |
| PUT /api/headless/v1/workspaces/{id}/product-connection/tools | Déclare les outils de lecture que le panneau de cet espace client peut appeler sur votre serveur MCP. |
| GET /api/headless/v1/workspaces/{id}/usage | Rend la facture du sous-arbre : jours-sièges, sièges au prorata et consommation ventilés par espace et par payeur. |
| DELETE /api/headless/v1/members | Retire un membre de l'espace de la clé, et de lui seul ; son compte reste. |
| DELETE /api/headless/v1/panel/third-party-turns | Efface les échanges du panneau intégré de votre produit qui portent du contenu tiers, pour une personne, à son départ. |
| DELETE /api/headless/v1/users | Supprime un utilisateur final et purge toute sa mémoire. |
Chaque requête porte un header Authorization: Bearer <clé>. La clé est une clé workspace de scope headless:chat, distincte des clés historiques (notify, MCP) : ces dernières sont inertes sur les endpoints headless, même valides par ailleurs.
Une seule clé active à la fois par couple workspace + scope. Préversion privée : il n'existe pas encore d'écran Console dédié au scope headless:chat. Un admin du workspace la génère en appelant POST /api/workspace/{id}/api-key/rotate avec un corps {"scope": "headless:chat"} (session admin requise, même route que la clé MCP legacy, distinguée par ce paramètre). La réponse contient la clé en clair une seule fois, copiez-la immédiatement.
Un espace client suspendu ou fermé n'a plus de clé qui fonctionne, sur AUCUN endpoint headless : conversation, mémoire, utilisateurs, documents, comme les routes de gouvernance ci-dessous. 403, code workspace_suspended ou workspace_closed. Ce contrôle porte sur l'espace DE LA CLÉ elle-même, indépendamment d'un espace client visé par son contenu : un revendeur suspendu perd donc à la fois le droit d'ouvrir de nouveaux espaces clients et celui de parler au nom des existants.
Crée l'utilisateur final s'il n'existe pas encore pour ce workspace, sinon retourne l'existant (find-or-create idempotent). external_user_id est votre identifiant interne (par exemple l'id utilisateur icon), jamais un email ni une donnée sensible.
curl -X POST https://gilbert.solveholding.com/api/headless/v1/users \
-H "Authorization: Bearer $GILBERT_HEADLESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_user_id": "icon-user-4821",
"display_name": "Camille"
}'{
"external_user_id": "icon-user-4821",
"created": true
}Champs : external_user_id (string, requis, 1 à 256 caractères), display_name (string, optionnel). 400 si external_user_id est vide, manquant ou dépasse 256 caractères.
Il n'est pas nécessaire d'appeler cet endpoint avant /chat : le premier message d'un external_user_id inconnu crée l'utilisateur au passage. Un appel explicite reste utile pour provisionner un compte en avance ou récupérer created: false comme sonde d'existence.
Trois champs optionnels supplémentaires permettent de nommer une vraie personne plutôt qu'un simple identifiant : email (string), email_is_personal (booléen) et workspace_member (booléen). Aucun des trois n'est requis : sans email, rien ne change, le compte reste cloisonné à votre espace, sans aucune appartenance. C'est le chemin par défaut, celui d'un simple visiteur.
curl -X POST https://gilbert.solveholding.com/api/headless/v1/users \
-H "Authorization: Bearer $GILBERT_HEADLESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_user_id": "collab-42",
"display_name": "Alice",
"email": "alice@espace client-martin.example",
"email_is_personal": true,
"workspace_member": true
}'email_is_personal distingue une adresse propre à UNE personne d'une boîte partagée comme accueil@ ou secretariat@. À vrai, Gilbert cherche un compte existant pour cette adresse et le réutilise s'il existe, au lieu de fabriquer un nouveau compte : la même personne retrouve la même mémoire, quel que soit le produit par lequel elle passe. À faux ou absent (le défaut), Gilbert ne recherche jamais de compte existant : une boîte partagée par plusieurs personnes ne doit jamais leur faire porter la même identité.
Le rôle posé est toujours celui d'un membre simple, jamais administrateur : ce champ ne peut pas être choisi par l'appelant. Rappeler cet endpoint pour la même personne ne crée jamais de doublon, appartenance comprise.
Une personne rattachée avec workspace_member: true reçoit un courriel l'en informant, avec un lien pour se retirer de l'espace en un clic. C'est un envoi ponctuel, jamais bloquant pour votre appel : un souci d'envoi ne fait pas échouer le rattachement, la personne reste membre.
Un compte créé avant cette fonctionnalité reçoit sa vraie adresse au premier appel qui la fournit. Si l'identifiant externe correspond déjà à un compte portant encore une adresse fabriquée, Gilbert la remplace par l'adresse fournie, à condition qu'email_is_personal soit vrai. Un compte qui porte déjà une vraie adresse n'est jamais modifié : ce n'est écrasé qu'une adresse fabriquée, jamais une adresse réelle. Si l'adresse fournie appartient déjà à un autre compte Gilbert, l'appel échoue avec 409, code email_already_in_use, sans rien écrire : deux comptes ne peuvent pas porter la même adresse.
Envoie un message au nom d'un utilisateur final et retourne un stream application/x-ndjson, une ligne JSON par event. Le vocabulaire d'events reprend celui de l'API standard (text-delta, tool-call, tool-result, finish) et en ajoute deux, documentés plus bas : structured_output et error.
curl -N -X POST https://gilbert.solveholding.com/api/headless/v1/chat \
-H "Authorization: Bearer $GILBERT_HEADLESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_user_id": "icon-user-4821",
"message": "Quels evenements as-tu retenus de mes preferences ?"
}'{"type":"text-delta","text":"Tu"}
{"type":"text-delta","text":" as"}
{"type":"text-delta","text":" aime"}
{"type":"text-delta","text":" les afterworks tech a Lyon."}
{"type":"finish","finishReason":"stop","usage":{"inputTokens":210,"outputTokens":18}}Champs : external_user_id (string, requis), message (string, requis, 1 à 32 000 caractères), output_schema (objet, optionnel, voir ci-dessous). 400 si message est vide ; 400 ou 413 au-delà de 32 000 caractères.
402, code central_key_not_billable, avant l'ouverture du stream, si la conversation passerait par la clé centrale de Gilbert et que l'espace ne peut plus y être facturé ce mois-ci : budget atteint, ou dépense du mois impossible à chiffrer. Aucun utilisateur final n'est créé et aucun message n'est enregistré. Relevez le budget de l'espace ou configurez sa propre clé. Si la clé de l'espace tombe en panne en cours de réponse et que le repli sur la clé centrale est refusé pour la même raison, le stream est déjà en 200 : il se clôt sur un event error de code workspace_llm_unavailable.
La mémoire de l'utilisateur final est activée par défaut : Gilbert retient les préférences et faits marquants entre deux appels, comme en mode personnel.
Marque blanche. Dans ses réponses, l'assistant se présente sous le nom d'assistant réglé dans les Réglages de l'espace (par défaut « Solve AI »). Un intégrateur comme Meety expose ainsi l'API sous sa propre marque. Ce nom est celui de l'espace de la clé : un espace client qui n'a pas renommé son assistant répond sous « Solve AI », même quand le panneau intégré affiche la marque blanche d'un espace au-dessus de lui.
Fournissez un JSON Schema d'objet (type: object, properties) pour forcer Gilbert à répondre avec des données exploitables directement, sans parsing de prose. C'est le mode conseillé pour recréer un événement externe (Meetup, Eventbrite, etc.) sous forme de champs structurés.
curl -N -X POST https://gilbert.solveholding.com/api/headless/v1/chat \
-H "Authorization: Bearer $GILBERT_HEADLESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_user_id": "icon-user-4821",
"message": "Recree cet evenement : https://www.meetup.com/lyon-tech/events/12345",
"output_schema": {
"type": "object",
"properties": {
"titre": { "type": "string" },
"description": { "type": "string" },
"date": { "type": "string" },
"heure": { "type": "string" },
"en_ligne": { "type": "boolean" },
"adresse": { "type": "string" },
"photo": { "type": "string" }
},
"required": ["titre", "date", "heure", "en_ligne"]
}
}'{"type":"text-delta","text":"..."}
{"type":"tool-call","toolName":"fetch_url","toolCallId":"call_abc","input":{"url":"https://www.meetup.com/lyon-tech/events/12345"}}
{"type":"tool-result","toolCallId":"call_abc","output":"..."}
{"type":"structured_output","data":{
"titre": "Afterwork Tech Lyon",
"description": "Rencontre mensuelle des developpeurs lyonnais.",
"date": "2026-08-14",
"heure": "19:00",
"en_ligne": false,
"adresse": "12 rue de la Republique, 69002 Lyon",
"photo": "https://meetup.com/photos/xyz.jpg"
}}
{"type":"finish","finishReason":"stop","usage":{"inputTokens":640,"outputTokens":112}}Le stream se termine par un event {"type":"structured_output","data":{...}} avant le finish.
Règles du output_schema. Mots-clés pris en compte : type, properties, required, items et enum. Les autres mots-clés sont acceptés et ignorés. La racine doit être un schéma d'objet (type: object) ; son properties, s'il est présent, doit être un objet, et son required, s'il est présent, une liste de chaînes. Le schéma pèse 16 000 caractères au plus et descend sur 16 niveaux au plus. Un nœud dont la forme ne peut pas être lue par le validateur (une valeur null dans un properties, un enum renseigné qui n'est pas une liste, ou, à côté d'un properties, un required renseigné qui n'est ni une liste ni une chaîne) donne un 400 invalid_output_schema immédiat, avant tout appel modèle. La liste required est vérifiée à chaque niveau atteint par properties ou items, même quand ce niveau ne déclare aucun properties, et seules comptent les clés réellement présentes dans l'objet. Les routes /documents/read et /text/read appliquent les mêmes règles.
Si la sortie du modèle ne respecte pas le schéma, Gilbert retente une fois avec l'erreur de validation renvoyée au modèle ; en cas de deuxième échec, un event error de code structured_output_invalid clôt le stream (voir la section erreurs ci-dessous).
Liste les souvenirs persistants d'un utilisateur final, les plus récents en premier, jusqu'à 100 entrées. Le contenu est déchiffré côté serveur avant retour : aucune clé de chiffrement ne transite côté client.
curl https://gilbert.solveholding.com/api/headless/v1/memory?external_user_id=icon-user-4821 \
-H "Authorization: Bearer $GILBERT_HEADLESS_KEY"{
"memories": [
{
"id": "6f1e...c92a",
"kind": "preference",
"content": "Aime les afterworks tech a Lyon.",
"created_at": "2026-07-10T18:22:00Z"
}
]
}404, code end_user_not_found, si external_user_id n'existe pas pour ce workspace. La lecture est strictement scopée à cet utilisateur : aucune fuite cross-utilisateur possible, même en cas d'erreur côté appelant.
Cet endpoint sert aussi d'export : il n'existe pas d'endpoint d'export dédié en v1, GET memory couvre ce besoin.
Lit UN document et rend ce que le modèle en extrait. Appel synchrone : la réponse arrive dans le même appel, sans rappel ni file d'attente côté Gilbert. Un relevé bancaire prend cinq à trente secondes, le plafond de la route est de trois cents.
tier est optionnel et vaut smart par défaut ; provider aussi, voir plus bas ce qui se passe sans lui.
curl -X POST https://gilbert.solveholding.com/api/headless/v1/documents/read \
-H "Authorization: Bearer $GILBERT_HEADLESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"document_base64": "'"$(base64 -i releve.pdf)"'",
"media_type": "application/pdf",
"filename": "releve-2026-08.pdf",
"prompt": "Relève la date d’arrêté, le solde final et les mouvements.",
"provider": "anthropic",
"tier": "smart",
"output_schema": {
"type": "object",
"required": ["date_arrete", "solde"],
"properties": {
"date_arrete": { "type": "string" },
"solde": { "type": "number" }
}
}
}'{
"text": "{\"date_arrete\": \"2026-08-31\", \"solde\": 12840.55}",
"data": { "date_arrete": "2026-08-31", "solde": 12840.55 },
"provider": "anthropic",
"tier": "smart",
"provider_used": "anthropic",
"tier_used": "smart",
"fallback": null,
"model_id": "claude-sonnet-5",
"tokens_in": 4821,
"tokens_out": 96
}provider est facultatif. Absent, Gilbert suit le réglage de la famille lecture_document de cet espace client (voir plus bas), et à défaut le fournisseur par défaut de son espace. Fourni, il l'emporte pour cet appel, et aucun autre fournisseur ne lit le document à sa place : sans clé de l'espace pour ce fournisseur, la lecture est refusée en 503 workspace_llm_unavailable. Seul anthropic nommé peut être servi par la clé centrale de Gilbert, qui est une clé Anthropic.
Enregistrez ce qui a effectivement servi. La réponse porte provider_used et tier_used, et fallback vaut null sauf si, sans provider envoyé, le fournisseur réglé pour la lecture n'avait pas de clé dans cet espace client : il porte alors requested_provider et reason. Il ne compare qu'au réglage de la lecture : sans réglage, ou quand il nomme le fournisseur par défaut de l'espace, et que ce fournisseur sans clé est remplacé par la clé centrale de Gilbert, une clé Anthropic, fallback reste null et provider_used le dit. Un repli change de prestataire, donc de prix : un écran de dépenses qui afficherait le fournisseur demandé mentirait sans que personne ne le voie.
Quand le fournisseur répond par une erreur HTTP, la réponse porte provider_error : status, body tronqué à 2 000 caractères, body_truncated, et reasons, les motifs machine extraits du corps entier avant troncature. Classez sur reasons : une absence de motif dans un corps coupé ne prouve rien, et c'est ce que body_truncated vous dit. Nos codes : provider_rejected_key (la clé est refusée, 424), provider_rate_limited (débit, 429), provider_quota_exhausted (crédits épuisés, 402), llm_failed (le reste, 502). Un échec sans réponse d'erreur du fournisseur (connexion interrompue, réponse illisible, incident local, appel annulé) revient en llm_failed (502) sans provider_error, quelle que soit la clé. Quand la lecture s'est faite sur la clé centrale de Gilbert, la réponse ne porte aucun provider_error, et une clé refusée ou des crédits épuisés sur cette clé reviennent en llm_failed (502) : l'espace client n'a rien à y corriger. Sur toute clé, un débit dépassé chez le fournisseur revient en provider_rate_limited (429), même après les reprises automatiques : relancez plus tard, en sachant qu'un quota journalier attend sa remise à zéro. Un exemple mesuré qui explique pourquoi nous ne classons pas sur le seul statut : Google rend 400 sur une clé invalide, avec le sens dans error.details[].reason.
Quand output_schema est fourni, la sortie du modèle est parsée et validée contre lui : data porte alors la valeur validée, et une sortie non conforme rend 422, code structured_output_invalid, plutôt qu'un texte que vous devriez deviner. Le schéma est transmis au modèle avec la consigne. La réponse du modèle est bornée à 16 000 jetons : une réponse coupée, filtrée ou interrompue revient en 502, code output_incomplete. La même requête risque de s'arrêter au même endroit et d'être facturée de nouveau : demandez d'abord une réponse plus courte ou moins de champs dans output_schema.
Les refus sont nommés : 415 type non lisible (cette route n'accepte qu'une liste BLANCHE réduite à application/pdf, contrairement à POST /documents ci-dessous, qui refuse une liste NOIRE et accepte tout le reste, sous le même code unsupported_media_type), 413 document trop gros, 422 incapacité mesurée du couple choisi, 502 échec du fournisseur (llm_failed) ou réponse inachevée du modèle (output_incomplete, avec ou sans output_schema). Un nouvel appel peut entraîner une nouvelle consommation : cette route ne garantit pas l'idempotence. Un 503 se lit sur son code : avec workspace_llm_unavailable, l'espace n'a pas de clé utilisable pour le fournisseur qui doit lire et relancer n'y changera rien : configurez-la, ou, sans provider nommé ou avec anthropic, rouvrez le repli central de l'espace. Avec internal_error ou rate_limit_unavailable, c'est passager : relancez dans quelques secondes ; avec central_key_usage_unavailable, relancez après Retry-After.
429, code rate_limited, au-delà de 120 lectures par heure et par clé. La réponse porte un en-tête Retry-After en secondes : respectez-le plutôt que de réessayer immédiatement.
La limite applicative est de 32 Mio de PDF décodé, mais elle n'est pas atteignable sur cette route en production. Un document d'environ 3,3 Mo est déjà proche de la limite de transport ; la taille du prompt et du schéma de sortie réduit encore cette marge. Le document voyage encodé en base64 dans le corps JSON, ce qui gonfle sa taille d'environ un tiers, et la plateforme d'hébergement coupe tout corps de requête au-delà de 4,5 Mo, selon les limites Vercel. Un corps qui dépasse cette taille n'atteint donc jamais le code 413 de Gilbert : il est coupé en amont, et le refus que vous recevez alors ne porte pas l'enveloppe JSON d'erreur de Gilbert décrite ici.
Cette lecture porte sur le document entier envoyé dans l'appel, sans plage de pages : un appel qui échoue est à rejouer depuis le début, sans état conservé entre deux appels. Un document déjà déposé par POST /documents/uploads puis POST /documents/register ne peut pas être relu par cette route : cette voie alimente l'indexation documentaire de l'espace, et il n'existe aucune lecture par identifiant.
402, code central_key_not_billable, si le budget IA mensuel de l'espace sur la clé centrale de Gilbert est atteint, ou si la dépense du mois sur cette clé ne peut pas être chiffrée. Un espace sans clé fournisseur à lui lit sur la clé centrale quand son repli est ouvert, dans la limite de ce budget, et la lecture est facturée au coût majoré de 20 %, comme une conversation. Budget atteint : relevez-le ou configurez la clé de l'espace. Dépense impossible à chiffrer : configurez la clé de l'espace ou contactez-nous. Quand la clé centrale paie, la lecture se fait au palier smart au plus : un premium demandé est servi en smart, et tier_used le dit. Sur la clé de l'espace, ce plafond ne s'applique pas.
503, code central_key_usage_unavailable, avec Retry-After : Gilbert n'a pas pu lire la consommation du mois de l'espace sur la clé centrale. C'est passager, relancez après le délai indiqué. Le 402, lui, ne se résout pas en relançant.
Lit UN texte que votre produit a déjà extrait (courrier, pièce, message) et rend ce que le modèle en tire. C'est la jumelle de /documents/read pour l'appelant qui détient déjà le texte : aucun fichier à encoder, aucun type de document à déclarer. L'appel est synchrone, la réponse arrive dans le même appel, et le plafond de la route est de trois cents secondes.
curl -X POST https://gilbert.solveholding.com/api/headless/v1/text/read \
-H "Authorization: Bearer $GILBERT_HEADLESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Avenant n° 3 au bail commercial. Prise d’effet le 1er novembre 2026…",
"prompt": "Relève la date d’effet et le nouveau loyer annuel.",
"tier": "smart",
"output_schema": {
"type": "object",
"required": ["date_effet", "loyer_annuel"],
"properties": {
"date_effet": { "type": "string" },
"loyer_annuel": { "type": "number" }
}
}
}'{
"text": "{\"date_effet\": \"2026-11-01\", \"loyer_annuel\": 18600}",
"data": { "date_effet": "2026-11-01", "loyer_annuel": 18600 },
"provider": "anthropic",
"tier": "smart",
"provider_used": "anthropic",
"tier_used": "smart",
"fallback": null,
"key_source": "workspace",
"model_id": "claude-sonnet-5",
"tokens_in": 1210,
"tokens_out": 38
}text et prompt sont requis : 300 000 caractères au plus pour le texte, 8 000 pour la consigne. La réponse du modèle est bornée à 16 000 jetons : une réponse qui atteint cette borne revient en output_incomplete. provider est facultatif : absent, Gilbert applique le réglage de la famille lecture_document de l'espace client, et à défaut son fournisseur par défaut. tier vaut smart par défaut ; quand provider est absent et que l'espace a un réglage pour la lecture, le palier de ce réglage s'applique et le tier envoyé est ignoré, donc lisez tier_used. La réponse porte les mêmes champs que celle de /documents/read, plus key_source : enregistrez provider_used et tier_used, et lisez fallback pour savoir si le fournisseur réglé a été remplacé. Un provider envoyé n'est jamais remplacé : sans clé de l'espace pour lui, la lecture est refusée en 503 workspace_llm_unavailable, et seul anthropic nommé peut être servi par la clé centrale de Gilbert. Sur cette route, fallback n'est renseigné que si le réglage de la lecture nommait un autre fournisseur que celui qui a lu ; un service par la clé centrale du même fournisseur se lit dans key_source. Sans réglage de la lecture, ou quand il nomme le fournisseur par défaut de l'espace, et que ce fournisseur sans clé est remplacé par la clé centrale de Gilbert, une clé Anthropic, fallback reste null : provider_used et key_source le disent. key_source dit quelle clé a payé la lecture : workspace pour la clé de l'espace client, central pour la clé de Gilbert, facturée sous le plafond mensuel de l'espace. Une lecture peut donc avoir le même fournisseur que celui demandé, avec fallback à null et key_source à central.
Le texte envoyé est transmis au modèle comme une donnée à analyser, séparée de votre prompt. output_schema vérifie la forme de la sortie ; ses valeurs restent tirées d'un texte tiers, et se traitent comme telles. De l'appel, Gilbert mesure la consommation, et ne garde ni conversation ni mémoire.
Les refus sont nommés : 400 pour un corps invalide (missing_text, empty_prompt, prompt_too_long, invalid_provider, invalid_tier, invalid_output_schema) ; missing_text couvre aussi un corps qui n'est pas un objet JSON valide. 413 pour un texte ou un corps de requête trop long (text_too_large), 422 pour une sortie non conforme à output_schema (structured_output_invalid). Une erreur HTTP du fournisseur porte provider_error et les mêmes codes que sur /documents/read : provider_rejected_key (424), provider_rate_limited (429), provider_quota_exhausted (402), llm_failed (502) ; un échec sans réponse d'erreur du fournisseur revient en llm_failed (502) sans provider_error, comme sur /documents/read. output_incomplete (502) : le modèle n'a pas terminé normalement (sortie coupée, filtrée ou interrompue) ; relancez ou raccourcissez le texte. Quand la lecture s'est faite sur la clé centrale de Gilbert, la réponse ne porte aucun provider_error, et une clé refusée ou des crédits épuisés sur cette clé reviennent en llm_failed (502), car cette clé est celle de Gilbert : l'espace client n'a rien à y corriger. Un nouvel appel peut entraîner une nouvelle consommation : cette route ne garantit pas l'idempotence. Un incident de Gilbert rend 500 avec internal_error, dans le même corps d'erreur.
429, code rate_limited, au-delà de 240 lectures par heure et par clé, avec un en-tête Retry-After en secondes. Toute requête authentifiée compte, y compris celles refusées pour un corps invalide. Ce compteur est distinct de celui de /documents/read : une rafale de textes ne ferme pas la porte aux lectures de documents. 503 avec rate_limit_unavailable est passager, relancez dans quelques secondes ; avec workspace_llm_unavailable, l'espace n'a pas de clé utilisable pour le fournisseur qui doit lire : configurez-la, ou, sans provider nommé ou avec anthropic, rouvrez le repli central de l'espace.
402, code central_key_not_billable, si le budget IA mensuel de l'espace sur la clé centrale de Gilbert est atteint, ou si la dépense du mois sur cette clé ne peut pas être chiffrée. Comme pour les documents, un espace sans clé fournisseur à lui lit sur la clé centrale quand son repli est ouvert, dans la limite de ce budget, et la lecture est facturée au coût majoré de 20 %, comme une conversation. Budget atteint : relevez-le ou configurez la clé de l'espace. Dépense impossible à chiffrer : configurez la clé de l'espace ou contactez-nous. Quand la clé centrale paie, la lecture se fait au palier smart au plus : un premium demandé est servi en smart, et tier_used le dit. Sur la clé de l'espace, ce plafond ne s'applique pas.
503, code central_key_usage_unavailable, avec Retry-After : la consommation du mois de l'espace sur la clé centrale n'a pas pu être lue. Relancez après le délai indiqué.
Dépose UN document dans l'espace de la clé appelante, pour l'indexation documentaire (recherche RAG) de cet espace client. Contrairement à /documents/read, le corps est multipart/form-data, pas du JSON : vous envoyez un fichier, pas un texte encodé.
curl -X POST https://gilbert.solveholding.com/api/headless/v1/documents \
-H "Authorization: Bearer $GILBERT_HEADLESS_KEY" \
-F "file=@reglement-copropriete.pdf;type=application/pdf" \
-F "source=drive" \
-F "external_id=drive-file-88f2" \
-F "extracted_text=<reglement-copropriete.txt"{
"document_id": "9c1e...4a7b",
"extraction_status": "extracted",
"duplicate": false
}Cette route rend toujours 200, création comprise, contrairement à POST /workspaces ci-dessous qui rend 201 à la création. duplicate: false est le SEUL signal qu'un nouveau document a été créé.
Champs du formulaire : file (requis, voir le seuil de choix ci-dessous), source (requis, une des six valeurs upload, email_attachment, email_message, drive, github, manual), visibility (optionnel, workspace par défaut, ou user), owner_user_id (requis si visibility="user", refusé sinon avec 400 owner_user_id_not_allowed), external_id (optionnel, rangé tel quel après troncature à 500 caractères, jamais utilisé pour la déduplication), extracted_text (optionnel, 1 000 000 caractères au plus).
Deux voies de dépôt, et le choix vous appartient avant d'appeler. Un corps de requête au-delà de 4,5 Mo est coupé par la plateforme d'hébergement avant que Gilbert ne soit appelé : ce que vous recevez alors est un refus de la plateforme, avec un corps qui n'est pas l'enveloppe JSON d'erreur de Gilbert, donc aucun des codes décrits ici. Il n'existe aucun repli automatique côté serveur. La règle porte sur la SOMME, parce que extracted_text voyage dans le même corps que le fichier :
taille(fichier) + taille_utf8(extracted_text) + ~64 Kio d'enveloppe
< 4 Mio -> un seul appel : POST /api/headless/v1/documents
sinon -> deux temps : POST /api/headless/v1/documents/uploads
puis POST /api/headless/v1/documents/registerLe seuil de 4 Mio vaut 4,19 Mo : la marge sous la coupure à 4,5 Mo est volontaire, elle couvre l'enveloppe multipart, les entêtes et l'arrondi de la plateforme. Les deux chiffres ne se contredisent donc pas.
La voie en deux temps accepte jusqu'à 67 108 864 octets (64 Mio) par un plafond qui lui est propre. Elle est décrite juste après cette section.
source="email_message" exige les DEUX champs, visibility="user" ET owner_user_id : un mail entre toujours en portée personnelle, jamais en portée d'espace, mais poser owner_user_id seul ne suffit pas à le faire, il n'existe aucune promotion implicite de visibility. Omettre visibility="user" sur un email_message rend 400, code email_message_requires_user_visibility.
owner_user_id désigne un utilisateur interne à Gilbert (une UUID), distinct de external_user_id utilisé partout ailleurs dans cette API. En préversion, aucun endpoint documenté ici ne le retourne : pour déposer un document en portée user, cet identifiant doit vous parvenir par un autre canal pour l'instant. Un identifiant de bonne forme mais qui ne correspond à AUCUN compte (périmé, ou simplement faux) rend 400, code owner_user_id_unknown : c'est un défaut permanent de la requête, pas une panne, réessayer avec la même valeur échouera toujours de la même façon.
Fournir extracted_text évite un coût réel. Quand vous fournissez le texte déjà extrait, Gilbert ne télécharge ni n'analyse le fichier : aucune reconnaissance de caractères facturée, aucun appel d'extraction. Le fichier est néanmoins déposé dans le stockage dans tous les cas, texte fourni ou non : c'est la source qui permettra une réindexation future, et la base de la déduplication par contenu. Fournir extracted_text évite l'extraction, pas la vectorisation : celle-ci reste facturée sur la clé d'embedding de l'espace, qu'elle ait lieu tout de suite ou plus tard.
Le dépôt est acquis, y compris en cas de doublon. Un contenu déjà déposé dans le même espace, avec le même régime de visibilité, rend le MÊME document_id, avec duplicate: true, en 200, presque toujours sans erreur (exception ci-dessous). Un chargement complet peut donc être rejoué sans précaution ni journal de progression : redéposer deux fois la même facture ne coûte rien de plus qu'un aller-retour réseau.
La clé de déduplication dépend de la portée. En portée workspace, elle porte sur le couple contenu et régime de visibilité. En portée user, elle porte en plus sur owner_user_id : le même fichier déposé pour deux propriétaires différents donne DEUX documents, pas un. Et un même fichier déposé une fois en visibility="workspace" puis une fois en visibility="user" produit toujours deux documents distincts.
Exception à « jamais une erreur » : une course entre deux dépôts concurrents strictement identiques, ou une panne de lecture pendant la résolution du doublon, peut remonter 502, code document_deposit_failed. Rejouez l'appel, la déduplication rend ce rejeu sans risque.
{
"document_id": "9c1e...4a7b",
"extraction_status": "extracted",
"duplicate": true
}L'espace vient toujours de la clé. Un champ workspace_id posé dans ce formulaire n'est ni lu ni honoré : c'est l'espace de la clé Authorization qui reçoit systématiquement le document.
402, code embedding_central_key_not_allowed, si l'espace n'a ni clé de fournisseur d'embedding à lui ni le droit d'utiliser la clé centrale de Solve. La résolution ne cherche la clé que du SEUL fournisseur configuré pour l'indexation (mistral par défaut, réglable côté Gilbert) ; le message d'erreur nomme ce fournisseur explicitement, pour que vous sachiez laquelle configurer sans deviner. La clé de l'espace est toujours essayée en premier ; à défaut, le repli sur la clé de Solve suit le réglage allow_central_fallback de l'espace, autorisé par défaut. Un espace ouvert par un revendeur n'utilise la clé de Solve que si le payeur effectif de son arbre a ouvert son repli avec un budget mensuel (PATCH /workspaces/{id}/central-fallback) ; sans cela, il lui faut sa propre clé. C'est une RÈGLE, pas une panne : contrairement au 503 du limiteur de débit ci-dessous, qui se réessaie, ce refus ne se répare pas en relançant l'appel, il se répare en configurant l'espace (clé propre pour ce fournisseur, ou en autorisant le repli sur la clé de Solve dans ses réglages) et se remonte à un humain plutôt que réessayé en boucle. Rien n'est écrit ni stocké dans le seau ou en base avant ce contrôle, mais le refus consomme quand même votre quota horaire de dépôts : la vérification de débit intervient AVANT toute validation de ce point d'entrée, y compris celle-ci. Arrêtez un chargement en lot au premier refus, plutôt que de parcourir tout le corpus en boucle. Le même appel repasse sans changement une fois l'espace configuré.
Un document déposé est traité en arrière-plan, jamais pendant cette requête. extraction_status porte QUATRE valeurs, pas deux : pending (rien n'a encore été découpé), extracted (des fragments existent, texte fourni au dépôt ou déjà ingéré), unsupported (le format du fichier n'a pas pu être traité) et failed (l'extraction a échoué). Un dépôt tout juste créé (duplicate: false) ne rend jamais que pending ou extracted : les deux autres ne sortent que sur un dépôt duplicate: true, elles décrivent l'état d'un document déjà passé par l'ingestion en arrière-plan. Ne les traitez pas comme un signal à réessayer sans rien changer : redéposer le même fichier sans texte retombe sur le même document, inchangé. Si vous disposez du texte déjà extrait, redéposez-le avec extracted_text : un document sans aucun fragment reçoit alors les fragments de ce texte et repasse à extracted, même sur un doublon. Sinon, remontez le document à un humain plutôt que de réessayer en boucle.
Quand la lecture des pages scannées passe par la clé de Solve, elle est limitée à 2 000 pages par mois civil (UTC), partagées par tout l'espace pour ce qu'aucun membre n'a déposé lui-même : les documents déposés par cette API comme les pièces jointes des courriels reçus. Au-delà, un document sans aucun texte (une image, un PDF sans couche texte) rend failed, que rien ne distingue, par cette API, d'un autre échec, et sa lecture est reprise automatiquement une fois le mois changé, sans nouveau dépôt de votre part. Dans le même cas, un PDF qui porte du texte, même sur une partie de ses pages seulement, est indexé sur ce seul texte et rend extracted : ses pages scannées ne seront pas lues plus tard. Fournir extracted_text évite entièrement la lecture des pages scannées, donc cette limite.
Les fragments écrits au dépôt n'ont pas encore de vecteur ; c'est un balayage périodique séparé qui les vectorise ensuite. Un dépôt isolé sur une file vide devient cherchable de l'ordre de quelques minutes plus tard ; un chargement initial de plusieurs centaines de documents (l'ouverture d'un nouvel espace client, typiquement) s'étale sur environ une journée, ce balayage ne traitant qu'un document à la fois. Ce sont des ordres de grandeur observés sur le fonctionnement actuel, pas une garantie de délai. Il n'existe pas, en préversion, d'appel pour interroger l'état d'un document déjà déposé : le suivi d'un dépôt individuel n'est pas encore exposé par cette API.
429, code rate_limited, au-delà de 600 dépôts par heure et par clé (un plafond pensé pour un chargement initial de plusieurs centaines de documents, pas pour freiner un usage normal). 503, code rate_limit_unavailable, si le limiteur lui-même est indisponible : les dépôts sont alors mis en pause plutôt que de passer sans compteur, réessayez dans quelques secondes. Les deux réponses portent un en-tête Retry-After en secondes, pas seulement le 429.
Les autres refus : 400 pour un formulaire mal formé (invalid_form_data), un fichier absent (missing_file), un fichier présent mais VIDE, zéro octet (document_empty, distinct de missing_file), une source absente ou hors de la liste fermée (missing_source, invalid_source), une visibility invalide (invalid_visibility), un owner_user_id qui n'est pas une UUID (invalid_owner_user_id), une incohérence entre source, visibility et owner_user_id (email_message_requires_user_visibility, owner_user_id_required, owner_user_id_not_allowed, document_deposit_constraint_violated), un owner_user_id de bonne forme mais inconnu (owner_user_id_unknown, voir plus haut), ou un extracted_text trop long (provided_text_too_large). 413, code document_too_large, au-delà de la borne de cette voie pour le fichier ; la même limite se déclenche aussi, avant même la lecture du corps, si l'en-tête Content-Length annonce un corps trop large pour l'enveloppe entière. En pratique, un corps qui dépasse 4,5 Mo n'atteint jamais ce code : il est coupé par la plateforme d'hébergement en amont, et le refus que vous recevez alors ne porte pas l'enveloppe JSON d'erreur de Gilbert. Au-dessus de ce seuil, employez la voie en deux temps. 415, code unsupported_media_type, pour les quelques types exécutables ou binaires système explicitement refusés (liste NOIRE : tout le reste passe) : déclarez le vrai type du fichier (type=application/pdf, par exemple) plutôt que de laisser un client HTTP deviner. Ce même code porte une politique OPPOSÉE sur /documents/read ci-dessus, qui n'accepte qu'une liste BLANCHE réduite à application/pdf : lisez la section de la route appelée, pas seulement le code. 502, code document_deposit_failed, pour un échec de stockage ou d'écriture côté Gilbert : rejouez l'appel, la déduplication rend ce rejeu sans risque. 500, code internal_error, pour le reste.
La voie en deux temps, pour tout document qui dépasse le seuil de la section précédente. Les octets ne traversent jamais Gilbert : ils vont directement au stockage par une URL signée, ce qui lève le plafond de corps de la plateforme. Cette voie accepte jusqu'à 67 108 864 octets (64 Mio).
Temps 1, demander un billet. Un corps JSON qui décrit le document, sans un seul octet de son contenu. Le billet est le champ ticket de la réponse : une chaîne opaque, signée par Gilbert, que vous rendez tel quel au temps 2.
curl -X POST https://gilbert.solveholding.com/api/headless/v1/documents/uploads \
-H "Authorization: Bearer $GILBERT_HEADLESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"filename": "plan-de-masse.pdf",
"media_type": "application/pdf",
"byte_size": 49392114,
"source": "drive",
"external_id": "drive-file-91c4",
"sha256": "9f2c...b71e"
}'{
"duplicate": false,
"upload_url": "https://...supabase.co/storage/v1/object/upload/sign/...",
"upload_token": "eyJhbGciOi...",
"storage_path": "04ca...c85/staged/7f31...9ab.pdf",
"ticket": "eyJ2IjoxLC...",
"ticket_expires_at": "2026-09-21T03:41:12.000Z",
"document_id": null,
"extraction_status": null
}Champs du corps : filename (requis, caractères de contrôle retirés, tronqué à 255), media_type (requis sur cette voie, le fichier n'étant pas là pour le déclarer, type MIME hors liste noire), byte_size (requis, entier strictement positif, 67 108 864 au plus), source (requis, les mêmes six valeurs qu'à la section précédente), visibility (optionnel, workspace par défaut, ou user), owner_user_id (mêmes règles qu'à la section précédente), external_id (optionnel, tronqué à 500 caractères), sha256 (optionnel, 64 caractères hexadécimaux).
Ce temps valide tout ce qui se valide sans les octets : la forme, le type déclaré, la cohérence entre source, visibility et owner_user_id, l'existence du compte désigné, et le consentement d'indexation de l'espace. Le seul refus que vous pouvez encore éviter au temps 2 en corrigeant votre requête est « les octets ne correspondent pas ». Trois autres refus y restent possibles sans que votre requête soit en cause : le consentement d'indexation retiré entre les deux appels, le compte désigné disparu entre les deux appels, et les pannes.
sha256 est facultatif et n'est jamais la valeur enregistrée : Gilbert recalcule l'empreinte sur les octets réels. Il sert à deux choses. S'il désigne un document déjà présent dans l'espace, la réponse est duplicate: true sans jeton d'envoi (upload_token), ce qui vous épargne un transfert de plusieurs dizaines de mégaoctets. Sinon il est figé dans le billet et confronté aux octets réels.
Le billet vit 10 minutes (ticket_expires_at). Le jeton d'envoi du stockage vit plus longtemps, et c'est donc le billet qui borne la fenêtre utile : passé cette date, redemandez-en un.
Entre les deux, vous envoyez vos octets. Avec un client supabase-js, uploadToSignedUrl(storage_path, upload_token, corps) ; sinon un PUT sur upload_url. Posez le bon Content-Type : le stockage le conserve, et l'extraction s'en sert pour choisir son analyseur.
Temps 2, enregistrer. Le corps ne porte que le billet, et éventuellement extracted_text. Rien d'autre : tous les champs métier sont dans le billet, signés au temps 1, donc aucun ne peut avoir changé entre les deux appels.
curl -X POST https://gilbert.solveholding.com/api/headless/v1/documents/register \
-H "Authorization: Bearer $GILBERT_HEADLESS_KEY" \
-H "Content-Type: application/json" \
-d '{ "ticket": "eyJ2IjoxLC..." }'La réponse a exactement la forme de celle de la voie en un appel : document_id, extraction_status, duplicate. Rejouer le même billet après un enregistrement NEUF rend duplicate: true et le même document_id.
Une exception à connaître avant de programmer un réessai. Quand la réponse est duplicate: true parce que le CONTENU était déjà présent dans votre espace sous un autre document, le fichier que vous venez de déposer est supprimé : il ne sert plus à rien. Rejouer ce billet rend alors 409 upload_not_found, ce qui invite à renvoyer le fichier alors que le document est bel et bien enregistré. Annoncer sha256 au temps 1 évite entièrement ce détour : le doublon est reconnu avant tout transfert.
Ce qui fait supprimer l'objet que vous avez déposé : une taille, un type réel ou une empreinte qui ne correspondent pas à ce que vous aviez annoncé, un contenu déjà présent dans votre espace sous un AUTRE document, et les deux refus définitifs de l'écriture (owner_user_id_unknown, document_deposit_constraint_violated). Dans ces cas l'objet déposé ne sert plus à rien, et votre billet n'est plus rejouable : reprenez au temps 1.
Ce qui le laisse en place : un refus de consentement d'indexation, et toute panne de lecture de notre côté (transfert coupé, transfert incomplet, délai dépassé), qui rendent 502. Ce ne sont pas des défauts de vos octets. Le même billet peut resservir dans sa fenêtre, sans retransférer le fichier.
Codes propres à cette voie. 502 upload_ticket_failed : le stockage a refusé d'émettre l'URL d'envoi, réessayez. 400 invalid_upload_ticket : billet absent, mal formé, mal signé, ou émis pour un autre espace. 410 upload_ticket_expired : billet périmé, redemandez-en un. 409 upload_not_found : aucun objet au chemin du billet, l'envoi n'a pas abouti. 409 upload_size_mismatch et 409 upload_hash_mismatch : les octets déposés ne sont pas ceux annoncés. 400 missing_media_type : le champ media_type est requis sur cette voie, le fichier n'étant pas là pour le déclarer. Les autres codes de la section précédente s'appliquent à l'identique, avec une précision sur un seul : provided_text_too_large rend 400 quand c'est le champ extracted_text qui dépasse sa borne, et 413 quand c'est le corps entier qui la dépasse, avant même d'être lu.
Débit : cette voie partage le compteur de la voie en un appel, soit 600 dépôts par heure et par clé, les deux voies confondues. Il se consomme au temps 1 ; le temps 2 ne coûte aucun jeton de débit, donc un temps 2 refusé pour octets discordants ne vous coûte rien. Alterner les deux voies ne double pas votre budget.
Ouvre l'espace d'un client à la demande, sans geste humain côté Gilbert : c'est la route qu'un revendeur appelle quand un espace client est créé chez lui. Rejouable par le couple (votre clé, external_ref) : rappeler avec le même external_ref rend le même espace client, en 200 plutôt qu'en 201, sans réémettre de clé. Une exception, définitive : un espace client qui a quitté votre arbre (voir « Départ d'un espace client » plus bas) répond 404 workspace_not_found à tout rappel avec son external_ref, même revenu sous vous. Pour ouvrir un nouvel espace à ce client, appelez cette route avec un nouvel external_ref.
curl -X POST https://gilbert.solveholding.com/api/headless/v1/workspaces \
-H "Authorization: Bearer $GILBERT_PROVISION_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_ref": "3f2a...",
"name": "NAZ & Associés",
"llm_keys": [
{ "provider": "anthropic", "api_key": "sk-...", "label": "Anthropic prod" },
{ "provider": "mistral", "api_key": "...", "label": "Mistral (embeddings)" }
]
}'{
"workspace_id": "8f0c...e21b",
"slug": "naz",
"created": true,
"api_key": "glb_hl_secret"
}api_key n'est rendue qu'à la création, ou si aucune clé headless:chat active ne reste pour cet espace client : un rejeu (même external_ref) en réémet alors une neuve. Tant qu'une clé active existe, ce même rejeu répond api_key: null : la réémettre couperait l'intégration déjà en place chez ce client, un simple retry réseau ne doit jamais produire cet effet. Pour récupérer une clé perdue sans attendre qu'elle disparaisse de votre côté, utilisez chat-key/rotate ci-dessous plutôt que de rappeler cette route.
llm_keys est optionnel : sans clé fournisseur propre, l'espace client démarre sur le comportement par défaut de votre espace (BYOLLM hérité ou repli central, selon vos réglages). Authentification : la capacité headless:provision, jamais headless:chat : une clé ouvre des espaces, ou elle parle au nom d'un espace, jamais les deux.
central_fallback ({ "allow", "cap_eur" }, optionnel) ouvre le repli sur la clé centrale dès l'ouverture de l'espace, avec les mêmes règles que PATCH /workspaces/{id}/central-fallback. Seul le payeur effectif de l'espace sous lequel il s'ouvre peut le faire, et l'appelant doit être ce payeur : un revendeur dont un ancêtre paie à sa place, ou qui ouvre l'espace sous une agence qui se paie elle-même, reçoit 403 central_fallback_not_payer, avant toute création. Il ouvre alors l'espace sans central_fallback, et ce payeur fixe le budget ensuite.
Une clé de conversation ne fait pas d'embeddings. Anthropic (l'exemple ci-dessus pour la conversation) ne fait pas d'embeddings : si cet espace client doit déposer des documents (POST /documents plus haut), il lui faut EN PLUS une clé pour le fournisseur d'embeddings configuré côté Gilbert (mistral par défaut), sous peine de 402 embedding_central_key_not_allowed au premier dépôt.
Liste tout le sous-arbre (pas seulement les enfants directs) de l'espace de votre clé, paginé par curseur opaque : ?cursor=...&limit=100&status=suspended&parent_id=.... 100 par défaut, 500 au plus.
curl "https://gilbert.solveholding.com/api/headless/v1/workspaces?status=suspended&limit=100" \
-H "Authorization: Bearer $GILBERT_PROVISION_KEY"{
"items": [
{
"id": "8f0c...e21b",
"name": "NAZ & Associés",
"slug": "naz",
"external_ref": "3f2a...",
"status": "suspended",
"effective_status": "suspended",
"parent_workspace_id": null,
"created_at": "2026-01-01T00:00:00.000Z"
}
],
"next_cursor": null
}Chaque élément porte seulement identifiant, nom, slug, référence externe, statut propre et statut effectif, parent, date de création : jamais de contenu, jamais de membres. Gouverner n'est pas appartenir.
Suspend ou réactive un espace client de votre sous-arbre. Seules "suspended" et "active" sont acceptées ; "closed" répond 400, code invalid_status : la fermeture se fait uniquement par POST .../close, elle est définitive et révoque des clés, ce qu'un simple changement de statut ne doit jamais faire par accident.
curl -X PATCH https://gilbert.solveholding.com/api/headless/v1/workspaces/$WORKSPACE_ID \
-H "Authorization: Bearer $GILBERT_PROVISION_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "suspended" }'{
"id": "8f0c...e21b",
"name": "NAZ & Associés",
"slug": "naz",
"external_ref": "3f2a...",
"status": "suspended",
"effective_status": "suspended",
"parent_workspace_id": null,
"created_at": "2026-01-01T00:00:00.000Z"
}Un espace client hors de votre sous-arbre répond 404, jamais 403. Suspendre un espace client coupe aussitôt tout ce qui parle à un modèle en son nom (chat, routines, brouillons) et toute clé qui lui appartient ; réactiver rouvre tout, y compris ses propres descendants.
Un espace qui appartient à une famille (parent, enfants) peut porter un statut posé par un autre espace de cette famille, ou par la plateforme elle-même. Durcir ce statut (passer de "suspended" à "closed") reste toujours permis. L'assouplir (repasser à "active", ou de "closed" à "suspended") n'est permis que si tous les changements de statut depuis son dernier passage à actif viennent de votre espace ou d'un espace en dessous du vôtre. Sinon : 403, code status_set_by_ancestor si l'un d'eux vient d'un espace que vous ne gouvernez pas (jamais nommé), ou status_set_by_platform s'il vient de la plateforme. Un statut que votre propre clé a posé reste changeable par elle, tant qu'aucun espace au-dessus du vôtre ne l'a reposé. Un espace au-dessus qui repose la même suspension ou la même fermeture en devient l'auteur, sans rien changer à la période en cours ni à la facture ; votre clé reçoit alors 403, code status_set_by_ancestor, si elle tente de l'assouplir.
Lit un espace client de votre sous-arbre : les mêmes champs qu'une entrée de la liste, plus central_fallback, l'état de son repli sur la clé centrale de Solve (détaillé dans la section suivante). central_fallback vaut null pour un espace qui se paie lui-même : un espace racine, ou un espace enfant qui a choisi de se facturer lui-même. Sa clé centrale se règle alors par son propre contrat. Tout autre espace, dont le payeur effectif est un espace au-dessus de lui, porte un central_fallback.
curl https://gilbert.solveholding.com/api/headless/v1/workspaces/$WORKSPACE_ID \
-H "Authorization: Bearer $GILBERT_PROVISION_KEY"{
"id": "8f0c...e21b",
"name": "NAZ & Associés",
"slug": "naz",
"external_ref": "3f2a...",
"status": "active",
"effective_status": "active",
"parent_workspace_id": null,
"created_at": "2026-01-01T00:00:00.000Z",
"central_fallback": {
"allow": true,
"cap_eur": 150,
"spent_eur": 42.18,
"month": "2026-09"
}
}Un espace hors de votre sous-arbre répond 404, code workspace_not_found. Une lecture impossible répond 500, code internal_error, et se réessaie. Limite : 600 lectures par heure et par clé (429 rate_limited, ou 503 rate_limit_unavailable, avec Retry-After).
Un espace client ouvert par un revendeur n'utilise que ses propres clés de fournisseur, tant que le payeur effectif de son arbre n'a rien ouvert ici. Cette route lui permet de se replier sur la clé centrale de Solve quand il n'a pas de clé à lui, dans la limite d'un budget mensuel que ce payeur fixe et que Solve lui facture. Chaque espace de l'arbre a son propre budget, à tout niveau sous son payeur : dix espaces ouverts avec 150 € chacun engagent au plus 1 500 € par mois. Corps : { "allow": boolean, "cap_eur": number | null }.
curl -X PATCH https://gilbert.solveholding.com/api/headless/v1/workspaces/$WORKSPACE_ID/central-fallback \
-H "Authorization: Bearer $GILBERT_PROVISION_KEY" \
-H "Content-Type: application/json" \
-d '{ "allow": true, "cap_eur": 150 }'{
"workspace_id": "8f0c...e21b",
"central_fallback": {
"allow": true,
"cap_eur": 150,
"spent_eur": 42.18,
"month": "2026-09"
}
}curl -X PATCH https://gilbert.solveholding.com/api/headless/v1/workspaces/$WORKSPACE_ID/central-fallback \
-H "Authorization: Bearer $GILBERT_PROVISION_KEY" \
-H "Content-Type: application/json" \
-d '{ "allow": false, "cap_eur": null }'404 workspace_not_found, comme pour un espace hors de son arbre. Si l'arbre change entre la vérification et l'écriture, la même réponse s'applique. Authentification : la capacité headless:provision.allow n'y change rien, et la réponse rend l'état réel du repli. allow: false y remet le budget à zéro.cap_eur est un montant en euros facturés par mois civil, positif, fini, avec au plus deux décimales. Solve n'impose aucun maximum. Il est obligatoire quand allow vaut true (sinon 422 central_fallback_cap_required, y compris pour 0 ou un montant négatif). Quand allow vaut false, il est omis, nul ou égal à 0 (sinon 400 invalid_body). Un corps qui n'est pas un objet JSON, un allow non booléen ou un montant à plus de deux décimales répondent aussi 400 invalid_body.{ "workspace_id", "central_fallback": { "allow", "cap_eur", "spent_eur", "month" } }. spent_eur est la dépense du mois en cours sur la clé centrale, dans la même unité que cap_eur (euros facturés, soit le coût du fournisseur multiplié par 1,2), et month ce mois au format AAAA-MM. spent_eur vaut null quand la dépense ne peut pas être chiffrée : le repli est alors fermé pour ce mois, aucun appel ne passe. Si la lecture de la dépense échoue après l'écriture, le réglage est bien enregistré et la réponse omet spent_eur et month ; relisez-les par GET /workspaces/{id}.402 central_key_not_billable jusqu'à la fin du mois, ou jusqu'à ce que vous releviez le budget.409 workspace_suspended ou workspace_closed. Fermer le repli reste toujours possible, pour couper une dépense pendant une suspension.allow: false) et, une fois l'accès ouvert par cet administrateur, fixer son budget ; l'ouvrir répond 409 central_fallback_owned_by_admin. Sans administrateur à lui, le payeur l'ouvre et le ferme comme ci-dessus.409 central_fallback_not_provisioned pour un espace qui se paie lui-même au moment de l'écriture (un espace racine, ou un espace enfant qui a choisi de se facturer lui-même) : il se règle par son propre contrat. 429 rate_limited au-delà de 60 écritures par heure et par clé, ou 503 rate_limit_unavailable, les deux avec Retry-After. 500 central_fallback_failed si l'écriture échoue ; elle se réessaie.Facturation. Tout ce qu'un espace client que vous avez ouvert consomme sur la clé centrale vous est facturé au coût réel du fournisseur multiplié par 1,2, une fois par mois, sur la facture du payeur effectif de votre arbre. La facture porte une ligne par espace client et par fournisseur pour la conversation, et des lignes à part pour la lecture de documents (comptée en pages lues) et pour l'indexation documentaire (comptée en jetons indexés). Le montant d'un mois ne dépasse jamais ni la dépense mesurée, ni le plus grand budget en vigueur pendant ce mois : un budget baissé le 30 borne le mois par le budget qui était en vigueur avant, et un budget ouvert le 1er du mois suivant ne facture rien du mois précédent.
Ferme définitivement un espace client de votre sous-arbre : statut "closed" ET révocation de TOUTES ses clés API actives (conversation, ouverture, historiques, fermer c'est fermer). Aucune donnée supprimée, seul l'accès est coupé. Rejouable : fermer un espace client déjà fermé rend le même résultat sans rien réémettre. Si cette fermeture avait été posée par un espace en dessous du vôtre, la vôtre la reprend à votre nom (même période, facture inchangée) : la clé de cet espace reçoit désormais 403, code status_set_by_ancestor, si elle tente de rouvrir l'espace client ou de le repasser en suspension.
curl -X POST https://gilbert.solveholding.com/api/headless/v1/workspaces/$WORKSPACE_ID/close \
-H "Authorization: Bearer $GILBERT_PROVISION_KEY"Réponse : le même objet espace client que PATCH, statut "closed". Si la révocation des clés réussit mais que le changement de statut échoue ensuite, la réponse porte l'en-tête X-Gilbert-Keys-Revoked: true : vos clés sont déjà mortes même si l'espace client affiche encore un statut actif, un état intermédiaire à corriger en rappelant cette même route.
Réémet la clé headless:chat d'un espace client, réservé à son ouvreur direct : un ancêtre plus haut dans l'arbre qui gouverne quand même cet espace client reçoit un 404, exactement comme un tiers hors sous-arbre. Opérer n'est pas extraire.
curl -X POST https://gilbert.solveholding.com/api/headless/v1/workspaces/$WORKSPACE_ID/chat-key/rotate \
-H "Authorization: Bearer $GILBERT_PROVISION_KEY"{
"api_key": "glb_hl_nouvelle_cle"
}C'est le SEUL moyen de récupérer une clé de conversation perdue encore active. Rappeler POST /workspaces avec le même external_ref ne réémet une clé QUE s'il n'en reste aucune active pour cet espace client ; tant qu'une clé existe, ce rejeu répond api_key: null. Refusé (409, code workspace_suspended ou workspace_closed) sur tout espace client non actif : rouvrez d'abord via PATCH, faites tourner la clé ensuite.
Départ d'un espace client. Quand un espace que vous avez ouvert sort de votre arbre (détaché, ou déplacé sous un parent qui n'est pas sous vous), ses clés de conversation, les clés de fournisseur que vous y aviez posées et les intégrations de votre produit sur cet espace sont coupées pour de bon, au moment même du départ. Ce départ se décide chez vous, ou dans un espace placé au-dessus du vôtre : un espace intermédiaire, entre vous et ce client, ne peut ni le détacher ni le déplacer hors de votre arbre. Les utilisateurs que vous y aviez inscrits par cette API perdent en même temps leur appartenance à cet espace et leur accès ; leurs conversations restent dans l'espace. Vous perdez aussi, définitivement, les pouvoirs réservés à l'ouvreur (cette rotation, les clés de fournisseur, le routage des tâches), même si l'espace revient plus tard sous vous : ces routes répondent alors 404. Rappeler POST /workspaces avec son external_ref répond aussi 404 workspace_not_found, pour de bon : une nouvelle ouverture pour ce client demande un nouvel external_ref. L'espace parti se gère ensuite comme tout espace : s'il a son propre administrateur, c'est lui qui ouvre son accès à la clé de Solve ; sinon, celui qui le paie peut le lui ouvrir.
Règle, pour un espace client que votre clé a ouvert, quel fournisseur et quel palier servent chaque famille de tâches. Cinq familles : conversation, redaction_mail, lecture_document, tri, voix. Elles sont stables : nos traitements internes peuvent changer de nom sans que vous ayez à suivre.
Le GET rend les cinq familles, réglées ou non. source dit configured ou workspace_default. provider_key_present et effective_provider existent pour une raison précise : un réglage qui pointe vers un fournisseur sans clé est un repli permanent, invisible jusqu'au premier appel. Sans ces deux champs, un espace client peut croire tourner sur Mistral pendant des mois.
{
"routing": [
{ "task_family": "conversation", "provider": "anthropic", "model_tier": "smart",
"source": "workspace_default", "provider_key_present": true, "effective_provider": "anthropic" },
{ "task_family": "lecture_document", "provider": "mistral", "model_tier": "smart",
"source": "configured", "provider_key_present": false, "effective_provider": "anthropic" }
],
"solve_indexing_consent": { "granted": true, "granted_at": "2026-09-23T10:00:00.000Z" }
}Le PUT ne touche que les familles citées, et provider: null retire un réglage pour revenir au défaut de l'espace client. Un remplacement total vous ferait écraser quatre réglages en voulant en changer un, et la perte ne se verrait qu'à la facture suivante.
solve_indexing_consent, à côté du routage. C'est l'accord de l'espace client pour que ses documents soient aussi indexés par le moteur de recherche hébergé par Solve. Il est indépendant du fournisseur et du niveau de la lecture de document : un espace peut donner son accord ET confier la lecture à un fournisseur et un niveau de son choix. Le GET le rend sous la forme { granted, granted_at }. Le PUT accepte routing, solve_indexing_consent: { granted: true | false }, ou les deux dans le même corps ; une clé absente laisse sa partie inchangée. Retirer l'accord efface l'index déjà calculé par Solve, et le redonner ne le rend pas : il faut réindexer. Ne posez l'accord que sur un choix explicite de l'espace client. provider: "solve" n'est plus accepté et renvoie un 400.
L'espace client voit et modifie les mêmes réglages depuis sa console, dans Réglages › Quel modèle pour quelle tâche, si l'un de ses admins s'y connecte. Les deux surfaces lisent et écrivent la même chose : un réglage posé par votre API s'y affiche, et un réglage changé là s'affiche dans votre GET.
curl -X PUT https://gilbert.solveholding.com/api/headless/v1/workspaces/$WORKSPACE_ID/task-routing \
-H "Authorization: Bearer $GILBERT_PROVISION_KEY" \
-H "Content-Type: application/json" \
-d '{
"routing": [
{ "task_family": "lecture_document", "provider": "mistral", "model_tier": "smart" },
{ "task_family": "tri", "provider": null }
]
}'Régler un palier enlève le repli. Sans réglage, un brouillon de mail vise le meilleur modèle et redescend d'un cran si ce modèle est indisponible. Avec un réglage de la famille redaction_mail, Gilbert respecte le palier choisi et ne redescend plus à la place de l'espace client : un modèle indisponible donne un échec nommé, qui se répare en changeant le réglage. Prévenez le client quand il règle ce palier, avant qu'un appel n'échoue.
Les cinq familles pilotent de vrais appels. Aucune ne se contente de s'enregistrer.
Une exception, la téléphonie. Pour voix, seul le fournisseur est retenu. Le palier reste choisi appel par appel par le modèle qui pilote la routine, selon la délicatesse de la décision à prendre : au téléphone la latence s'entend, et forcer un palier dégraderait les appels nuancés ou ralentirait tous les autres. Si vous offrez un réglage de palier pour la voix, dites qu'il reste sans effet.
La conversation se règle en entier. Un espace client qui choisit un palier pour conversation l'applique à chaque tour de chat, et aussi au titre et au résumé de la conversation. Régler cette famille sur premium revient donc à payer premium sur tout ce qui touche au fil de discussion, y compris ce qui se calculait jusque-là au palier le moins cher.
Authentification : la clé d'ouverture d'espaces (headless:provision), jamais une clé de conversation. Un espace client que cette clé n'a pas ouvert, ou qui a quitté votre arbre, répond 404, jamais 403 : un 403 confirmerait l'existence de l'espace d'un tiers.
Le panneau intégré à votre produit (la barre, la fenêtre flottante et la vue insérée dans une page) prend les couleurs de l'espace client qu'il sert. Vous les réglez ici, pour le mode clair et pour le mode sombre à la fois. Un espace client sans couleurs à lui prend celles de l'espace le plus proche au-dessus de lui qui en a réglé : régler les couleurs de votre propre espace habille donc vos espaces clients, sauf ceux placés sous une marque blanche portée par un espace sous le vôtre (voir « La frontière de la marque blanche »), et chacun peut ensuite recevoir les siennes.
curl -X PUT https://gilbert.solveholding.com/api/headless/v1/workspaces/$WORKSPACE_ID/panel-theme \
-H "Authorization: Bearer $GILBERT_PROVISION_KEY" \
-H "Content-Type: application/json" \
-d '{
"clair": {
"fond": "#fbfaf7",
"surface": "#ffffff",
"surface_bandeau": "#f3f1ec",
"encre": "#1a1a1a",
"encre_douce": "#5c5c5c",
"bordure": "#d9d6cf",
"accent": "#b3261e",
"encre_sur_accent": "#ffffff",
"focus": "#b3261e"
},
"sombre": {
"fond": "#141414",
"surface": "#1f1f1f",
"surface_bandeau": "#262626",
"encre": "#f2f2f2",
"encre_douce": "#a3a3a3",
"bordure": "#3a3a3a",
"accent": "oklch(72% 0.15 25)",
"encre_sur_accent": "#1a0000",
"focus": "oklch(72% 0.15 25)"
}
}'{
"panel_theme": { "clair": { "fond": "#fbfaf7", "...": "..." }, "sombre": { "...": "..." } },
"panel_theme_set_by": "governor",
"effective_panel_theme": { "clair": { "fond": "#fbfaf7", "...": "..." }, "sombre": { "...": "..." } }
}curl -X DELETE https://gilbert.solveholding.com/api/headless/v1/workspaces/$WORKSPACE_ID/panel-theme \
-H "Authorization: Bearer $GILBERT_PROVISION_KEY"fond (le fond de la page), surface (le fond d'une fenêtre, que prennent la fenêtre flottante et la vue insérée dans une page), surface_bandeau (le fond d'un bouton de votre barre d'outils, que prend la barre), encre, encre_douce, bordure, accent (le bouton principal), encre_sur_accent (le texte de ce bouton) et focus. Chacune est une couleur #rrggbb ou un triplet oklch(L C H) de 64 caractères au plus, la teinte H sans %. Une couleur de trop, une couleur manquante, un mode absent ou toute autre forme rendent 422 invalid_panel_theme, et rien n'est écrit.PUT envoyé deux fois laisse le même état. DELETE retire les couleurs propres de l'espace client, et répond 200 même s'il n'en avait pas.panel_theme porte les couleurs propres de l'espace client, null quand il hérite, ou quand elles ont été réglées par un espace qui n'est plus au-dessus de lui (l'espace client a été déplacé sous un autre espace) : elles ne s'appliquent plus et ne bloquent plus votre réglage. panel_theme_set_by dit qui les a réglées : "workspace" (l'espace client lui-même, avec sa propre clé), "governor" (un espace au-dessus de lui : vous, un espace parent intermédiaire placé entre vous et lui, ou un espace au-dessus de vous) ou null sans couleurs propres. effective_panel_theme porte celles que le panneau affiche réellement, null quand aucune ne s'applique : l'espace client est fermé, aucun espace de sa lignée n'a réglé de couleurs, ou aucune de celles réglées ne compte, parce qu'elle est posée sur un espace situé au-dessus de l'espace qui porte sa marque blanche ou sur un espace fermé, ou par un espace fermé ou qui ne gouverne plus celui où elle est réglée. Le panneau garde alors son apparence par défaut.encre et encre_douce lisibles sur fond, surface et surface_bandeau, et encre_sur_accent lisible sur accent. focus doit rester visible sur fond, surface et surface_bandeau. En mode sombre, focus sert aussi de couleur de texte : l'onglet choisi, la conversation en cours et les liens cités dans une réponse s'écrivent dans cette couleur. Choisissez-le lisible comme texte, à 4,5:1 au moins, sur fond et sur surface. La couleur d'accent peut ainsi rester foncée dans les deux modes pour porter un texte clair sur le bouton principal.415 sans en-tête Content-Type: application/json, 413 au-delà de 4 Kio de corps, 400 sur un JSON illisible, 429 au-delà de 120 écritures ou 600 lectures par heure et par clé (avec Retry-After), 503 rate_limit_unavailable (avec Retry-After) ou panel_theme_unavailable sur une panne passagère : réessayez plus tard. Quand seule la relecture qui suit une écriture réussie échoue, un PUT répond tout de même 200 avec ce qu'il vient d'écrire ; un DELETE répond 503, et le rejouer est sans risque.headless:provision), pour son propre espace ou un espace client de son sous-arbre. Tout autre espace répond 404 workspace_not_found, jamais 403. Un espace client suspendu ou fermé répond 409 workspace_inactive à l'écriture. Votre propre espace suspendu ou fermé relève de la règle générale des clés (403) ; dans les secondes qui suivent sa suspension, une écriture peut encore répondre 409 workspace_inactive.409 panel_theme_owned_by_workspace, y compris quand les couleurs en cause sont celles d'un espace parent intermédiaire et que celles de l'espace visé sont à null. L'espace qui a réglé des couleurs peut toujours les retirer. Les réécrire, ou en régler de nouveau à cet endroit une fois retirées, peut en revanche répondre 409 : il suffit qu'entre-temps un espace parent intermédiaire, placé entre lui et l'espace visé sans être au-dessus de l'espace qui porte la marque blanche de l'espace visé, ait posé les siennes.409. La frontière peut aussi bouger. Quand la marque blanche de l'espace qui la porte prend fin, ou quand cet espace redonne à son assistant le nom par défaut, la frontière remonte vers le prochain espace au-dessus qui remplit les trois conditions de « La frontière de la marque blanche », ou disparaît. Les couleurs d'un espace parent intermédiaire, placé entre vous et le client, peuvent alors de nouveau l'emporter sur les vôtres, sans avertissement, dès qu'il ne se trouve plus au-dessus de l'espace qui porte la marque blanche, ou quand plus aucun espace ne porte de marque blanche pour ce client. Relisez effective_panel_theme par un GET pour savoir ce que le panneau affiche. Dans ce même cas, les couleurs de cet espace parent intermédiaire bloquent de nouveau vos réglages sur ce client : modifier vos couleurs sur ce client répond 409, les retirer reste possible. À l'inverse, quand un espace plus proche du client se met à porter une marque blanche, la frontière descend jusqu'à lui, et les couleurs réglées au-dessus de lui cessent de compter et de bloquer. Suspendre ou fermer un espace suspend ou ferme aussi tous les espaces placés sous lui : tant que le client est suspendu ou fermé, ses écritures répondent 409 workspace_inactive, ou 403 selon la règle générale des clés si l'espace concerné est le vôtre ou se trouve au-dessus du vôtre. Un espace fermé n'affiche plus de couleurs.Brancher votre serveur MCP sur le panneau. Le panneau intégré à votre produit peut appeler les outils de lecture de votre propre serveur MCP, au nom de la personne connectée. Pour un espace client que vous avez ouvert, vous posez ici la connexion, puis vous déclarez les outils que le panneau a le droit d'appeler par PUT …/product-connection/tools, et vous renouvelez son secret par POST …/product-connection/rotate-secret. L'intégration du panneau de cet espace client doit déjà exister : elle se crée avec l'équipe Solve.
curl -X PUT https://gilbert.solveholding.com/api/headless/v1/workspaces/$WORKSPACE_ID/product-connection \
-H "Authorization: Bearer $GILBERT_PROVISION_KEY" \
-H "Content-Type: application/json" \
-d '{
"panel_integration_id": "votreproduit-client-a",
"slug": "votreproduit",
"label": "Votre produit",
"endpoint_url": "https://client-a.votreproduit.example/api/mcp",
"token_endpoint": "https://client-a.votreproduit.example/api/gilbert/panel/token"
}'{
"workspace_id": "1f0c…",
"created": true,
"connection": {
"id": "8b2e…",
"slug": "votreproduit",
"label": "Votre produit",
"panel_integration_id": "votreproduit-client-a",
"client_id": "votreproduit-client-a",
"token_client_secret_sha256": "3f9a…(64 caractères hexadécimaux)",
"is_active": true,
"created_at": "2026-10-06T08:00:00Z"
}
}token_client_secret_sha256. Votre serveur d'échange compare l'empreinte du secret reçu à celle-ci ; le client_id que rend la lecture de l'état vaut panel_integration_id. N'envoyez ni secret, ni jeton, ni client_id : tout champ inconnu répond 422 unknown_field.endpoint_url, l'adresse de votre serveur MCP, et token_endpoint, celle où Gilbert échange la session de la personne contre un jeton de votre produit, sont en https, hôte en minuscules, sans port, sans identifiants, sans paramètre ni fragment. endpoint_url doit être sur l'origine de l'intégration du panneau (sinon 422 endpoint_origin_mismatch) ; token_endpoint mal formé ou hors de l'origine de endpoint_url répond 422 bad_panel_token_endpoint, et une endpoint_url mal formée 422 bad_endpoint. Une intégration dont l'adresse porte un autre port que 443 ne peut donc pas recevoir de connexion. Le slug fait 1 à 48 caractères parmi les minuscules, les chiffres et _, sans __ ni _ final (422 bad_slug sinon) ; le label, nom affiché, 1 à 120 caractères une fois les espaces de bord retirés (422 bad_label sinon). Un panel_integration_id mal formé répond 400 invalid_body ; un panel_integration_id qui ne désigne pas l'intégration de cet espace client répond 422 panel_integration_unknown. Le corps tient en 4 Kio (au-delà, 413 body_too_large). Gilbert n'appelle aucune autre adresse.PUT répond 201 la première fois, puis 200 avec la même connexion et la même empreinte. Le label n'entre pas dans la comparaison : un PUT qui ne change que lui répond 200 avec le label déjà posé ; pour le changer, retirez puis reposez la connexion, qui reçoit alors un nouveau secret. Une connexion déjà posée sous ce slug avec une autre adresse répond 409 product_connection_differs : retirez-la d'abord. Un espace client porte une seule connexion active par intégration du panneau et par hôte : une pose sous un autre slug qui reprend l'intégration ou l'hôte d'une connexion active répond aussi 409 product_connection_differs. Pour changer de slug, retirez d'abord l'ancienne connexion ; si elle n'est pas la vôtre (le retrait répond retired faux), contactez l'équipe Solve. Un slug tenu par l'équipe Solve ou par un autre produit répond 409 product_slug_taken. Une connexion posée sous ce slug sans passer par cette API répond 409 product_connection_differs si son adresse, son token_endpoint ou son intégration diffèrent de ceux envoyés ; avec les mêmes, 409 product_connection_unmanaged : renouvelez son secret si elle a un token_endpoint et que Gilbert peut encore l'utiliser pour agir au nom des personnes, sinon contactez l'équipe Solve. La même réponse vient d'une connexion que l'équipe Solve a rétablie sans rouvrir l'action au nom des personnes : contactez-la.409 product_connection_held sous ce slug. La pose répond de même sous tout autre slug qui reprend la même intégration du panneau ou le même hôte, y compris après un retrait ; le renouvellement et la déclaration d'outils lisent la coupure sur la connexion active de leur slug. Ne rejouez pas et ne reposez pas la connexion sous un autre nom : contactez l'équipe Solve, qui seule la rétablit. Le retrait reste possible.DELETE …/product-connection?slug=… débranche la connexion et retire ses outils du panneau, et rend workspace_id, retired (faux si aucune connexion active à vous ne portait ce slug) et connection_id. Il ne retire ces outils que des intégrations du panneau que vous pouvez écrire, celles de cet espace client sous votre référence : les outils que l'équipe Solve a placés ailleurs restent. Il retire aussi les outils d'une connexion que l'équipe Solve a coupée, en répondant retired faux, à trois conditions : vous avez posé ou renouvelé une connexion sous ce slug sur cet espace client, aucune connexion active ne porte ce slug, et toutes les connexions de ce slug sur cet espace client sont les vôtres, ou il n'en reste aucune. Dès qu'une connexion de ce slug vient de l'équipe Solve ou d'un autre produit, quelle que soit sa date, les outils placés sous ce nom restent. Sinon il ne touche à aucun outil. Il reste possible sur un espace client suspendu et sur une connexion coupée par l'équipe Solve, sans lever la coupure. Une nouvelle pose après un retrait ne rend pas au panneau les réponses déjà données par la connexion retirée. Poser ou renouveler une connexion demande un espace client actif (sinon 409 workspace_suspended ou workspace_closed).GET …/product-connection rend workspace_id et connections, la liste de vos connexions sur cet espace client, les actives d'abord, chacune avec id, slug, label, panel_integration_id, client_id, token_client_secret_sha256, is_active, created_at et les tool_ids déclarés pour une connexion active. Chaque renouvellement garde l'ancienne connexion, inactive : la liste s'arrête à 50 connexions, les actives d'abord, puis les plus récentes.GET, PUT et DELETE …/product-connection, POST …/product-connection/rotate-secret et PUT …/product-connection/tools. La clé d'ouverture d'espaces (headless:provision) de l'espace qui a ouvert cet espace client et le gouverne encore. Tout autre appelant reçoit 404, jamais 403. 429 au-delà de 150 changements de connexion, 300 déclarations d'outils ou 600 lectures de l'état par heure et par clé. 503 quand le limiteur est indisponible (avec Retry-After) ou que le service n'est pas encore ouvert ; 503 internal_error, sans Retry-After, quand l'état de l'espace ne se lit pas un instant ; 500 internal_error sur une panne de base. Dans ces cas l'issue est inconnue : rejouez la même requête après quelques secondes, elle est rejouable. Sur les trois routes qui prennent un corps (PUT …/product-connection, POST …/rotate-secret et PUT …/tools), un corps envoyé sans Content-Type: application/json répond 415 unsupported_media_type, un JSON illisible 400 invalid_body.Vos outils s'interrompent. Entre le renouvellement et le déploiement de votre serveur qui accepte la nouvelle empreinte, les outils de votre produit sont indisponibles dans le panneau de cet espace client : vous n'apprenez la nouvelle empreinte qu'au renouvellement. Accepter deux empreintes pendant ce déploiement vous permet de retirer l'ancienne plus tard, sans seconde interruption.
Les assistants reliés se délient. Les personnes qui ont relié un assistant (Claude, ChatGPT) à votre produit à travers Gilbert doivent le relier de nouveau après le renouvellement.
curl -X POST https://gilbert.solveholding.com/api/headless/v1/workspaces/$WORKSPACE_ID/product-connection/rotate-secret \
-H "Authorization: Bearer $GILBERT_PROVISION_KEY" \
-H "Content-Type: application/json" \
-d '{ "slug": "votreproduit", "demande_id": "0b6b7c0e-2a8f-4b8a-9f5e-0e8a1b2c3d4e" }'POST …/product-connection/rotate-secret rend workspace_id, rotated, previous_connection_id et connection, de même forme que celle de la pose. Il remplace la connexion par la même avec un nouveau secret, et rend la nouvelle empreinte (201). Le demande_id est obligatoire (un UUID, lu en minuscules) : le rejouer rend la même réponse en 200, ou 409 rotation_superseded si un renouvellement plus récent l'a remplacé (avec current_connection_id, la connexion en vigueur). Un demande_id absent ou mal formé répond 422 invalid_demande_id, un demande_id déjà employé pour une autre demande 409 demande_id_reused, et l'absence de connexion active sous ce slug, retrait compris, 409 product_connection_missing. La même réponse vient d'un demande_id rejoué après un retrait suivi d'une nouvelle pose sous ce slug : la connexion en vigueur n'est plus celle que ce renouvellement a remplacée. Lisez-la par le GET de la connexion, sans la reposer. Le renouvellement demande un espace client actif et une connexion que l'équipe Solve n'a pas coupée, rejeu compris : si l'espace a été suspendu entre-temps et que la réponse s'est perdue, le GET de la connexion rend l'empreinte en vigueur. Le corps tient en 4 Kio (au-delà, 413 body_too_large).slug tenu par l'équipe Solve ou par un autre produit répond 409 product_slug_taken : contactez l'équipe Solve. Une connexion sans intégration du panneau ou sans token_endpoint, ou dont l'équipe Solve n'a pas rouvert l'action au nom des personnes, répond 409 product_connection_unmanaged : contactez l'équipe Solve. Si l'intégration du panneau a changé d'adresse depuis la pose, 422 endpoint_origin_mismatch ; si elle n'est plus celle de cet espace client, 422 panel_integration_unknown. Une connexion coupée par l'équipe Solve répond 409 product_connection_held.Déclarer les outils du panneau. Envoyez, pour une connexion posée, les outils que le panneau peut appeler, sous la forme exacte où votre serveur les publie (name, description, inputSchema). Gilbert calcule et retient l'empreinte de chaque description et de chaque schéma. Si votre serveur publie ensuite un outil dont la description ou le schéma diffère, même d'un caractère, cet outil disparaît du panneau jusqu'à ce que vous le déclariez de nouveau : redéclarez vos outils après chaque déploiement qui les change. Les champs title, annotations et _meta de chaque outil sont acceptés, pour que vous puissiez envoyer la liste telle que votre serveur la publie ; ils n'entrent dans aucune empreinte et ne sont pas conservés. Une exception : un outil dont _meta["gilbert/third-party-content"] vaut true est marqué comme rendant du contenu écrit par un tiers (par exemple l'expéditeur d'un mail). Ce marquage est conservé et entre dans l'empreinte : ajoutez-le ou retirez-le, puis redéclarez vos outils. Toute autre valeur que true ou false fait refuser la déclaration, code bad_tool.
curl -X PUT https://gilbert.solveholding.com/api/headless/v1/workspaces/$WORKSPACE_ID/product-connection/tools \
-H "Authorization: Bearer $GILBERT_PROVISION_KEY" \
-H "Content-Type: application/json" \
-d '{
"panel_integration_id": "votreproduit-client-a",
"slug": "votreproduit",
"effect": "read",
"tools": [
{
"name": "factures_a_valider",
"description": "Liste les factures en attente de validation.",
"inputSchema": { "type": "object", "properties": {} }
}
]
}'effect vaut "read" ; toute autre valeur répond 422 panel_tool_effect_not_open.slug, et laisse intacts tous les autres outils du panneau. La réponse rend workspace_id, panel_integration_id, slug, les identifiants d'outils (tool_ids), changed (faux quand rien n'a bougé) et policies_sha256, l'empreinte de la déclaration, donnée pour information. Une liste vide retire tous les outils de ce slug. Envoyez votre déclaration à chaque déploiement et après chaque pose qui répond 201, sans la comparer à ce que vous aviez envoyé : un retrait suivi d'une nouvelle pose efface les outils déclarés, et changed vous dit ce qui a bougé.409 workspace_suspended ou workspace_closed), y compris pour en retirer : sur un espace suspendu, seul le retrait de la connexion restreint le panneau.panel_integration_id de la connexion posée sous ce slug : un autre répond 409 product_connection_differs. Sans connexion active sous ce slug, 409 product_connection_missing : posez d'abord la connexion. Une connexion active sous ce slug sans intégration du panneau, ou dont l'équipe Solve n'a pas rouvert l'action au nom des personnes, répond 409 product_connection_unmanaged, comme à la pose : contactez l'équipe Solve. Les autres refus de la pose valent ici : 409 product_slug_taken pour un slug tenu par l'équipe Solve ou par un autre produit, 422 panel_integration_unknown et 422 endpoint_origin_mismatch pour une intégration qui n'est plus celle de cet espace client ou qui a changé d'adresse, 409 product_connection_held pour une connexion coupée.readOnlyHint à false, ou destructiveHint à true sans readOnlyHint à true) est refusé.422 bad_tool au-delà), et le panneau 100 au plus en comptant ses autres outils, ceux des autres connexions et ceux de votre site compris (422 panel_tool_limit au-delà). Chaque outil doit avoir un name fait de lettres, de chiffres, de _ et de -, assez court pour que mcp_, le slug, __ et ce nom tiennent ensemble en 64 caractères ; un inputSchema de type object, de 16 Kio et de 64 niveaux d'imbrication au plus ; une description de 4 096 caractères au plus. Le corps tient en 512 Kio (413 body_too_large), l'ensemble des schémas en 384 Kio (413 panel_tools_too_large). Un outil refusé répond 422 bad_tool, avec sa position dans la liste (tools[3]) dans le message ; un nom déclaré deux fois, 422 duplicate_tool.Rend la facture de la partie du sous-arbre gouverné par votre clé que cet espace paie (ou de votre propre espace client, si {id} le désigne lui-même). Un espace en dessous qui a choisi de se facturer lui-même est facturé à lui-même : sa consommation ne compte pas dans cette facture. Pour chaque espace, les jours-sièges, les sièges au prorata, et la consommation ventilée par payeur (workspace si la clé propre de l'espace client a réglé l'appel, central si c'est la clé centrale de Gilbert qui a servi de repli) et par fournisseur, plus un total sur tout le sous-arbre. À vous d'appliquer votre propre grille tarifaire aux jetons rendus ici : cette route compte, elle ne fixe pas de prix.
Période réglée par from et to (horodatages ISO 8601, bornés respectivement inclus et exclu). Par défaut, sans l'un ou l'autre : du premier jour du mois calendaire en cours (00:00 UTC) jusqu'à minuit UTC du lendemain (la journée en cours est donc incluse en entier, pas seulement jusqu'à l'instant de l'appel). Bornée à 366 jours ; au-delà, 400, code period_too_long. Une période mal formée ou où to ne suit pas from répond 400, code invalid_period.
from et to doivent tomber pile sur minuit UTC (00:00:00.000Z) quand vous les fournissez : sinon 400, code period_not_midnight_utc. La consommation est stockée AU JOUR, un jour touché ne serait-ce qu'une seconde comptant en entier : une borne à midi facturerait le jour-pivot en double entre deux appels adjacents, et une période calée sur un fuseau non-UTC (par exemple un mois en heure de Paris) ne coïnciderait jamais avec les bornes d'un mois calendaire UTC, ce qui ferait tomber la facture à zéro, en silence, une fois le détail journalier purgé à 90 jours. Passez toujours des horodatages en Z (UTC), jamais un décalage local.
curl "https://gilbert.solveholding.com/api/headless/v1/workspaces/$WORKSPACE_ID/usage?from=2026-09-01T00:00:00.000Z&to=2026-10-01T00:00:00.000Z" \
-H "Authorization: Bearer $GILBERT_PROVISION_KEY"{
"period": { "from": "2026-09-01T00:00:00.000Z", "to": "2026-10-01T00:00:00.000Z" },
"per_workspace": [
{
"workspace_id": "8f0c...e21b",
"seat_days": 120,
"prorated_seats": 4,
"billed": true,
"usage": {
"workspace": { "tokens_in": 197000, "tokens_out": 44000, "call_count": 360 },
"central": { "tokens_in": 9400, "tokens_out": 1100, "call_count": 12 },
"unknown": { "tokens_in": 0, "tokens_out": 0, "call_count": 0 }
},
"usage_by_provider": {
"anthropic": {
"workspace": { "tokens_in": 182000, "tokens_out": 41000, "call_count": 340 },
"central": { "tokens_in": 9400, "tokens_out": 1100, "call_count": 12 },
"unknown": { "tokens_in": 0, "tokens_out": 0, "call_count": 0 }
},
"mistral": {
"workspace": { "tokens_in": 15000, "tokens_out": 3000, "call_count": 20 },
"central": { "tokens_in": 0, "tokens_out": 0, "call_count": 0 },
"unknown": { "tokens_in": 0, "tokens_out": 0, "call_count": 0 }
}
}
}
],
"total": {
"seat_days": 120,
"usage": {
"workspace": { "tokens_in": 197000, "tokens_out": 44000, "call_count": 360 },
"central": { "tokens_in": 9400, "tokens_out": 1100, "call_count": 12 },
"unknown": { "tokens_in": 0, "tokens_out": 0, "call_count": 0 }
},
"usage_by_provider": {
"anthropic": {
"workspace": { "tokens_in": 182000, "tokens_out": 41000, "call_count": 340 },
"central": { "tokens_in": 9400, "tokens_out": 1100, "call_count": 12 },
"unknown": { "tokens_in": 0, "tokens_out": 0, "call_count": 0 }
},
"mistral": {
"workspace": { "tokens_in": 15000, "tokens_out": 3000, "call_count": 20 },
"central": { "tokens_in": 0, "tokens_out": 0, "call_count": 0 },
"unknown": { "tokens_in": 0, "tokens_out": 0, "call_count": 0 }
}
}
}
}Le nombre de sièges d'un espace ne se déclare pas : il se compte automatiquement sur les personnes qui y sont réellement présentes. Rien à poser ni à tenir à jour de votre côté, cette route rend directement le nombre déjà à jour.
Le comptage des sièges ne remonte pas avant une certaine date : demander une période dont from précède la mise en service de ce comptage répond 400, code period_before_seat_tracking_start, plutôt qu'un total plausible mais incomplet. Redemandez une période qui commence à partir de cette date, ou plus tard.
Les sièges se facturent au prorata des jours actifs. Un espace suspendu le 12 d'un mois de 30 jours compte 12/30 de ses sièges : dans l'exemple ci-dessus, un espace client à 10 sièges resté actif 12 jours sur 30 pèse seat_days: 120 puis prorated_seats: 4, pas 10. Un revendeur qui coupe un client qui ne paie plus cesse de payer pour lui le jour même, ni avant ni après.
Un espace transféré d'un revendeur à un autre en cours de mois coûte ses jours à l'ancien et le reste au nouveau, pour les sièges comme pour la consommation : un espace client ouvert par le revendeur A jusqu'au 10 du mois puis transféré au revendeur B jusqu'à la fin d'un mois de 30 jours compte 10 jours-sièges (et sa consommation de ces 10 jours) sur la facture de A, et 20 jours-sièges (et le reste de sa consommation) sur celle de B. Personne ne paie deux fois, personne ne paie pour des jours qu'il ne gouvernait pas.
Le détail jour par jour de la consommation n'est conservé que 90 jours. Avant sa purge, un récapitulatif mensuel est figé par espace, par fournisseur et par payeur : interroger une période plus ancienne sur cette même route reste possible, au mois plutôt qu'au jour, avec exactement les chiffres déjà figés à l'époque plutôt qu'un recalcul sur une source déjà purgée.
Reste lisible sur un espace client suspendu ou fermé : gouverner n'est pas appartenir, et un revendeur doit pouvoir facturer un client qu'il vient de suspendre ou de fermer. Un espace client hors de votre sous-arbre (et qui n'est pas le vôtre) répond 404, jamais 403 : la réponse ne doit pas confirmer l'existence de l'espace d'un tiers.
{id} doit être le payeur effectif de son propre arbre : seul l'espace qui paie réellement pour sa lignée (sa propre racine, ou un espace qui a choisi de se facturer lui-même) porte jamais le rôle de payeur. Si vous gouvernez vous-même un sous-revendeur et que vous demandez la facture d'un de ses espaces clients plutôt que la sienne, la réponse est 409, code not_a_payer_root, jamais un 200 rempli de zéros qui se confondrait avec une consommation réellement nulle. Demandez alors la facture au payeur effectif de cet espace client : le plus souvent votre propre espace, dont la facture contient déjà ce client. Le sous-revendeur n'est le bon interlocuteur que s'il a choisi de se facturer lui-même.
L'espace du revendeur lui-même apparaît aussi dans la réponse, marqué billed: false : il n'est jamais facturé, quelle que soit sa consommation réelle. C'est voulu, pas une panne de mesure : un revendeur ne paie pas pour sa propre équipe. Ses compteurs restent donc à zéro sur cette ligne précise, tout en reflétant la vraie consommation des autres espaces.
Un espace de votre arbre qui se facture lui-même est aussi marqué billed: false : il reste listé parce qu'il est rattaché sous vous, mais aucune part de la période ne vous est facturée, ni pour lui ni pour les espaces dont il prend la charge. Ses compteurs sont à zéro sur votre facture ; sa consommation figure sur la sienne. Un espace qui vous a quitté en cours de période garde billed: true pour les jours où vous le payiez.
Cette route ne rend que des agrégats : jours, sièges, jetons. Jamais un message, un titre de conversation ou un contenu de document. Un revendeur administre la consommation de ses clients sans jamais lire ce qu'ils écrivent.
Dit, au grain du couple (fournisseur, palier), ce que Gilbert sait de la capacité à lire un document. Lisez-le plutôt que de recopier une liste chez vous : une liste recopiée se périme le jour où Gilbert fait tourner un modèle.
{
"capabilities": [
{ "provider": "anthropic", "tier": "smart", "model_id": "claude-sonnet-5", "capability": "capable" },
{ "provider": "openai", "tier": "fast", "model_id": "gpt-4o-mini", "capability": "incapable" },
{ "provider": "google", "tier": "premium", "model_id": "gemini-3.1-pro-preview", "capability": "unprobed" }
]
}capability vaut capable, incapable ou unprobed. Trois états et non deux : unprobed signifie que la sonde n'a pas encore mesuré ce couple, et la lecture se tente quand même. Ne traitez pas un unprobed comme un refus. Seul un incapable mesuré fait refuser la lecture, avec le code provider_cannot_read_documents.
Ces valeurs sont mesurées, pas déclarées. Une sonde quotidienne envoie à chaque modèle un PDF d'une page portant un jeton et ne conclut capable que si le modèle le restitue : accepter la requête ne suffit pas. Un incident passager (quota, réseau, délai dépassé) laisse la valeur à unprobed plutôt que de la faire basculer à tort en incapable.
Retire un membre de l'espace de votre clé, et de cet espace seulement. Son compte Gilbert, son espace personnel et les autres espaces dont il est membre restent intacts. C'est l'appel à faire au départ d'un membre ; la suppression définitive (ci-dessous) ne sert qu'à une demande explicite d'effacement.
curl -X DELETE "https://gilbert.solveholding.com/api/headless/v1/members?external_user_id=icon-user-4821" \
-H "Authorization: Bearer $GILBERT_HEADLESS_KEY"{
"removed": true
}Ses accès à l'espace par l'assistant IA de votre produit sont coupés. Ce qu'il a créé pour l'espace y reste et passe au plus ancien des administrateurs humains de l'espace (le compte technique d'un espace provisionné et les comptes que votre produit a inscrits par l'API, même promus administrateurs, ne sont jamais retenus) : les routines partagées avec l'espace ou suivies par un collègue, qui continuent de tourner comme avant, et les adresses de renvoi. L'historique de ses propres exécutions, ses routines personnelles et ses abonnements dans l'espace sont supprimés.
La réponse est {"removed": true}, y compris quand le membre a déjà été retiré : l'appel se rejoue sans risque. 404, code end_user_not_found, si l'utilisateur est inconnu pour cet espace. 409, code workspace_has_no_human_admin, et rien ne change : le membre a créé des éléments à reprendre et aucun autre administrateur humain ne peut les recevoir, ou il est le dernier administrateur de l'espace ; nommez administrateur, dans la console Gilbert, une personne qui a son propre compte Gilbert (un compte inscrit par l'API de votre produit ne compte pas), puis relancez l'appel. Un membre qui n'a rien créé pour l'espace est retiré même sans autre administrateur humain. Rejouez une réponse 5xx.
Cessez d'abord d'autoriser ce membre à connecter l'assistant IA de votre produit à Gilbert, puis faites l'appel : une connexion qu'il mènerait à son terme après l'appel le réinscrirait dans l'espace.
Efface tout de suite, dans l'espace de votre clé et sur le panneau intégré de votre produit, les échanges d'une personne qui portent du contenu tiers : ceux d'une conversation où un outil déclaré contenu tiers a été appelé, comme la lecture d'un mail. Le paramètre panel_integration_id désigne l'intégration de panneau de votre produit dans cet espace client, le même identifiant que celui de sa connexion produit : un autre produit branché sur le même espace garde ses propres échanges, même s'il nomme la personne de la même façon. Le paramètre subject est l'identifiant que votre produit donne au panneau pour cette personne, le même que dans sa session de panneau, distinct de l'external_user_id de l'API de chat. Encodez-le dans l'adresse, avec encodeURIComponent par exemple. C'est l'appel à faire à son départ, à côté du retrait de membre, une fois son accès à votre produit retiré : un échange écrit après l'appel n'est effacé qu'au bout de 30 jours.
curl -X DELETE "https://gilbert.solveholding.com/api/headless/v1/panel/third-party-turns?panel_integration_id=votreproduit-client-a&subject=votreproduit-user-4821" \
-H "Authorization: Bearer $GILBERT_HEADLESS_KEY"{
"deleted": 3
}La réponse donne le nombre d'échanges effacés, 0 quand il n'y en a plus : l'appel se rejoue sans risque. Les autres échanges de la personne restent, et une conversation qui a lu du contenu tiers reste fermée aux actions et aux services reliés. 400, code invalid_panel_integration_id, si panel_integration_id manque ou ne fait pas 1 à 128 caractères parmi les lettres, les chiffres, _ et - ; 400, code invalid_subject, si subject manque, est vide, ne contient que des espaces ou dépasse 256 caractères ; 422, code panel_integration_unknown, si l'intégration n'appartient pas à l'espace de votre clé, et rien n'est effacé ; 500, code panel_forget_failed, à rejouer plus tard. Sans cet appel, ces échanges sont de toute façon effacés 30 jours après le premier contenu tiers lu dans leur conversation.
Supprime définitivement un utilisateur final : sa mémoire, ses messages et ses sessions sont purgés en cascade. Opération irréversible, à utiliser pour honorer un droit à l'oubli ou nettoyer un compte de test.
curl -X DELETE "https://gilbert.solveholding.com/api/headless/v1/users?external_user_id=icon-user-4821" \
-H "Authorization: Bearer $GILBERT_HEADLESS_KEY"{
"deleted": true
}404, code end_user_not_found, si l'utilisateur n'existe pas déjà. Sinon {"deleted": true}.
external_user_id.usage.text/read : 300 000 caractères de texte et 8 000 caractères de consigne au plus, 240 lectures par heure et par clé.Au-delà du plafond d'utilisateurs : 403, code end_user_cap_reached. Au-delà du rythme de création : 429, code end_user_creation_rate_limited.
Deux familles bien distinctes : les codes HTTP classiques (avant que le stream ne démarre) et les events error à l'intérieur du flux NDJSON de /chat, une fois les en-têtes déjà envoyés en 200.
| Statut | Code | Sens |
|---|---|---|
| 400 | validation_error | Champ requis manquant, vide, trop long, ou output_schema mal formé. |
| 400 | missing_text | text absent, vide ou non textuel, ou corps qui n'est pas un objet JSON valide (text/read). Les autres refus de cette route (empty_prompt, prompt_too_long, invalid_provider, invalid_tier, invalid_output_schema) sont décrits dans sa section. |
| 400 | invalid_period | from ou to n'est pas un horodatage ISO 8601 valide, ou to ne suit pas from (usage). |
| 400 | period_too_long | Période from/to supérieure à 366 jours (usage). |
| 400 | period_not_midnight_utc | from ou to ne tombe pas pile sur minuit UTC (usage). |
| 400 | period_before_seat_tracking_start | from précède la mise en service du comptage des sièges (usage) : demandez une période qui commence plus tard. |
| 400 | invalid_body | Corps JSON absent, mal formé, ou qui ne respecte pas le schéma attendu (ouverture d'un espace client, changement de statut, réglage du routage par tâche). |
| 400 | invalid_external_user_id | external_user_id absent, vide ou invalide, dans le corps ou en paramètre. |
| 400 | invalid_panel_integration_id | panel_integration_id absent ou hors de sa forme, 1 à 128 caractères parmi les lettres, les chiffres, _ et - (panel/third-party-turns). |
| 400 | invalid_subject | subject absent, vide, fait seulement d'espaces ou au-delà de 256 caractères (panel/third-party-turns). |
| 401 | - | Clé absente, inconnue ou révoquée. |
| 402 | central_key_not_billable | L'espace ne peut plus être facturé ce mois-ci sur la clé centrale. Sur documents/read et text/read : budget atteint, ou dépense du mois impossible à chiffrer, et relancer n'y change rien. Sur les autres routes, ce code couvre aussi une consommation illisible, qui passe en relançant. |
| 403 | scope_missing | Clé valide mais de mauvais type ou mauvais scope. |
| 403 | end_user_cap_reached | Plafond de 5 000 utilisateurs finaux atteint pour ce workspace. |
| 403 | workspace_suspended / workspace_closed | L'espace de VOTRE clé n'est plus actif, sur n'importe quel endpoint headless. |
| 403 | status_set_by_ancestor | PATCH /workspaces/{id} : le statut de cet espace a été posé ou durci par un espace que vous ne gouvernez pas ; seuls lui ou un espace au-dessus de lui peuvent l'assouplir. |
| 403 | status_set_by_platform | PATCH /workspaces/{id} : le statut de cet espace a été posé par la plateforme ; cette clé ne peut pas l'assouplir. |
| 404 | end_user_not_found | external_user_id inconnu pour ce workspace (memory, delete, members). |
| 404 | workspace_not_found | Réponse opaque des routes de gouvernance : porte aussi bien sur un espace client qui n'existe pas que sur un espace client hors de votre sous-arbre. |
| 409 | workspace_has_no_human_admin | Aucun autre administrateur humain ne peut reprendre ce que le membre a créé pour l'espace, ou il en est le dernier administrateur : nommez-en un dans la console, une personne qui a son propre compte Gilbert (un compte inscrit par l'API ne compte pas), puis relancez (members). |
| 409 | workspace_suspended / workspace_closed | L'espace client CIBLÉ (pas votre clé) n'est pas actif : rotation de clé, réglage du routage par tâche. |
| 409 | workspace_inactive | L'espace client CIBLÉ, ou votre propre espace, est suspendu ou fermé : couleurs du panneau, portail de connexion. |
| 409 | panel_theme_owned_by_workspace | Les couleurs du panneau (panel-theme) ont été réglées par un espace placé sous le vôtre (le client, ou un espace parent intermédiaire placé entre vous et lui) : vous ne pouvez ni les remplacer ni les retirer, ni régler celles d'un espace placé sous l'espace qui les porte. Deux cas ne bloquent pas : les couleurs portées au-dessus de l'espace qui porte la marque blanche de l'espace visé, et celles réglées par un espace qui n'est plus au-dessus de celui qui les porte. La règle complète est décrite sous « Quelles couleurs bloquent un réglage ». |
| 409 | email_already_in_use | Cette personne a déjà confirmé une autre adresse pour cet espace (users) : l'adresse fournie ne peut plus la remplacer. |
| 409 | not_a_payer_root | {id} n'est pas le payeur effectif de son propre arbre : seul ce payeur peut être facturé (usage). |
| 413 | - | Message au-delà de 32 000 caractères. |
| 413 | text_too_large | Texte au-delà de 300 000 caractères, ou corps de requête trop volumineux (text/read). |
| 422 | panel_integration_unknown | panel_integration_id ne désigne pas une intégration de panneau de l'espace de la clé (panel/third-party-turns) : rien n'est effacé. |
| 429 | end_user_creation_rate_limited | Plus de 100 créations d'utilisateurs finaux en une heure pour ce workspace. |
| 500 | panel_forget_failed | Les échanges du panneau n'ont pas pu être effacés (panel/third-party-turns) : rejouez l'appel plus tard. |
| 502 | output_incomplete | Le modèle n'a pas terminé normalement (sortie coupée, filtrée ou interrompue). Relancer la même requête peut s'arrêter au même endroit : demandez une réponse plus courte (texte plus court sur text/read, consigne plus courte ou moins de champs dans output_schema sur documents/read). |
| 503 | central_key_usage_unavailable | La consommation du mois sur la clé centrale n'a pas pu être lue : relancez après Retry-After (documents/read, text/read). |
| 503 | internal_error | Le statut de l'espace de VOTRE clé est momentanément illisible (panne de lecture, pas un refus) : réessayez. |
Une fois le stream /chat démarré, la réponse HTTP est déjà à 200 : une erreur survenant en cours de route n'a pas de code HTTP à elle, elle arrive comme une ligne NDJSON de type error, qui clôt le stream.
{"type":"error","code":"workspace_llm_unavailable","message":"La cle LLM du workspace est invalide ou indisponible."}workspace_llm_unavailable : le workspace a sa propre clé de fournisseur LLM (BYOLLM) et le repli vers la clé centrale Gilbert est désactivé pour ce workspace ; l'appel échoue de façon explicite plutôt que de basculer silencieusement sur une clé qui n'est pas la vôtre.
central_key_not_billable : la clé centrale a refusé de payer pour l'espace pendant le tour, alors que la vérification d'avant le stream l'autorisait (budget atteint entre les deux). Même sens que le 402 du même nom.
{"type":"error","code":"structured_output_invalid","message":"La sortie ne correspond pas au schema fourni apres relance."}structured_output_invalid : après une relance, la sortie du modèle ne respecte toujours pas output_schema. Pas de deuxième retry automatique côté serveur.