# Documentation API — Qubly

> L'assistant IA de vos documents, hébergé en Europe.
> Ce fichier est la documentation complète de l'API, destinée à être donnée à un
> LLM ou lue par un développeur. Dernière mise à jour : 2026-09-05.

**Base URL :** `https://api.qubly.io`
**Console :** `https://app.qubly.io` · **Widget :** `https://widget.qubly.io/widget.js`
**Docs web :** `https://qubly.io/docs` · **Statut :** `https://qubly.io/statut`

---

## 1. Démarrage

Chaque appel part de la console : créez un compte gratuit sur
https://app.qubly.io — votre base « Base principale » et votre première clé API
sont créées automatiquement.

1. Créez le compte → la clé principale s'affiche (une seule fois).
2. Uploadez un premier document (console ou API).
3. Interrogez-le via `POST /v1/chat` ou le widget.

## 2. Authentification

Toutes les routes passent par l'en-tête `x-api-key`.
Les clés ont la forme `rag_<préfixe>_<secret>` ; seul leur SHA-256 est stocké,
elles sont révocables instantanément.

```bash
curl https://api.qubly.io/v1/knowledge-bases \
  -H "x-api-key: rag_votre_cle"
```

Une clé ne voit que les données de son tenant : chaque requête est isolée côté
base métier et vecteurs, même en cas de compromission.

### Périmètre des clés

| Périmètre | Création | Accès |
| --- | --- | --- |
| `FULL` (défaut) | `{ "scope": "FULL" }` | Tout, y compris la gestion (clés, bases, compte, facturation) |
| `CHAT` | `{ "scope": "CHAT" }` | Chat, recherche, upload et lecture — jamais la gestion (`403 forbidden_scope`) |

Une clé exposée publiquement (widget, page statique) doit être `CHAT` : même
volée, elle ne peut ni supprimer vos données ni gérer votre compte.

## 3. Chat — RAG en streaming

`POST /v1/chat`

Pipeline complet : vectorisation de la question → recherche dans la base →
réponse générée uniquement à partir des extraits retrouvés, avec sources.
La réponse arrive en Server-Sent Events.

### Corps de la requête

| Champ | Type | Description |
| --- | --- | --- |
| `knowledgeBaseId` | string | UUID de la base à interroger |
| `messages` | array | Historique ; le dernier message `user` est la question |
| `topK` | int (défaut 5) | Nombre d'extraits injectés dans le contexte |

### Événements SSE reçus

| Type | Payload | Quand |
| --- | --- | --- |
| `sources` | `documents[] {id, name, score, folder?, excerpt?}` | Avant la réponse : documents utilisés (score, chemin de dossier et extrait du passage) |
| `delta` | `content` | Fragment de réponse (markdown) |
| `skill` | `label` | Un Skill s'exécute (« Génération du classeur Excel… ») |
| `artifact` | `artifact {id, kind, name, mimeType, sizeBytes, svg?}` | Fichier généré par un Skill (voir ci-dessous) |
| `done` | `messageId` | Fin du flux |
| `error` | `code`, `message` | Erreur en cours de flux |

```bash
curl -N https://api.qubly.io/v1/chat \
  -H "x-api-key: rag_votre_cle" \
  -H "Content-Type: application/json" \
  -d '{
    "knowledgeBaseId": "90dfe604-…",
    "messages": [{ "role": "user", "content": "Quelles sont vos conditions de résiliation ?" }]
  }'

# data: {"type":"sources","documents":[{"id":"…","name":"CGV-2026.pdf","folder":"Contrats / 2026","score":0.87,"excerpt":"…extrait du passage utilisé…"}]}
# data: {"type":"delta","content":"Vos contrats sont résiliables"}
# data: {"type":"delta","content":" à tout moment après le 12ᵉ mois…"}
# data: {"type":"done","messageId":"…"}
```

Le flux continue d'écrire même après un code 200 : les erreurs de pipeline
arrivent en événement `error` (le flux est déjà ouvert). Consommez avec
`text/event-stream` et fermez la connexion à `done`.

### Skills : l'assistant génère des fichiers

Si l'utilisateur le demande (« fais-moi un rapport Word de ces CGV »,
« un graphique des volumes par mois »), l'assistant exécute un Skill et
joint le fichier produit à la conversation :

- `create_docx` — rapport Word (.docx) structuré en sections
- `create_xlsx` — classeur Excel (.xlsx), une feuille par tableau
- `create_chart` — graphique SVG (barres, courbe, camembert), affiché
  directement dans le chat (`artifact.svg` contient le SVG complet)

