{
  "name": "Qubly API",
  "description": "L'assistant IA de vos documents, hébergé en Europe. Documentation machine-lisible de l'API Qubly — base URL, authentification, endpoints, formats acceptés, erreurs. Pensée pour être chargée par un LLM ou un outil d'intégration.",
  "base_url": "https://api.qubly.io",
  "console_url": "https://app.qubly.io",
  "widget_url": "https://widget.qubly.io/widget.js",
  "docs_url": "https://qubly.io/docs",
  "status_url": "https://qubly.io/statut",
  "updated": "2026-09-06",
  "auth": {
    "header": "x-api-key",
    "key_format": "rag_<prefix>_<secret>",
    "storage": "SHA-256 uniquement, révocable immédiatement",
    "isolation": "Chaque clé ne voit que les données de son tenant (base métier + vecteurs)",
    "scopes": {
      "FULL": "Tout, y compris la gestion (clés, bases, compte, facturation)",
      "CHAT": "Chat, recherche, upload et lecture — jamais la gestion (403 forbidden_scope)"
    },
    "note": "Une clé exposée publiquement (widget) doit être CHAT."
  },
  "endpoints": [
    {
      "method": "POST",
      "path": "/v1/chat",
      "summary": "RAG en streaming SSE : vectorisation → recherche → réponse générée uniquement à partir des extraits, avec sources",
      "request": {
        "knowledgeBaseId": "string (UUID)",
        "messages": "array {role: 'user'|'assistant', content: string} — le dernier user est la question",
        "topK": "int, défaut 5"
      },
      "sse_events": [
        {
          "type": "sources",
          "payload": "documents[] {id, name, score, folder?, excerpt?}"
        },
        {
          "type": "delta",
          "payload": "content (fragment markdown)"
        },
        {
          "type": "skill",
          "payload": "label — un Skill s'exécute (création de fichier demandée par l'utilisateur)"
        },
        {
          "type": "artifact",
          "payload": "artifact {id, kind: 'docx'|'xlsx'|'svg', name, mimeType, sizeBytes, svg?} — fichier généré ; svg contient le graphique complet pour rendu inline"
        },
        {
          "type": "done",
          "payload": "messageId"
        },
        {
          "type": "error",
          "payload": "code, message"
        }
      ],
      "skills": "Sur demande explicite (« génère un rapport Word », « un graphique »), l'assistant produit des fichiers côté serveur (sans service tiers) : create_docx, create_xlsx, create_chart (barres/courbe/camembert en SVG). Bornes : 2 exécutions par réponse, 3 fichiers, 2 Mo par fichier.",
      "rate_limit": "10 chats/min par tenant (429 + Retry-After)"
    },
    {
      "method": "GET",
      "path": "/v1/artifacts/:id/download",
      "summary": "Télécharge un fichier généré par un Skill du chat (rétention 30 jours)",
      "auth": "x-api-key ou session console ; l'artefact appartient au tenant de l'appelant",
      "response": "binaire (Content-Type + Content-Disposition attachment)"
    },
    {
      "method": "POST",
      "path": "/v1/search",
      "summary": "Recherche sémantique sans LLM : extraits top-K scorés, JSON immédiat",
      "request": {
        "knowledgeBaseId": "string (UUID)",
        "query": "string (2–2000)",
        "topK": "int, défaut 6, max 50"
      },
      "response": {
        "results": "array {documentId, documentName, chunkIndex, score, content}"
      },
      "rate_limit": "30 recherches/min par tenant"
    },
    {
      "method": "POST",
      "path": "/v1/knowledge-bases/:kbId/documents",
      "summary": "Upload multipart (champ file, 25 Mo max) — asynchrone : 202 + statut PENDING",
      "accepted_formats": {
        "bureautique": [
          ".pdf",
          ".docx",
          ".xlsx"
        ],
        "texte_donnees": [
          ".txt",
          ".md",
          ".csv",
          ".json",
          ".xml",
          ".html"
        ],
        "images": [
          ".png",
          ".jpg",
          ".webp",
          ".gif",
          ".bmp",
          ".tiff",
          ".heic"
        ],
        "audio": [
          ".mp3",
          ".wav",
          ".m4a",
          ".ogg",
          ".flac"
        ],
        "video": [
          ".mp4",
          ".webm",
          ".mov",
          ".mkv",
          ".avi"
        ]
      },
      "processing": "Parsing natif ; OCR auto pour PDF scannés et images ; transcription Voxtral pour audio ; ffmpeg + transcription pour vidéo",
      "rate_limit": "10 uploads/min par tenant"
    },
    {
      "method": "POST",
      "path": "/v1/knowledge-bases/:kbId/import-url",
      "summary": "Scrape et indexe une page web publique",
      "request": {
        "url": "string http/https public (anti-SSRF : adresses internes refusées)"
      },
      "response": "202 { document: { format: 'URL', status: 'PENDING' } }"
    },
    {
      "method": "GET",
      "path": "/v1/knowledge-bases/:kbId/documents/:documentId",
      "summary": "Statut d'ingestion",
      "statuses": [
        "PENDING (en file)",
        "INDEXED (interrogeable, avec chunkCount)",
        "FAILED (champ error)"
      ]
    },
    {
      "method": "GET",
      "path": "/v1/knowledge-bases",
      "summary": "Liste des bases du tenant"
    },
    {
      "method": "POST",
      "path": "/v1/knowledge-bases",
      "summary": "Créer une base",
      "request": {
        "name": "string"
      }
    },
    {
      "method": "GET",
      "path": "/v1/knowledge-bases/:kbId/suggestions",
      "summary": "Trois questions de démonstration générées depuis le contenu réel de la base (cache 1 h, invalidé à l'indexation d'un nouveau document)",
      "response": "{ suggestions: string[] } — questions auxquelles la base sait répondre"
    },
    {
      "method": "PATCH",
      "path": "/v1/knowledge-bases/:kbId",
      "summary": "Renommer la base, rangement automatique ou consigne système",
      "request": {
        "name": "string (optionnel)",
        "autoClassify": "boolean (optionnel)",
        "systemPrompt": "string | null (optionnel, 2000 car.) — consigne propre à la base (ton, format, règles métier), appliquée au chat et au widget ; null la retire"
      }
    },
    {
      "method": "DELETE",
      "path": "/v1/knowledge-bases/:kbId",
      "summary": "Supprimer une base (purge vecteurs + cascade documents)"
    },
    {
      "method": "GET",
      "path": "/v1/knowledge-bases/:kbId/documents",
      "summary": "Liste paginée des documents",
      "query": "limit (≤100), offset",
      "response": {
        "documents": "array",
        "total": "int",
        "limit": "int",
        "offset": "int"
      }
    },
    {
      "method": "DELETE",
      "path": "/v1/knowledge-bases/:kbId/documents/:documentId",
      "summary": "Supprimer un document (purge vecteurs immédiate)"
    },
    {
      "method": "POST",
      "path": "/v1/knowledge-bases/:kbId/documents/:documentId/retry",
      "summary": "Relancer l'indexation d'un document FAILED : purge des vecteurs précédents, re-scrap (URL) ou binaire refile (conservé 7 jours après un échec)",
      "response": "202 { document } (statut PENDING) — 409 si le document n'est pas en échec ou si le fichier source a expiré"
    },
    {
      "method": "GET",
      "path": "/v1/knowledge-bases/:kbId/folders",
      "summary": "Arborescence de dossiers de la base (5 niveaux max) avec compteurs",
      "response": {
        "tree": "array {id, name, documentCount, children}",
        "rootDocumentCount": "int (documents non rangés)"
      }
    },
    {
      "method": "POST",
      "path": "/v1/knowledge-bases/:kbId/folders",
      "summary": "Créer un dossier",
      "request": {
        "name": "string",
        "parentId": "UUID | null"
      },
      "auth_required": "FULL"
    },
    {
      "method": "PATCH",
      "path": "/v1/folders/:folderId",
      "summary": "Renommer ou déplacer un dossier (cycles refusés)",
      "request": {
        "name": "string",
        "parentId": "UUID | null"
      },
      "auth_required": "FULL"
    },
    {
      "method": "DELETE",
      "path": "/v1/folders/:folderId",
      "summary": "Supprimer un dossier : documents et sous-dossiers remontent d'un niveau (aucun document supprimé)",
      "auth_required": "FULL"
    },
    {
      "method": "PATCH",
      "path": "/v1/knowledge-bases/:kbId/documents/:documentId",
      "summary": "Déplacer un document (null = racine) ; accepte aussi les clés CHAT",
      "request": {
        "folderId": "UUID | null"
      }
    },
    {
      "method": "POST",
      "path": "/v1/knowledge-bases/:kbId/classify",
      "summary": "Rangement automatique IA par lots de 30 (nom + extrait), réutilise les dossiers existants",
      "request": {
        "all": "boolean — true pour reclasser toute la base"
      },
      "response": {
        "summary": "{classified, foldersCreated, skipped}"
      },
      "auth_required": "FULL"
    },
    {
      "method": "GET",
      "path": "/v1/api-keys",
      "summary": "Lister les clés (exige FULL ou session)"
    },
    {
      "method": "POST",
      "path": "/v1/api-keys",
      "summary": "Créer une clé — {name, scope: FULL|CHAT} ; le secret s'affiche UNE SEULE fois ; 10 clés actives max",
      "auth_required": "FULL"
    },
    {
      "method": "DELETE",
      "path": "/v1/api-keys/:keyId",
      "summary": "Révoquer une clé (même la dernière : la console reste accessible par email/mot de passe)",
      "auth_required": "FULL"
    },
    {
      "method": "GET",
      "path": "/v1/usage",
      "summary": "Consommation courante et limites du plan"
    },
    {
      "method": "GET",
      "path": "/v1/usage/daily",
      "summary": "Historique d'usage par jour calendaire (param days, 1 à 90, défaut 14) — nourrit les graphes de la console",
      "response": "{ days, series: [{ date, chats, uploads, skills }], chatByKnowledgeBase: [{ knowledgeBaseId, name, chats }] }"
    },
    {
      "method": "GET",
      "path": "/v1/llm-settings",
      "summary": "Réglages d'inférence courants — {provider: platform|custom, baseUrl, model, hasApiKey, platformModel}. La clé du provider n'est jamais renvoyée. Périmètre FULL requis."
    },
    {
      "method": "PUT",
      "path": "/v1/llm-settings",
      "summary": "Choisir son moteur d'inférence (souveraineté) — {provider:'platform'} ou {provider:'custom', baseUrl, model, apiKey}. Endpoint OpenAI-compatible : OVHcloud, Scaleway, vLLM… https obligatoire (http local/réseau privé accepté). Change la génération du chat et des Skills ; embeddings/OCR restent sur l'inférence partagée (aucune réindexation)."
    },
    {
      "method": "POST",
      "path": "/v1/llm-settings/test",
      "summary": "Tester une connexion d'inférence candidate avant enregistrement — {provider, baseUrl?, model?, apiKey?} → {ok, latencyMs, model, reply} ou 502 avec le détail. 6 tests/min."
    }
  ],
  "plans": {
    "FREE": {
      "price_eur_per_month": 0,
      "knowledge_bases": 3,
      "documents": 50,
      "chunks": 1500,
      "chats_per_day": 50,
      "skill_artifacts_per_day": 5
    },
    "STARTER": {
      "price_eur_per_month": 19,
      "knowledge_bases": 5,
      "documents": 300,
      "chunks": 15000,
      "chats_per_day": 500,
      "skill_artifacts_per_day": 50
    },
    "PRO": {
      "price_eur_per_month": 49,
      "knowledge_bases": 20,
      "documents": 1500,
      "chunks": 75000,
      "chats_per_day": 2000,
      "skill_artifacts_per_day": 250
    },
    "ENTERPRISE": {
      "price_eur_per_month": 199,
      "knowledge_bases": 100,
      "documents": 10000,
      "chunks": 500000,
      "chats_per_day": 20000,
      "skill_artifacts_per_day": 2000
    }
  },
  "widget": {
    "install": "<script src=\"https://widget.qubly.io/widget.js\" data-api-url=\"https://api.qubly.io\" data-api-key=\"rag_...\" data-kb-id=\"UUID\" async></script>",
    "attributes": {
      "data-api-url": "https://api.qubly.io",
      "data-api-key": "clé dédiée CHAT",
      "data-kb-id": "UUID de la base",
      "data-title": "titre du panneau",
      "data-greeting": "message d'accueil",
      "data-placeholder": "placeholder de l'input",
      "data-color": "couleur d'accent (défaut zinc)",
      "data-position": "left | right",
      "data-theme": "light | dark | auto (défaut : auto — suit le mode du système du visiteur)",
      "data-upload": "false pour retirer la zone de dépôt de documents (défaut : active)",
      "data-skills": "false pour désactiver la génération de fichiers (Skills) sur ce widget (défaut : active)",
      "data-source": "étiquette de suivi des questions (en-tête x-widget-source, aucune question journalisée sans elle)"
    },
    "notes": "42 Ko (14,9 gzip), rendu markdown, aucune dépendance. Conversation persistée dans le localStorage du visiteur (par base), effaçable d'un clic. Panneau redimensionnable (angle supérieur, double-clic = défaut) ; extraits des sources rendus en markdown ; thème clair/sombre."
  },
  "errors": [
    {
      "http": 400,
      "code": "bad_request",
      "cause": "Corps invalide (détails dans details)"
    },
    {
      "http": 401,
      "code": "unauthorized",
      "cause": "Clé absente, invalide ou révoquée"
    },
    {
      "http": 402,
      "code": "quota_exceeded",
      "cause": "Quota du plan atteint"
    },
    {
      "http": 403,
      "code": "forbidden_scope",
      "cause": "Route de gestion appelée avec une clé CHAT"
    },
    {
      "http": 404,
      "code": "not_found",
      "cause": "Ressource inconnue ou hors de votre tenant"
    },
    {
      "http": 413,
      "code": "payload_too_large",
      "cause": "Fichier > 25 Mo"
    },
    {
      "http": 415,
      "code": "unsupported_media_type",
      "cause": "Format non supporté"
    },
    {
      "http": 429,
      "code": "rate_limited",
      "cause": "Burst dépassé — respecter Retry-After"
    },
    {
      "http": 501,
      "code": "billing_not_configured",
      "cause": "Fonctionnalité non activée sur l'instance"
    }
  ]
}
