{"openapi":"3.1.0","info":{"title":"Gilbert API","description":"\n# Gilbert API\n\nL'API Gilbert permet aux intégrations tierces de récupérer et de piloter les **réunions**,\nleurs **transcriptions** et leurs **synthèses** d'un utilisateur. Vous pouvez alimenter un\nCRM, un data warehouse, un outil de productivité (Notion, Slack, Zapier, n8n, Make), mais\naussi **uploader de l'audio**, **éditer des réunions**, **rechercher** dans l'historique\net **recevoir des notifications push** via **webhooks**.\n\n**Version stable** : `v1.0.0`. Les changements incompatibles donneront lieu à une\nversion majeure `v2` sans casser `v1`.\n\n---\n\n## Quickstart\n\n1. Un administrateur Gilbert vous génère une clé API depuis la console d'administration.\n2. Vérifier la validité de la clé :\n\n```bash\ncurl -s https://gilbert-assistant.ovh/api/v1/me \\\n  -H \"Authorization: Bearer glbrt_live_XXXXXXXXXXXXXXXXXXXXXXXX\"\n```\n\n3. Lister les 50 dernières réunions terminées :\n\n```bash\ncurl -s \"https://gilbert-assistant.ovh/api/v1/meetings?status=completed&per_page=50\" \\\n  -H \"Authorization: Bearer glbrt_live_XXXXXXXXXXXXXXXXXXXXXXXX\"\n```\n\n---\n\n## Authentification\n\nToutes les requêtes doivent inclure la clé API dans le header `Authorization` au format\n**Bearer token** :\n\n```\nAuthorization: Bearer glbrt_live_XXXXXXXXXXXXXXXXXXXXXXXX\n```\n\n**Alternative** : header `X-API-Key` (pratique pour certains outils no-code) :\n\n```\nX-API-Key: glbrt_live_XXXXXXXXXXXXXXXXXXXXXXXX\n```\n\n### Règles de sécurité\n\n- La clé est générée **uniquement par un administrateur Gilbert**. Elle n'est **affichée qu'une seule fois** à sa création ; il est impossible de la retrouver ensuite (une rotation est nécessaire).\n- **Isolation utilisateur** : chaque clé donne accès aux réunions **d'un seul utilisateur** (son propriétaire, plus les réunions qui lui sont partagées). Une clé ne peut pas voir les données d'autres utilisateurs.\n- **Ne jamais commit** la clé dans un dépôt public. Utilisez des variables d'environnement ou un gestionnaire de secrets (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, 1Password, Doppler).\n- **Révocation immédiate** possible par l'administrateur à tout moment, sans préavis.\n- **Expiration** : par défaut 1 an. L'administrateur peut créer des clés 30 / 90 jours ou sans expiration.\n- Rotation recommandée tous les 6 mois.\n\n### Formats de clés\n\n| Préfixe | Usage |\n| --- | --- |\n| `glbrt_live_` | Clé de production (environnement réel) |\n| `glbrt_test_` | Clé de test (même API, permet d'identifier l'usage dans les logs) |\n\n---\n\n## Base URL\n\n```\nhttps://gilbert-assistant.ovh/api/v1\n```\n\nToutes les requêtes doivent utiliser **HTTPS**. Les appels HTTP seront refusés.\n\n---\n\n## Tiers et scopes\n\nChaque clé API est rattachée à un **tier** (`free` / `pro` / `enterprise`) qui détermine\nses rate limits, la taille max des fichiers upload et les **scopes** (capabilities)\nautorisés.\n\n| Tier | req/min | req/jour | Upload audio / jour | Max fichier | Webhooks max | Scopes autorisés |\n| --- | --- | --- | --- | --- | --- | --- |\n| **free** | 60 | 5 000 | — | — | 0 | `meetings:read` |\n| **pro** | 300 | 50 000 | 600 min | 500 MB | 3 | `meetings:read`, `meetings:write`, `meetings:upload`, `speakers:write`, `webhooks:manage` |\n| **enterprise** | 1 000 | 500 000 | 10 000 min | 2 GB | 20 | tous les scopes, incluant `meetings:delete` |\n\n### Scopes (capabilities) disponibles\n\n| Scope | Description | Endpoints concernés |\n| --- | --- | --- |\n| `meetings:read` | Lecture des réunions, transcripts et synthèses | `GET /meetings*`, `GET /me` |\n| `meetings:write` | Édition titre et folder d'une réunion | `PATCH /meetings/{id}` |\n| `meetings:upload` | Upload d'audio et création de réunion | `POST /meetings/upload` |\n| `meetings:delete` | Suppression définitive | `DELETE /meetings/{id}` |\n| `speakers:write` | Renommage des locuteurs détectés | `PATCH /meetings/{id}/speakers/{speaker_id}` |\n| `webhooks:manage` | (Réservé — gestion admin uniquement pour l'instant) | — |\n\nUn appel avec un scope manquant renvoie **`403 Forbidden`**.\n\n---\n\n## Rate limits\n\nLes limites dépendent du **tier** de la clé (voir tableau ci-dessus). Par défaut :\n\n| Fenêtre | Free | Pro | Enterprise |\n| --- | --- | --- | --- |\n| 1 minute | 60 | 300 | 1 000 |\n| 24 heures | 5 000 | 50 000 | 500 000 |\n\nChaque réponse inclut les compteurs courants dans ses headers :\n\n```\nX-RateLimit-Limit-Minute: 60\nX-RateLimit-Remaining-Minute: 57\nX-RateLimit-Reset-Minute: 1745432400\nX-RateLimit-Limit-Day: 5000\nX-RateLimit-Remaining-Day: 4783\nX-RateLimit-Reset-Day: 1745472000\n```\n\nLes valeurs `Reset-*` sont des timestamps Unix (epoch secondes) qui indiquent quand le\ncompteur sera réinitialisé.\n\n### Dépassement\n\nEn cas de dépassement, l'API renvoie `429 Too Many Requests` avec le header `Retry-After`\nindiquant le nombre de secondes à attendre. Implémentez un backoff exponentiel :\n\n```python\nimport time, requests\n\ndef call_with_backoff(url, headers, max_retries=5):\n    for attempt in range(max_retries):\n        r = requests.get(url, headers=headers)\n        if r.status_code == 429:\n            wait = int(r.headers.get(\"Retry-After\", 2 ** attempt))\n            time.sleep(wait)\n            continue\n        return r\n    r.raise_for_status()\n```\n\n---\n\n## Format des réponses\n\n### Listes paginées\n\nToutes les listes utilisent le format enveloppe standard :\n\n```json\n{\n  \"data\": [ { /* objets */ } ],\n  \"meta\": {\n    \"page\": 1,\n    \"per_page\": 50,\n    \"total\": 247,\n    \"total_pages\": 5\n  }\n}\n```\n\n### Ressources individuelles\n\nRetournées directement comme objet JSON, sans enveloppe :\n\n```json\n{\n  \"id\": \"84ddd6d8-274d-485d-8419-fe2e37b241e6\",\n  \"title\": \"Réunion du 17 avril\",\n  \"created_at\": \"2026-04-17T14:30:00Z\"\n}\n```\n\n### Conventions\n\n- Tous les **timestamps** sont en **ISO 8601 UTC** (`2026-04-17T14:30:00Z`)\n- Les **identifiants** sont des **UUID v4**\n- Les **champs null** indiquent une absence de donnée (exemple : synthèse pas encore générée)\n- Les **chaînes** sont en UTF-8\n- Les **durées** sont exprimées en **secondes**\n\n---\n\n## Codes d'erreur\n\n| Code | Signification | Action recommandée |\n| --- | --- | --- |\n| `200` | Succès | — |\n| `400` | Paramètre invalide (format de date, UUID malformé) | Vérifier les paramètres de la requête |\n| `401` | Clé API manquante, invalide, révoquée ou expirée | Régénérer une clé auprès de l'administrateur |\n| `403` | Scope insuffisant pour cette ressource | Demander les scopes nécessaires à l'administrateur |\n| `404` | Ressource introuvable (ou appartient à un autre utilisateur) | Vérifier l'identifiant |\n| `422` | Données non valides (exemple : `status` hors enum) | Corriger selon le schéma |\n| `429` | Rate limit dépassé | Respecter `Retry-After`, backoff exponentiel |\n| `500` | Erreur serveur | Retry avec backoff. Si le problème persiste, contactez le support. |\n\n### Format d'erreur\n\n```json\n{\n  \"detail\": \"Description humaine de l'erreur\"\n}\n```\n\nCertaines erreurs de validation (code 422) incluent plus de détails structurés :\n\n```json\n{\n  \"detail\": [\n    {\n      \"loc\": [\"query\", \"page\"],\n      \"msg\": \"ensure this value is greater than or equal to 1\",\n      \"type\": \"value_error.number.not_ge\"\n    }\n  ]\n}\n```\n\n---\n\n## Filtres et pagination\n\n### Pagination\n\nTous les endpoints de liste acceptent les paramètres suivants :\n\n- `page` : numéro de page (1-indexed, défaut 1, minimum 1)\n- `per_page` : nombre d'éléments par page (défaut 50, minimum 1, **maximum 100**)\n\n### Filtres sur `/meetings`\n\n| Paramètre | Format | Description |\n| --- | --- | --- |\n| `status` | `pending` \\| `processing` \\| `completed` \\| `error` | Filtre sur `transcript_status` |\n| `folder_id` | UUID | Restreint à un dossier précis |\n| `from` | ISO 8601 | Borne inférieure sur `created_at` (exemple : `2026-01-01`) |\n| `to` | ISO 8601 | Borne supérieure sur `created_at` |\n\nExemple :\n\n```bash\nGET /api/v1/meetings?status=completed&from=2026-01-01&to=2026-04-01&per_page=100\n```\n\n---\n\n## Formats de données\n\n### `transcript_text`\n\nChaîne texte avec les locuteurs préfixés :\n\n```\nSpeaker 0: Bonjour tout le monde, on commence la réunion.\nSpeaker 1: D'accord, je prends des notes.\nSpeaker 0: Aujourd'hui on va parler de la roadmap Q3.\n```\n\n**Limitations de la v1** :\n\n- Pas de timestamps word-level (prévu pour `v1.1`)\n- Les noms personnalisés des locuteurs ne sont pas injectés (prévu pour `v1.1`)\n\n### `summary_text`\n\nMarkdown structuré que vous pouvez afficher directement dans votre interface ou convertir\nen HTML / PDF :\n\n```markdown\n## Points clés\n- Décision prise sur le sujet X\n- Action assignée à Martin pour le 25 avril\n\n## Détails\n...\n```\n\n### `recording_client`\n\nIndique depuis quelle plateforme l'enregistrement a été effectué : `web`, `desktop`,\n`ios`, `android`.\n\n---\n\n## Sécurité\n\n- **TLS 1.2+** obligatoire (HTTPS only)\n- Les clés ne sont **jamais loguées** côté serveur (headers Authorization redactés)\n- L'API ne retourne jamais les données d'autres utilisateurs, même si l'identifiant est deviné\n- Rotation de clé régulière recommandée (tous les 6 mois)\n\n---\n\n## Recherche full-text\n\n`GET /meetings/search?q=...` effectue une recherche **plein texte** dans les titres,\ntranscriptions et synthèses de l'utilisateur. Basée sur l'index PostgreSQL `tsvector`\navec pondération titre > transcript > synthèse. Chaque résultat renvoie un `rank`\n(pertinence) et un `snippet` HTML (`<b>mot</b>`) pour le highlight.\n\n```bash\ncurl \"https://gilbert-assistant.ovh/api/v1/meetings/search?q=budget+Q3\" \\\n  -H \"Authorization: Bearer glbrt_live_...\"\n```\n\n---\n\n## Upload audio\n\n`POST /meetings/upload` accepte un `multipart/form-data` avec un champ `file` (audio)\net un `title` optionnel. La transcription est déclenchée automatiquement.\n\n- Scope requis : `meetings:upload` (tier **Pro** ou **Enterprise**)\n- Formats : `webm`, `mp4`, `m4a`, `wav`, `mp3`, `ogg`\n- Taille max : 500 MB (Pro) / 2 GB (Enterprise)\n\n```bash\ncurl -X POST \"https://gilbert-assistant.ovh/api/v1/meetings/upload\" \\\n  -H \"Authorization: Bearer glbrt_live_...\" \\\n  -F \"file=@meeting.mp3\" \\\n  -F \"title=Point produit 17 avril\"\n```\n\nRetour : `201` avec le détail complet de la réunion (`transcript_status=pending`).\nUtilisez ensuite le polling ou un **webhook** `meeting.transcribed` pour être notifié.\n\n---\n\n## Webhooks\n\nGilbert peut pusher des events HTTP POST vers votre endpoint dès qu'une réunion est\ntranscrite, synthétisée ou mise à jour. Chaque livraison est signée HMAC-SHA256 dans le\nheader `X-Gilbert-Signature`, avec retry automatique 3× (backoff 1 min / 5 min / 30 min).\n\n### Events disponibles\n\n- `meeting.transcribed` — transcription terminée\n- `meeting.summary_generated` — synthèse prête\n- `meeting.updated` — titre/folder modifié\n- `meeting.deleted` — réunion supprimée\n\n### Configuration\n\nPour l'instant, les webhooks sont configurés par un **administrateur Gilbert** (envoyez\nl'URL cible et la liste d'events à contact@lexiapro.fr). Le self-service est prévu\ndans une future version.\n\n### Vérifier la signature (Python)\n\n```python\nimport hmac, hashlib\n\ndef verify_signature(body: bytes, header_sig: str, secret: str) -> bool:\n    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, header_sig)\n```\n\n---\n\n## Ce que l'API ne fait pas (encore)\n\n- **Pas d'accès à l'audio brut** : les fichiers audio sont supprimés après transcription pour des raisons de conformité RGPD. L'API expose uniquement le texte.\n- **Pas de self-service webhooks** : la configuration passe par un administrateur. Roadmap : endpoint `/webhooks` pour gérer soi-même ses webhooks.\n- **Pas de timestamps word-level** : le transcript ne contient que les labels `Speaker X:` sans timings précis par mot. Roadmap v1.1.\n- **Pas de streaming** : l'upload se fait en un seul `POST`, pas de chunked upload via l'API publique pour l'instant.\n\n---\n\n## Exemples par langage\n\n### cURL\n\n```bash\ncurl -s \"https://gilbert-assistant.ovh/api/v1/meetings?per_page=10\" \\\n  -H \"Authorization: Bearer glbrt_live_XXXXXXXXXXXXXXXXXXXXXXXX\"\n```\n\n### Python (avec `requests`)\n\n```python\nimport os\nimport requests\n\nAPI_KEY = os.environ[\"GILBERT_API_KEY\"]\nBASE = \"https://gilbert-assistant.ovh/api/v1\"\nHEADERS = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\n# Lister les 50 dernières réunions terminées\nr = requests.get(\n    f\"{BASE}/meetings\",\n    headers=HEADERS,\n    params={\"status\": \"completed\", \"per_page\": 50},\n)\nr.raise_for_status()\nmeetings = r.json()[\"data\"]\n\n# Pour chaque réunion, récupérer le transcript complet\nfor m in meetings:\n    full = requests.get(f\"{BASE}/meetings/{m['id']}\", headers=HEADERS).json()\n    print(full[\"title\"], \"-\", (full.get(\"transcript_text\") or \"\")[:200])\n```\n\n### JavaScript (Node ou navigateur avec `fetch`)\n\n```js\nconst API_KEY = process.env.GILBERT_API_KEY;\nconst BASE = \"https://gilbert-assistant.ovh/api/v1\";\nconst headers = { Authorization: `Bearer ${API_KEY}` };\n\nconst res = await fetch(`${BASE}/meetings?status=completed&per_page=50`, { headers });\nconst { data } = await res.json();\n\nfor (const m of data) {\n  const full = await fetch(`${BASE}/meetings/${m.id}`, { headers }).then(r => r.json());\n  console.log(full.title, \"-\", (full.summary_text || \"\").slice(0, 200));\n}\n```\n\n### PHP\n\n```php\n$apiKey = getenv('GILBERT_API_KEY');\n$ch = curl_init(\"https://gilbert-assistant.ovh/api/v1/meetings?per_page=50\");\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\"Authorization: Bearer $apiKey\"],\n]);\n$response = json_decode(curl_exec($ch), true);\nforeach ($response['data'] as $meeting) {\n    echo $meeting['title'] . \"\\n\";\n}\n```\n\n### Ruby\n\n```ruby\nrequire 'net/http'\nrequire 'json'\n\nuri = URI(\"https://gilbert-assistant.ovh/api/v1/meetings?per_page=50\")\nreq = Net::HTTP::Get.new(uri)\nreq[\"Authorization\"] = \"Bearer #{ENV['GILBERT_API_KEY']}\"\nres = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }\nJSON.parse(res.body)[\"data\"].each { |m| puts m[\"title\"] }\n```\n\n### Go\n\n```go\npackage main\n\nimport (\n    \"encoding/json\"\n    \"fmt\"\n    \"net/http\"\n    \"os\"\n)\n\nfunc main() {\n    req, _ := http.NewRequest(\"GET\", \"https://gilbert-assistant.ovh/api/v1/meetings?per_page=50\", nil)\n    req.Header.Set(\"Authorization\", \"Bearer \"+os.Getenv(\"GILBERT_API_KEY\"))\n    res, _ := http.DefaultClient.Do(req)\n    defer res.Body.Close()\n    var out struct {\n        Data []map[string]interface{} `json:\"data\"`\n    }\n    json.NewDecoder(res.Body).Decode(&out)\n    for _, m := range out.Data {\n        fmt.Println(m[\"title\"])\n    }\n}\n```\n\n---\n\n## Support\n\n- **Contact technique** : [contact@lexiapro.fr](mailto:contact@lexiapro.fr)\n- **Formulaire de contact** : [gilbert-assistant.fr/contact](https://www.gilbert-assistant.fr/contact)\n- **Statut de l'API** : [gilbert-assistant.ovh/health](https://gilbert-assistant.ovh/health)\n\nPour toute question relative à la génération, rotation ou révocation de clés, contactez\nvotre administrateur Gilbert.\n\n---\n\n## Informations légales\n\n- [Conditions générales d'utilisation](https://www.gilbert-assistant.fr/conditions-utilisation)\n- [Politique de confidentialité](https://www.gilbert-assistant.fr/politique-confidentialite)\n- [Mentions légales](https://www.gilbert-assistant.fr/mentions-legales)\n\n---\n\n## Changelog\n\n### v1.0.0 (avril 2026)\n\nPremière version publique de l'API.\n\n**Endpoints lecture** (scope `meetings:read`)\n- `GET /me` : identité de la clé (user, scopes, api_key_id)\n- `GET /meetings` : liste paginée avec filtres (status, folder_id, from, to)\n- `GET /meetings/search?q=...` : recherche full-text PostgreSQL avec rank et snippet\n- `GET /meetings/{id}` : détail complet (transcript + synthèse)\n- `GET /meetings/{id}/transcript` : transcript seul\n- `GET /meetings/{id}/summary` : synthèse seule\n\n**Endpoints écriture** (tier Pro / Enterprise)\n- `POST /meetings/upload` : upload audio + transcription auto (`meetings:upload`)\n- `PATCH /meetings/{id}` : modifier titre / folder (`meetings:write`)\n- `PATCH /meetings/{id}/speakers/{speaker_id}` : renommer un speaker (`speakers:write`)\n- `DELETE /meetings/{id}` : suppression (`meetings:delete`, Enterprise uniquement)\n\n**Infrastructure**\n- Authentification par clé API (`Authorization: Bearer` ou `X-API-Key`)\n- Tiers **free** / **pro** / **enterprise** avec rate limits et capabilities distincts\n- **Webhooks** signés HMAC-SHA256 avec retry (configuration admin)\n- Rate limits tier-aware : 60 / 300 / 1 000 req/min, 5k / 50k / 500k req/jour\n- Index full-text PostgreSQL (`tsvector`) sur titres, transcriptions et synthèses\n","version":"1.0.0","contact":{"name":"Lexia — Support Gilbert","email":"contact@lexiapro.fr","url":"https://www.gilbert-assistant.fr/contact"},"license":{"name":"Conditions d'utilisation","url":"https://www.gilbert-assistant.fr/conditions-utilisation"},"termsOfService":"https://www.gilbert-assistant.fr/conditions-utilisation"},"paths":{"/api/v1/meetings":{"get":{"tags":["Public API","Réunions"],"summary":"Liste des réunions","description":"Retourne la liste **paginée** des réunions de l'utilisateur propriétaire de la clé API.\n\nLa réponse inclut uniquement les **metadata** (titre, date, durée, statut) — pas le transcript complet\npour rester léger. Pour obtenir transcript + synthèse, utilisez `GET /meetings/{id}` par réunion.\n\n### Cas d'usage typique\n\nSynchronisation incrémentale : récupérer les réunions `completed` créées depuis la dernière sync.\n\n```bash\ncurl \"https://gilbert-assistant.ovh/api/v1/meetings?status=completed&from=2026-04-01\" \\\n  -H \"Authorization: Bearer glbrt_live_...\"\n```\n\n### Tri\n\nLes résultats sont triés par `created_at` décroissant (plus récentes en premier).\n\n### Pagination\n\nUtilisez `meta.total_pages` pour boucler. Évitez de demander `per_page > 100` (cap).","operationId":"list_meetings_api_v1_meetings_get","parameters":[{"description":"Page (1-indexed)","required":false,"schema":{"type":"integer","minimum":1.0,"title":"Page","description":"Page (1-indexed)","default":1},"name":"page","in":"query"},{"description":"Résultats par page (max 100)","required":false,"schema":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Per Page","description":"Résultats par page (max 100)","default":50},"name":"per_page","in":"query"},{"description":"Filtrer par transcript_status : pending | processing | completed | error","required":false,"schema":{"type":"string","pattern":"^(pending|processing|completed|error)$","title":"Status","description":"Filtrer par transcript_status : pending | processing | completed | error"},"name":"status","in":"query"},{"description":"Filtrer par ID de dossier","required":false,"schema":{"type":"string","title":"Folder Id","description":"Filtrer par ID de dossier"},"name":"folder_id","in":"query"},{"description":"Date de début (ISO 8601, ex: 2026-01-01). Filtre sur created_at.","required":false,"schema":{"type":"string","title":"From","description":"Date de début (ISO 8601, ex: 2026-01-01). Filtre sur created_at."},"name":"from","in":"query"},{"description":"Date de fin (ISO 8601). Filtre sur created_at.","required":false,"schema":{"type":"string","title":"To","description":"Date de fin (ISO 8601). Filtre sur created_at."},"name":"to","in":"query"}],"responses":{"200":{"description":"Liste récupérée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeetingListResponse"}}}},"400":{"description":"Paramètre invalide (folder_id pas un UUID, date mal formée)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Clé API manquante ou invalide","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit dépassé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ApiKeyBearer":[]},{"ApiKeyHeader":[]}]}},"/api/v1/meetings/search":{"get":{"tags":["Public API","Réunions"],"summary":"Recherche full-text dans les réunions","description":"Recherche dans les **titres**, **transcriptions** et **synthèses** des réunions de l'utilisateur.\n\n### Exemples de requêtes\n\n- Un mot : `q=budget`\n- Plusieurs mots (ET logique) : `q=budget Q3`\n- Phrase exacte : `q=\"roadmap produit\"`\n\nL'index full-text PostgreSQL (`tsvector`) pondère les matches dans le titre > transcript > synthèse.\nUn score de pertinence est retourné par résultat avec un snippet HTML `<b>...</b>` pour le highlight.","operationId":"search_meetings_api_v1_meetings_search_get","parameters":[{"description":"Expression de recherche","required":true,"schema":{"type":"string","maxLength":200,"minLength":1,"title":"Q","description":"Expression de recherche"},"name":"q","in":"query"},{"required":false,"schema":{"type":"integer","minimum":1.0,"title":"Page","default":1},"name":"page","in":"query"},{"required":false,"schema":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Per Page","default":25},"name":"per_page","in":"query"}],"responses":{"200":{"description":"Résultats retournés","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}}},"400":{"description":"Query vide ou invalide","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too Many Requests","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ApiKeyBearer":[]},{"ApiKeyHeader":[]}]}},"/api/v1/meetings/{meeting_id}":{"get":{"tags":["Public API","Réunions"],"summary":"Détail d'une réunion","description":"Retourne **toutes les informations** d'une réunion, y compris transcript et synthèse.\n\n### Comportement\n\n- Si la transcription est en cours, `transcript_text` est `null` et `transcript_status` indique `processing` ou `pending`.\n- Si la synthèse n'a pas été générée, `summary_text` est `null`.\n- L'appel retourne `404` si la réunion n'existe pas **OU** si elle appartient à un autre utilisateur (même message, pas de fuite d'info).\n\n### Poids de la réponse\n\nLa réponse peut être volumineuse (transcript d'une réunion 4h = plusieurs MB). Si vous n'avez besoin\nque du transcript ou que de la synthèse, utilisez les endpoints dédiés `/transcript` et `/summary`.","operationId":"get_meeting_api_v1_meetings__meeting_id__get","parameters":[{"required":true,"schema":{"type":"string","title":"Meeting Id"},"name":"meeting_id","in":"path"}],"responses":{"200":{"description":"Réunion trouvée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeetingDetail"}}}},"401":{"description":"Clé API manquante ou invalide","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Réunion introuvable ou hors scope de la clé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit dépassé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ApiKeyBearer":[]},{"ApiKeyHeader":[]}]},"delete":{"tags":["Public API","Réunions"],"summary":"Supprimer une réunion","description":"Supprime définitivement une réunion (transcript + synthèse + fichier audio si présent). Scope requis : `meetings:delete`. Disponible uniquement en tier Enterprise.","operationId":"delete_meeting_api_v1_meetings__meeting_id__delete","parameters":[{"required":true,"schema":{"type":"string","title":"Meeting Id"},"name":"meeting_id","in":"path"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ApiKeyBearer":[]},{"ApiKeyHeader":[]}]},"patch":{"tags":["Public API","Réunions"],"summary":"Modifier une réunion","description":"Modifie le `title` et/ou le `folder_id` d'une réunion. Scope requis : `meetings:write`.","operationId":"patch_meeting_api_v1_meetings__meeting_id__patch","parameters":[{"required":true,"schema":{"type":"string","title":"Meeting Id"},"name":"meeting_id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeetingUpdateRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeetingDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ApiKeyBearer":[]},{"ApiKeyHeader":[]}]}},"/api/v1/meetings/{meeting_id}/transcript":{"get":{"tags":["Public API","Réunions"],"summary":"Transcription d'une réunion","description":"Retourne uniquement la **transcription texte** d'une réunion, sans les autres metadata.\n\n### Format\n\nLe champ `transcript_text` est une chaîne plain text avec les locuteurs préfixés :\n\n```\nSpeaker 0: Bonjour tout le monde, on commence la réunion.\nSpeaker 1: D'accord, je prends des notes.\nSpeaker 0: Aujourd'hui on va parler de la roadmap Q3.\n```\n\n### État d'avancement\n\nSi la transcription n'est pas terminée :\n- `transcript_status` = `pending` → en file d'attente\n- `transcript_status` = `processing` → en cours de traitement\n- `transcript_status` = `error` → échec (un retry automatique est planifié à 1h, 6h, 24h)\n\nDans tous ces cas, `transcript_text` sera `null`. Re-appelez l'endpoint plus tard pour vérifier.\n\n### Polling recommandé\n\nSi `status = processing`, attendez ~1 min avant de rappeler. La transcription d'une réunion d'1h\nprend typiquement 10-20 min.","operationId":"get_transcript_api_v1_meetings__meeting_id__transcript_get","parameters":[{"required":true,"schema":{"type":"string","title":"Meeting Id"},"name":"meeting_id","in":"path"}],"responses":{"200":{"description":"Transcript retourné (ou null si non terminé)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranscriptResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Réunion introuvable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too Many Requests","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ApiKeyBearer":[]},{"ApiKeyHeader":[]}]}},"/api/v1/meetings/{meeting_id}/summary":{"get":{"tags":["Public API","Réunions"],"summary":"Synthèse d'une réunion","description":"Retourne uniquement la **synthèse structurée** (résumé markdown) d'une réunion.\n\n### Format\n\nLe champ `summary_text` est du **markdown** que vous pouvez afficher directement dans votre UI\nou convertir en HTML/PDF. Structure typique :\n\n```markdown\n## Points clés\n- Décision 1\n- Décision 2\n\n## Actions à suivre\n- [ ] Martin : préparer le deck avant vendredi\n- [ ] Alice : valider le budget avec la finance\n\n## Détails\n...\n```\n\n### Quand la synthèse est-elle générée ?\n\nLa synthèse est générée **après** la transcription, à partir d'un template que l'utilisateur\na choisi dans Gilbert. Elle peut prendre 30s à 2 min supplémentaires.\n\nSi la réunion est récente, `summary_status` peut être `null` (pas encore démarrée),\n`processing` (en cours) ou `completed`.","operationId":"get_summary_api_v1_meetings__meeting_id__summary_get","parameters":[{"required":true,"schema":{"type":"string","title":"Meeting Id"},"name":"meeting_id","in":"path"}],"responses":{"200":{"description":"Synthèse retournée (ou null si pas générée)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SummaryResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Réunion introuvable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too Many Requests","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ApiKeyBearer":[]},{"ApiKeyHeader":[]}]}},"/api/v1/meetings/upload":{"post":{"tags":["Public API","Réunions"],"summary":"Upload audio via API","description":"Uploade un fichier audio et crée une nouvelle réunion. La transcription est lancée en arrière-plan.\n\n**Scope requis** : `meetings:upload` (tier Pro ou Enterprise).\n\n### Limites par tier\n\n- **Pro** : jusqu'à 600 min/jour, fichiers < 500 MB\n- **Enterprise** : jusqu'à 10 000 min/jour, fichiers < 2 GB\n\nLe quota de minutes audio est déduit du quota Discovery utilisateur classique.\n\n### Formats supportés\n\n`audio/webm`, `audio/mp4`, `audio/m4a`, `audio/wav`, `audio/mp3`, `audio/ogg`.\n\n### Retour\n\nRetourne les metadata de la réunion avec `transcript_status=pending`. Utilisez `GET /meetings/{id}` pour polling\nou un webhook `meeting.transcribed` pour être notifié.","operationId":"upload_meeting_api_api_v1_meetings_upload_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_meeting_api_api_v1_meetings_upload_post"}}},"required":true},"responses":{"201":{"description":"Réunion créée, transcription en cours","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeetingDetail"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Scope meetings:upload manquant ou tier insuffisant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"Fichier trop volumineux pour ce tier","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Format audio non supporté","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too Many Requests","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"ApiKeyBearer":[]},{"ApiKeyHeader":[]}]}},"/api/v1/meetings/{meeting_id}/speakers/{speaker_id}":{"patch":{"tags":["Public API","Réunions"],"summary":"Renommer un speaker","description":"Assigne un nom personnalisé à un locuteur détecté dans la transcription. Scope requis : `speakers:write`.","operationId":"rename_speaker_api_v1_meetings__meeting_id__speakers__speaker_id__patch","parameters":[{"required":true,"schema":{"type":"string","title":"Meeting Id"},"name":"meeting_id","in":"path"},{"required":true,"schema":{"type":"string","title":"Speaker Id"},"name":"speaker_id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpeakerUpdateRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ApiKeyBearer":[]},{"ApiKeyHeader":[]}]}},"/api/v1/me":{"get":{"tags":["Public API","Identité"],"summary":"Identité de la clé API","description":"Retourne les informations de l'**utilisateur propriétaire** de la clé API utilisée.\n\n### Usage typique\n\n- **Test rapide** après avoir reçu une nouvelle clé : vérifier qu'elle fonctionne et à quel user elle est associée.\n- **Debug** : lire `api_key_id` pour corréler avec les logs admin.\n\n### Response\n\n```json\n{\n  \"user_id\": \"5279a6e0-293a-45ff-9880-36f143b1663f\",\n  \"email\": \"jean.dupont@example.com\",\n  \"full_name\": \"Jean Dupont\",\n  \"api_key_id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n  \"scopes\": [\"meetings:read\"]\n}\n```","operationId":"get_api_identity_api_v1_me_get","responses":{"200":{"description":"Clé valide","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityResponse"}}}},"401":{"description":"Clé manquante, invalide, révoquée ou expirée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"ApiKeyBearer":[]},{"ApiKeyHeader":[]}]}}},"components":{"schemas":{"Body_upload_meeting_api_api_v1_meetings_upload_post":{"properties":{"file":{"type":"string","format":"binary","title":"File","description":"Fichier audio"},"title":{"type":"string","maxLength":500,"title":"Title","default":"Enregistrement sans titre"}},"type":"object","required":["file"],"title":"Body_upload_meeting_api_api_v1_meetings_upload_post"},"ErrorResponse":{"properties":{"detail":{"type":"string","title":"Detail","example":"Réunion introuvable"}},"type":"object","required":["detail"],"title":"ErrorResponse"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"IdentityResponse":{"properties":{"user_id":{"type":"string","title":"User Id","description":"UUID du user propriétaire de la clé"},"email":{"type":"string","title":"Email","example":"user@example.com"},"full_name":{"type":"string","title":"Full Name","example":"Jean Dupont"},"api_key_id":{"type":"string","title":"Api Key Id","description":"UUID de la clé API utilisée pour cet appel"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes","example":["meetings:read"]}},"type":"object","required":["user_id"],"title":"IdentityResponse"},"MeetingDetail":{"properties":{"id":{"type":"string","title":"Id","description":"Identifiant unique (UUID v4)","example":"84ddd6d8-274d-485d-8419-fe2e37b241e6"},"title":{"type":"string","title":"Title","description":"Titre de la réunion","example":"Réunion produit du 17 avril"},"created_at":{"type":"string","title":"Created At","description":"Date de création en ISO 8601 UTC","example":"2026-04-17T14:30:00Z"},"duration_seconds":{"type":"integer","title":"Duration Seconds","description":"Durée totale en secondes","example":3600},"speakers_count":{"type":"integer","title":"Speakers Count","description":"Nombre de locuteurs détectés","example":4},"language":{"type":"string","title":"Language","description":"Langue détectée (code BCP-47)","example":"fr"},"transcript_status":{"type":"string","title":"Transcript Status","description":"État de la transcription : `pending` (en attente), `processing` (en cours), `completed` (terminée), `error` (échec)","example":"completed"},"summary_status":{"type":"string","title":"Summary Status","description":"État de la synthèse : identique à `transcript_status`. `null` si non générée.","example":"completed"},"folder_id":{"type":"string","title":"Folder Id","description":"UUID du dossier si la réunion est classée"},"recording_client":{"type":"string","title":"Recording Client","description":"Plateforme d'enregistrement : `web`, `desktop`, `ios`, `android`","example":"web"},"transcript_text":{"type":"string","title":"Transcript Text","description":"Transcription complète au format `Speaker 0: ...\\nSpeaker 1: ...`. `null` si la transcription n'est pas terminée."},"summary_text":{"type":"string","title":"Summary Text","description":"Synthèse structurée en markdown. `null` si elle n'a pas encore été générée."},"word_count":{"type":"integer","title":"Word Count","description":"Nombre total de mots dans la transcription"}},"type":"object","required":["id","title","created_at","transcript_status"],"title":"MeetingDetail","description":"Détail complet d'une réunion, transcript et synthèse inclus."},"MeetingListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/MeetingSummary"},"type":"array","title":"Data"},"meta":{"$ref":"#/components/schemas/PaginationMeta"}},"type":"object","required":["data","meta"],"title":"MeetingListResponse"},"MeetingSummary":{"properties":{"id":{"type":"string","title":"Id","description":"Identifiant unique (UUID v4)","example":"84ddd6d8-274d-485d-8419-fe2e37b241e6"},"title":{"type":"string","title":"Title","description":"Titre de la réunion","example":"Réunion produit du 17 avril"},"created_at":{"type":"string","title":"Created At","description":"Date de création en ISO 8601 UTC","example":"2026-04-17T14:30:00Z"},"duration_seconds":{"type":"integer","title":"Duration Seconds","description":"Durée totale en secondes","example":3600},"speakers_count":{"type":"integer","title":"Speakers Count","description":"Nombre de locuteurs détectés","example":4},"language":{"type":"string","title":"Language","description":"Langue détectée (code BCP-47)","example":"fr"},"transcript_status":{"type":"string","title":"Transcript Status","description":"État de la transcription : `pending` (en attente), `processing` (en cours), `completed` (terminée), `error` (échec)","example":"completed"},"summary_status":{"type":"string","title":"Summary Status","description":"État de la synthèse : identique à `transcript_status`. `null` si non générée.","example":"completed"},"folder_id":{"type":"string","title":"Folder Id","description":"UUID du dossier si la réunion est classée"},"recording_client":{"type":"string","title":"Recording Client","description":"Plateforme d'enregistrement : `web`, `desktop`, `ios`, `android`","example":"web"}},"type":"object","required":["id","title","created_at","transcript_status"],"title":"MeetingSummary","description":"Résumé d'une réunion (format retourné par `/meetings`)."},"MeetingUpdateRequest":{"properties":{"title":{"type":"string","maxLength":500,"minLength":1,"title":"Title"},"folder_id":{"type":"string","title":"Folder Id","description":"UUID du dossier cible, null pour retirer du dossier"}},"type":"object","title":"MeetingUpdateRequest"},"PaginationMeta":{"properties":{"page":{"type":"integer","title":"Page","description":"Page courante (1-indexed)","example":1},"per_page":{"type":"integer","title":"Per Page","description":"Nombre d'éléments par page","example":50},"total":{"type":"integer","title":"Total","description":"Nombre total d'éléments","example":247},"total_pages":{"type":"integer","title":"Total Pages","description":"Nombre total de pages","example":5}},"type":"object","required":["page","per_page","total","total_pages"],"title":"PaginationMeta"},"SearchHit":{"properties":{"id":{"type":"string","title":"Id"},"title":{"type":"string","title":"Title"},"created_at":{"type":"string","title":"Created At"},"transcript_status":{"type":"string","title":"Transcript Status"},"rank":{"type":"number","title":"Rank","description":"Pertinence (0..1), plus haut = plus pertinent"},"snippet":{"type":"string","title":"Snippet","description":"Extrait textuel autour du match (HTML <b>...</b>)"}},"type":"object","required":["id","title","transcript_status","rank"],"title":"SearchHit"},"SearchResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/SearchHit"},"type":"array","title":"Data"},"meta":{"$ref":"#/components/schemas/PaginationMeta"},"query":{"type":"string","title":"Query"}},"type":"object","required":["data","meta","query"],"title":"SearchResponse"},"SpeakerUpdateRequest":{"properties":{"custom_name":{"type":"string","maxLength":100,"minLength":1,"title":"Custom Name","description":"Nouveau nom affiché pour ce speaker"}},"type":"object","required":["custom_name"],"title":"SpeakerUpdateRequest"},"SummaryResponse":{"properties":{"meeting_id":{"type":"string","title":"Meeting Id","example":"84ddd6d8-274d-485d-8419-fe2e37b241e6"},"summary_status":{"type":"string","title":"Summary Status","example":"completed"},"summary_text":{"type":"string","title":"Summary Text","description":"Synthèse en markdown. `null` si pas encore générée."}},"type":"object","required":["meeting_id"],"title":"SummaryResponse"},"TranscriptResponse":{"properties":{"meeting_id":{"type":"string","title":"Meeting Id","example":"84ddd6d8-274d-485d-8419-fe2e37b241e6"},"transcript_status":{"type":"string","title":"Transcript Status","description":"`pending` | `processing` | `completed` | `error`","example":"completed"},"transcript_text":{"type":"string","title":"Transcript Text","description":"Format : `Speaker X: ...`. `null` si pas encore prêt."},"speakers_count":{"type":"integer","title":"Speakers Count","example":4},"language":{"type":"string","title":"Language","example":"fr"},"duration_seconds":{"type":"integer","title":"Duration Seconds","example":3600}},"type":"object","required":["meeting_id","transcript_status"],"title":"TranscriptResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"ApiKeyBearer":{"type":"http","scheme":"bearer","description":"Clé API au format `glbrt_live_XXXXXXXXXXXXXXXXXXXXXXXX`. Générée par un administrateur Gilbert."},"ApiKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Alternative : clé API dans le header `X-API-Key`."}}},"servers":[{"url":"https://gilbert-assistant.ovh","description":"Production"}],"externalDocs":{"description":"Mentions légales et politique de confidentialité","url":"https://www.gilbert-assistant.fr/mentions-legales"},"tags":[{"name":"Public API","description":"Endpoints publics accessibles avec une clé API. Lecture seule."},{"name":"Identité","description":"Vérifier la validité et les droits de la clé API utilisée."},{"name":"Réunions","description":"Récupérer la liste, le détail, le transcript et la synthèse d'une réunion."}]}