Les fichiers sont générés côté serveur, sans service tiers, et bornés
(2 exécutions par réponse, 3 fichiers, 2 Mo par fichier). Chaque artefact
se télécharge pendant 30 jours via :

```bash
curl https://api.qubly.io/v1/artifacts/<id>/download \
  -H "x-api-key: rag_votre_cle" -o rapport.docx
```

## 4. Recherche sémantique (sans LLM)

`POST /v1/search`

Le cœur du RAG sans la génération : vectorise la question et renvoie les
extraits les plus proches avec leur score. Idéal pour l'autocomplétion, la
recherche interne, ou pour bâtir votre propre couche LLM — réponse JSON
immédiate, aucun token de génération consommé.

| Champ | Type | Description |
| --- | --- | --- |
| `knowledgeBaseId` | string | UUID de la base à interroger |
| `query` | string (2–2000) | La question / requête en langage naturel |
| `topK` | int (défaut 6, max 50) | Nombre d'extraits renvoyés |

```bash
curl https://api.qubly.io/v1/search \
  -H "x-api-key: rag_votre_cle" \
  -H "Content-Type: application/json" \
  -d '{ "knowledgeBaseId": "90dfe604-…", "query": "conditions de résiliation", "topK": 3 }'

# → 200 { "results": [
#   { "documentId": "…", "documentName": "CGV-2026.pdf", "chunkIndex": 4,
#     "score": 0.87, "content": "Les contrats sont résiliables à tout moment…" } ] }
```

Burst : 30 recherches/min par tenant (`429` + `Retry-After`).

## 5. Upload de documents

`POST /v1/knowledge-bases/:kbId/documents`

Multipart, champ `file` — 25 Mo max. Formats acceptés :

| Famille | Formats | Traitement |
| --- | --- | --- |
| Bureautique | `.pdf` `.docx` `.xlsx` | Parsing natif ; OCR automatique pour les PDF scannés |
| Texte & données | `.txt` `.md` `.csv` `.json` `.xml` `.html` | Structure conservée (titres, lignes, cellules) |
| Images | `.png` `.jpg` `.webp` `.gif` `.bmp` `.tiff` `.heic` | OCR Mistral (HEIC converti automatiquement) |
| Audio | `.mp3` `.wav` `.m4a` `.ogg` `.flac`… | Transcription Voxtral |
| Vidéo | `.mp4` `.webm` `.mov` `.mkv` `.avi` | Piste audio extraite (ffmpeg) puis transcription |

L'ingestion est asynchrone : la réponse est immédiate (`202`, statut
`PENDING`), le document est ensuite parsé, découpé, vectorisé et indexé par le
worker.

```bash
curl https://api.qubly.io/v1/knowledge-bases/90dfe604-…/documents \
  -H "x-api-key: rag_votre_cle" \
  -F "file=@CGV-2026.pdf;type=application/pdf"
# → 202 { "document": { "id": "…", "status": "PENDING", … } }
```

### Import par URL

`POST /v1/knowledge-bases/:kbId/import-url`

Importe une page web : le worker la scrape (rendu HTML nettoyé, tableaux et
titres conservés) et indexe son contenu. Seules les URL publiques `http/https`
sont acceptées (garde anti-SSRF : les adresses internes sont refusées).

```bash
curl https://api.qubly.io/v1/knowledge-bases/90dfe604-…/import-url \
  -H "x-api-key: rag_votre_cle" -H "Content-Type: application/json" \
  -d '{"url": "https://exemple.fr/article"}'
# → 202 { "document": { "format": "URL", "status": "PENDING", … } }
```

### Suivi du statut

`GET /v1/knowledge-bases/:kbId/documents/:documentId`

| Statut | Sens |
| --- | --- |
| `PENDING` | En file d'ingestion |
| `INDEXED` | Vectorisé et interrogeable (avec chunkCount) |
| `FAILED` | Échec (champ error explicatif) |

## 6. Bases de connaissances

- `GET /v1/knowledge-bases` — liste des bases du tenant.
- `POST /v1/knowledge-bases` — `{ "name": "Support technique" }`. À
  l'inscription, la première base est créée avec trois documents de prise en
  main déjà indexés.
- `PATCH /v1/knowledge-bases/:kbId` — `{ "name": "…", "autoClassify": true }` :
  renomme la base ou active le rangement automatique (les nouveaux documents
  sont classés dans l'arborescence par l'IA après indexation).
  Champ `systemPrompt` (texte, 2 000 caractères max, `null` pour retirer) :
  consigne propre à la base — ton, format, règles métier — appliquée au chat
  de test et au widget, en complément des règles de réponse documentée.
- `GET /v1/knowledge-bases/:kbId/suggestions` — trois questions de
  démonstration générées depuis le contenu réel de la base (cache 1 h).
  Idéal pour amorcer un widget : `{ suggestions: string[] }`.
- `DELETE /v1/knowledge-bases/:kbId` — supprime la base : purge de tous ses
  vecteurs puis cascade sur les documents.

## 7. Documents

- `GET /v1/knowledge-bases/:kbId/documents?limit=50&offset=0` — liste paginée
  (`limit` ≤ 100). Filtre optionnel `?folderId=` (UUID d'un dossier, ou `root`
  pour les non rangés). Réponse : `{ documents, total, limit, offset }`.
- `POST /v1/knowledge-bases/:kbId/documents/:documentId/retry` — relance
  l'indexation d'un document `FAILED` : les vecteurs du passage précédent
  sont purgés, la page web est re-scrapée ou le binaire (conservé 7 jours
  après un échec) refile. Réponse `202` avec le document repassé `PENDING`.
- `DELETE /v1/knowledge-bases/:kbId/documents/:documentId` — supprime le
  document et purge ses vecteurs immédiatement.

## 8. Dossiers et rangement automatique

Chaque base possède une arborescence de dossiers (5 niveaux max). Les documents
non rangés restent « à la racine ». L'IA peut les classer automatiquement
(nom + extrait du contenu), et le chat connaît l'arborescence : il peut dire où
se trouve un document.

- `GET /v1/knowledge-bases/:kbId/folders` — arborescence complète avec compteurs :
  `{ tree: [{ id, name, documentCount, children: […] }], rootDocumentCount }`.
- `POST /v1/knowledge-bases/:kbId/folders` — `{ "name": "Factures", "parentId": null }`.
- `PATCH /v1/folders/:folderId` — `{ "name": "…", "parentId": "…" }` (cycles refusés).
- `DELETE /v1/folders/:folderId` — supprime le dossier : documents et sous-dossiers
  remontent d'un niveau (aucun document n'est supprimé).
- `PATCH /v1/knowledge-bases/:kbId/documents/:documentId` — `{ "folderId": "…" | null }` :
  déplace un document (null = racine). Périmètre `CHAT` suffisant.
- `POST /v1/knowledge-bases/:kbId/classify` — rangement automatique :
  `{ "all": false }` classe les non rangés (toute la base avec `true`), par lots
  de 30, en réutilisant les dossiers existants. Réponse :
  `{ summary: { classified, foldersCreated, skipped } }`. Périmètre `FULL`.
- Upload : champ form optionnel `folderId` pour ranger le document dès l'upload.

## 9. Clés API

- `GET /v1/api-keys`
- `POST /v1/api-keys` — `{ "name": "Site vitrine", "scope": "CHAT" }`
- `DELETE /v1/api-keys/:keyId`

10 clés actives max par tenant. La clé créée s'affiche une seule fois (seul le
hash est conservé). Créez une clé dédiée par intégration — le widget, par
exemple, doit avoir la sienne (périmètre `CHAT`), révocable indépendamment.
Même la dernière clé active peut être révoquée : la console reste accessible
par email et mot de passe. Ces routes exigent une clé `FULL` ou la session
console.

## 10. Usage & quotas

`GET /v1/usage`

Consommation courante et limites du plan (bases, documents, chunks, chats et
fichiers Skills du jour). Au-delà : `402 quota_exceeded`. Burst : 10 chats/min
et 10 uploads/min par tenant (`429` + en-tête `Retry-After`).

`GET /v1/usage/daily?days=14` — historique d'usage par jour calendaire
(1 à 90 jours) : `{ series: [{ date, chats, uploads, skills }] }` (un point
par jour, jours vides compris) et `chatByKnowledgeBase` (top 8 des bases les
plus interrogées sur la fenêtre).

| Plan | Prix | Bases | Documents | Chunks | Chats/jour | Fichiers Skills/jour |
| --- | --- | --- | --- | --- | --- | --- |
| Free | 0 € | 3 | 50 | 1 500 | 50 | 5 |
| Starter | 19 €/mois | 5 | 300 | 15 000 | 500 | 50 |
| Pro | 49 €/mois | 20 | 1 500 | 75 000 | 2 000 | 250 |
| Enterprise | 199 €/mois | 100 | 10 000 | 500 000 | 20 000 | 2 000 |

## 11. Inférence — choisir son moteur (souveraineté)

Par défaut, les réponses du chat sont générées par l'inférence partagée Qubly
(Mistral AI, France). Vous pouvez brancher **votre propre endpoint
OpenAI-compatible** : OVHcloud AI Endpoints, Scaleway Generative APIs, ou
votre serveur (vLLM, Ollama, LM Studio). Vos questions et le contexte de vos
documents transitent alors uniquement vers l'inférence choisie, avec votre clé.

`GET /v1/llm-settings` — réglages courants (la clé n'est jamais renvoyée).

```json
{
  "provider": "platform",          // "platform" | "custom"
  "baseUrl": null,                  // rempli si "custom"
  "model": null,
  "hasApiKey": false,
  "platformModel": "mistral-small-latest"
}
```

`PUT /v1/llm-settings` — enregistre le choix. `POST /v1/llm-settings/test`
teste la connexion candidate (completion minimale) avant d'enregistrer.

```bash
curl -X PUT https://api.qubly.io/v1/llm-settings \
  -H "x-api-key: rag_..." -H "Content-Type: application/json" \
  -d '{
    "provider": "custom",
    "baseUrl": "https://ai.endpoints.ovhai.net/v1",
    "model": "qwen2.5-72b-instruct",
    "apiKey": "votre-cle-ovh"
  }'
```

Règles : `https` obligatoire (le `http` simple est accepté pour `localhost`
et les réseaux privés — serveur on-premise) ; périmètre de clé `FULL` requis ;
6 tests de connexion/min. La clé du provider est stockée côté serveur et
jamais réaffichée.

**Ce qui change** : la génération des réponses et des fichiers (Skills).
**Ce qui ne change pas** : la vectorisation (embeddings), l'OCR et le
classement automatique restent sur l'inférence partagée — vos documents déjà
indexés ne sont ni réindexés ni déplacés.

## 12. Widget

Pas une ligne d'intégration à écrire — un `<script>` sur votre site :

```html
<script
  src="https://widget.qubly.io/widget.js"
  data-api-url="https://api.qubly.io"
  data-api-key="rag_cle_dediee_widget"
  data-kb-id="90dfe604-…"
  data-title="Assistant Qubly"
  data-greeting="Bonjour — comment puis-je vous aider ?"
  data-placeholder="Posez votre question…"
  data-color="#4f46e5"
  data-position="right"
  async
></script>
```

- `data-color` : couleur d'accent (défaut zinc).
- `data-position` : `left` | `right`.
- `data-theme` : `light` | `dark` | `auto` (défaut `auto` — suit le mode
  clair/sombre du système du visiteur).
- `data-title`, `data-greeting`, `data-placeholder` : textes.
- `data-upload="false"` : retire la zone de dépôt de documents (widget en
  consultation seule — recommandé pour un site public).
- `data-skills="false"` : désactive la génération de fichiers (Skills) sur ce
  widget — utile pour un widget public dont vous ne voulez pas qu'il consomme
  votre quota de générations.
- `data-source="mon-site"` : étiquette de suivi — les questions posées depuis
  ce widget remontent dans les logs de l'API (en-tête `x-widget-source`),
  pour suivre ce que vos visiteurs demandent. Sans elle, aucune question
  n'est journalisée.

Le panneau est redimensionnable par son angle supérieur (double-clic pour
revenir à la taille par défaut) ; l'extrait affiché au clic sur une source
est rendu en markdown (tableaux, listes).

42 Ko (14,9 gzip), rendu markdown, aucune dépendance. La conversation du
visiteur est conservée dans son navigateur (localStorage, par base de
connaissances) : il retrouve son historique en revenant sur la page, et peut
l'effacer d'un clic. Démo : https://widget.qubly.io/demo.html

## 13. Codes d'erreur

| HTTP | error | Cause |
| --- | --- | --- |
| 400 | `bad_request` | Corps invalide (détails dans `details`) |
| 401 | `unauthorized` | Clé absente, invalide ou révoquée |
| 402 | `quota_exceeded` | Quota du plan atteint |
| 403 | `forbidden_scope` | Route de gestion appelée avec une clé `CHAT` |
| 404 | `not_found` | Ressource inconnue ou hors de votre tenant |
| 413 | `payload_too_large` | Fichier > 25 Mo |
| 415 | `unsupported_media_type` | Format non supporté (bureautique, texte, données, image, audio, vidéo) |
| 429 | `rate_limited` | Burst dépassé — respecter `Retry-After` |
| 501 | `billing_not_configured` | Fonctionnalité non activée sur l'instance |
