# Documentation API DahuGPT

## Base URL
```
http://localhost:8000/api
```

## Authentication
Toutes les routes protégées nécessitent un token Bearer obtenu lors de l'inscription ou de la connexion.

```
Authorization: Bearer {token}
```

Les métadonnées publiques sous `/.well-known` (OAuth/OIDC documentaires) et leur rapport avec Sanctum sont décrites dans **`docs/backend/agent-discovery.md`**.

### Clés API développeur (abilities)

En plus des jetons retournés par `POST /api/register` et `POST /api/login`, les utilisateurs dont le plan autorise l’accès développeur peuvent créer des **Personal Access Tokens** (PAT) depuis l’interface **`/developer`** ou via `GET/POST/DELETE /api/developer/api-keys`. Chaque clé porte une liste d’**abilities** (chaînes Sanctum), par défaut `["*"]` (accès complet côté contrôle d’abilities).

**Quand la vérification s’applique**

- **Requête authentifiée par un PAT** (en-tête `Authorization: Bearer …` pointant vers une entrée de `personal_access_tokens`) : le middleware `pat.can:{ability}` exige l’ability indiquée pour la route.
- **Application web (session Sanctum / cookies)** : l’utilisateur n’a en général **pas** de `currentAccessToken` ; les restrictions par ability **ne s’appliquent pas** (comportement inchangé pour le SPA).
- **`*` sur le jeton** : toutes les abilities listées ci‑dessous sont considérées comme accordées.

**Réponse en cas de refus**

- **403** si le PAT ne dispose pas de l’ability requise (exception Sanctum `MissingAbilityException`).

#### Référence des abilities

| Ability | Rôle |
|--------|------|
| `*` | Accès complet au sens abilities (toutes les routes ci‑dessous). |
| `chat:read` | Lecture « catalogue » : profil (`/me`, `/user`), historique chat, conversations, pièces jointes (statut), crédits (GET), modèles (GET hors catalogue public), **GET /developer/ping**. Équivaut aussi si le jeton a `chat:write`, `developer:keys`, ou `*`. |
| `chat:write` | Envoi de messages **`POST /chat`**, suppressions / modifications liées au flux chat : **`DELETE /chats/{id}`**, **`PATCH /conversations/{conversation}/project`**, **`DELETE /conversations/{conversation}`**. |
| `tts:write` | **`POST /tts`**. Équivaut aussi si le jeton a `chat:write` ou `*`. |
| `personas:read` | Lecture des routes **`GET /personas/...`** (liste conversations, latest, modules, détail conversation). Équivaut aussi avec `personas:write`, `chat:read`, `chat:write`, ou `*`. |
| `personas:write` | **`POST /personas/{persona}/chat`**, **`DELETE /personas/conversations/{conversation}`**. Équivaut aussi si le jeton a `chat:write` ou `*`. |
| `projects:read` | **`GET /projects`**, **`GET /projects/{project}`**, citations et fichier document. Équivaut aussi avec `projects:write`, `chat:read`, `chat:write`, ou `*`. |
| `projects:write` | Création / modification / suppression de projets et documents : **`POST/PUT/DELETE /projects`**, **`POST/DELETE /projects/.../documents`**. |
| `developer:keys` | Portail clés : **`GET /developer/usage-stats`**, **`GET/POST/DELETE /developer/api-keys`**. Ne remplace pas `chat:read` pour les autres routes (sauf hiérarchies ci‑dessus). |
| `billing:write` | Abonnements utilisateur (`POST/PATCH/DELETE /subscribe…`), **`POST /stripe/checkout`**, **`POST /stripe/billing-portal`**, **`POST /credits/topup`**, checkout et portail facturation organisation (`POST …/billing/checkout`, `POST …/billing/billing-portal`). `chat:write` n’accorde plus cette ability. |
| `n8n:workflows:manage` | Management workflows n8n: **`GET/POST/PATCH/DELETE /n8n/workflows`**, **`POST /n8n/workflows/{id}/activate`**, **`.../deactivate`**, **`.../archive`**, **`.../launch`**. Équivaut aussi avec `developer:keys` ou `*`. |
| `n8n:executions:read` | Lecture exécutions corrélées: **`GET /n8n/executions`**, **`GET /n8n/executions/{id}`**. Équivaut aussi avec `n8n:workflows:manage`, `developer:keys` ou `*`. |
| `n8n:callbacks` | Ability technique interne (service token) pour callbacks n8n si endpoint protégé par PAT. |

**Matrice détaillée**

Voir **`docs/backend/sanctum-abilities-matrix.md`**. Vue d’ensemble invitations web + sémantique `pat.can` : **`docs/overview/api-auth-security.md`**.

**Implémentation**

- Middleware : `app/Http/Middleware/RequirePersonalAccessTokenAbility.php`
- Configuration et rappels : `config/developer_api.php`
- **Spécification OpenAPI complète** : générée depuis `openapi/` via `composer openapi:build` → [`public/openapi.json`](../../public/openapi.json) ; documentation interactive **[/api-docs](/api-docs)** (Redoc) ; collection Postman **[/postman/dahuGPT.postman_collection.json](/postman/dahuGPT.postman_collection.json)** (`composer openapi:postman`)

---

## MCP (Model Context Protocol)

Serveur MCP exposé sur la **racine du site** (pas sous `/api`). Implémentation : `app/Http/Controllers/McpController.php`.

### Endpoint et transport

| Élément | Détail |
|--------|--------|
| **URL** | `POST {APP_URL}/mcp` |
| **Corps** | JSON-RPC **2.0** : `jsonrpc`, `id`, `method`, `params` |
| **Auth** | **Uniquement** un *Personal Access Token* Sanctum : `Authorization: Bearer <token>`. Les sessions web (cookies) ne sont pas acceptées : réponse JSON-RPC avec `error.code` **-32001** (`Forbidden: MCP requires a personal access token`). |
| **Compte** | Middleware `user.active` (compte désactivé : comportement habituel). |
| **Quota** | Middleware `throttle:developer-api` (même famille que le portail développeur). |
| **Découverte** | `GET /.well-known/mcp/server-card.json` — `endpoint`, `capabilities` (`tools`, `resources`). |

### Méthodes JSON-RPC

| Méthode | Rôle |
|---------|------|
| `initialize` | Poignée de main (`protocolVersion`, `serverInfo`, `capabilities`). |
| `initialized` | Accusé de réception (résultat vide). |
| `tools/list` | Définitions des outils disponibles. |
| `tools/call` | Exécution : `params.name`, `params.arguments`. |
| `resources/list` | Ressources exposées (conversations de l’utilisateur). |
| `resources/read` | Lecture d’une ressource via `params.uri`. |

### Outils (`tools/call`)

| Outil | Abilities PAT | Description |
|-------|----------------|-------------|
| `dahu_chat` | `chat:write` ou `*` | Chat catalogue (`SendChatAction`). Arguments : `prompt` (obligatoire), optionnellement `model`, `theme`, `level`, `conversation_id`. |
| `dahu_list_personas` | `personas:read`, ou équivalences (`chat:read`, `chat:write`, `*`, voir contrôleur) | Liste des personas actifs (`key`, `name`, `description`). |
| `dahu_persona_chat` | `personas:write` ou `chat:write` | Persona sans pièce jointe. Arguments : `persona`, `prompt` ; optionnellement `conversation_id`, `mode`. |
| `dahu_web_search` | `chat:read` ou `chat:write` | Recherche web Brave (même logique que le tool `web_search` côté LLM). Argument : `query`. |

### Ressources

- **URI :** `dahu://conversations/{ulid}` — conversations **chat** ou **persona** appartenant à l’utilisateur du jeton.
- Les abilities pour `resources/list` et `resources/read` suivent les mêmes règles que pour le chat / personas (voir `McpController`).

### Exemples JSON-RPC

**Initialize**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {}
}
```

**Liste des outils**

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}
```

**Recherche web (`dahu_web_search`)**

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "dahu_web_search",
    "arguments": {
      "query": "actualités IA Suisse"
    }
  }
}
```

**Chat (`dahu_chat`)**

```json
{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "dahu_chat",
    "arguments": {
      "prompt": "Explique Laravel en une phrase.",
      "theme": "general",
      "level": "light"
    }
  }
}
```

### Erreurs JSON-RPC fréquentes

| Code | Cas typique |
|------|-------------|
| -32602 | Corps invalide, `params` rejetés par la validation. |
| -32601 | Méthode ou nom d’outil inconnu (hors liste attendue). |
| -32001 | PAT absent / ability insuffisante / session sans PAT. |
| -32002 | Ressource introuvable (conversation). |
| -32603 | Erreur interne. |

---

## n8n (management + callbacks)

### Management workflows/exécutions

Routes protégées (`auth:sanctum`, `user.active`) :

- `GET /api/n8n/workflows`
- `POST /api/n8n/workflows`
- `GET /api/n8n/workflows/{n8nWorkflow}`
- `PATCH /api/n8n/workflows/{n8nWorkflow}`
- `DELETE /api/n8n/workflows/{n8nWorkflow}`
- `POST /api/n8n/workflows/{n8nWorkflow}/activate`
- `POST /api/n8n/workflows/{n8nWorkflow}/deactivate`
- `POST /api/n8n/workflows/{n8nWorkflow}/archive`
- `POST /api/n8n/workflows/{n8nWorkflow}/launch`
- `GET /api/n8n/executions`
- `GET /api/n8n/executions/{n8nExecution}`

### Callback n8n -> Laravel

Endpoint public signé HMAC :

- `POST /api/hooks/n8n/callbacks`

En-têtes requis :

- `X-N8N-Timestamp`
- `X-N8N-Signature` (`v1=<hmac_sha256(timestamp.body, N8N_CALLBACK_SECRET)>`)

Le callback met à jour `n8n_executions` et ajoute un événement append-only dans `n8n_execution_events`.

---

## 🔓 Routes Publiques

**Limitation de débit :** `POST /api/register` et `POST /api/login` acceptent au plus **10 requêtes par minute et par adresse IP** (`throttle:10,1`). Au-delà, la réponse est **429 Too Many Requests**.

### POST /api/register
Inscription d'un nouvel utilisateur avec 10 crédits de bienvenue. Le champ **`terms_accepted`** (booléen accepté) est **obligatoire**.

**Request Body:**
```json
{
  "name": "John Doe",
  "email": "john@example.com",
  "password": "password123",
  "password_confirmation": "password123",
  "preferred_language": "fr",
  "terms_accepted": true
}
```

**Response (201):**
```json
{
  "user": {
    "id": "01JGXXX...",
    "name": "John Doe",
    "email": "john@example.com",
    "credits_balance": "10.00",
    "preferred_language": "fr",
    "created_at": "2025-12-31T10:00:00.000000Z"
  },
  "token": "1|xxx...",
  "token_type": "Bearer"
}
```

**Erreur serveur (500) :** en cas d’exception interne lors de l’inscription, le message renvoyé au client est **générique** (aucun détail technique). En environnement `local` uniquement, une clé `debug` peut contenir le message d’exception pour faciliter le diagnostic.

### Vie privée (export / effacement)

Voir aussi `docs/backend/account-deactivation.md`.

| Méthode | Route | Ability PAT | Description |
|---------|-------|-------------|-------------|
| `GET` | `/api/user/data-export` | `account:manage` | Téléchargement JSON des données personnelles |
| `DELETE` | `/api/user` | `account:manage` | Désactivation du compte (`confirm` + `password` si applicable) |

Débit : `throttle:user-privacy` (5 req / heure / utilisateur).
---

### POST /api/login
Connexion d'un utilisateur existant.

**Request Body:**
```json
{
  "email": "john@example.com",
  "password": "password123"
}
```

**Response (200):**
```json
{
  "user": {
    "id": "01JGXXX...",
    "name": "John Doe",
    "email": "john@example.com",
    "credits_balance": "10.00",
    "preferred_language": "fr"
  },
  "token": "2|xxx...",
  "token_type": "Bearer"
}
```

**Error (422):**
```json
{
  "message": "The given data was invalid.",
  "errors": {
    "email": ["Les identifiants fournis sont incorrects."]
  }
}
```

**429 :** voir la limitation de débit sur les routes publiques d’authentification ci-dessus.

---

## 🔒 Routes Protégées

### GET /api/user
Récupérer les informations de l'utilisateur connecté.

**Headers:**
```
Authorization: Bearer {token}
```

**Response (200):** l’objet `user` suit `UserResource` (crédits, langue, etc.). Si la relation `subscription` est chargée, un objet `subscription` peut être présent :

```json
{
  "user": {
    "id": "01JGXXX...",
    "name": "John Doe",
    "email": "john@example.com",
    "credits": 10,
    "subscription": {
      "id": "01JGXXX...",
      "plan": "plus",
      "stripe_status": "active",
      "cancel_at_period_end": false,
      "stripe_canceled_at": null,
      "expires_at": "2026-04-24T12:00:00+00:00"
    }
  }
}
```

---

### POST /api/logout
Déconnexion de l'utilisateur (révocation du token).

**Headers:**
```
Authorization: Bearer {token}
```

**Response (200):**
```json
{
  "message": "Déconnexion réussie"
}
```

---

## 💬 Chat

### POST /api/chat
Envoyer un message au chat IA et recevoir une réponse.

**Headers:**
```
Authorization: Bearer {token}
Content-Type: application/json
```

**Request Body:**
```json
{
  "prompt": "Explique-moi la théorie de la relativité",
  "model": "anthropic/claude-3.5-sonnet",
  "conversation_id": "conv-123-abc",
  "type": "general"
}
```

**Paramètres (principaux):**
- `prompt` (required) : Le message de l'utilisateur.
- `theme` / `level` (optional) : Couple **contexte × niveau** aligné sur `plan_contexts` et `chat_catalog_entries`
  (sélection chat standard).
- `model` (optional) : **Soit** clé catalogue Infomaniak (`…`, `flux`, `photomaker`, …) ;
  **soit** désambiguïsation lorsque plusieurs lignes `chat_catalog_entries` existent pour le même
  `(theme, level)` — obligatoire dans ce cas (erreur 422 sinon).
- `model_theme_id` (optional, ULID) : Alias legacy — référence directe à une ligne **`chat_catalog_entries`**.
- `conversation_id` (optional) : ID de la conversation pour inclure le contexte.
- `type` (optional, déprécié) : `general` ou `code` — préférer `theme` / `level`.
- `web_search` (optional, bool) : Recherche web si le modèle l’autorise.
- `stream` (optional, bool) : Streaming SSE (hors génération d’image). L’événement
  `done` inclut `sources[]` (même contrat que `chat.sources`).
- `mode` (optional) : `text` ou `image` — pour la génération d’image avec `theme=image`, le client
  envoie en général `mode=image` (sinon combinaison `theme` / `level` / résolution modèle).
- `attachment` + `attachment_kind` (optionnel) : Fichier `multipart/form-data` ;
  `attachment_kind` ∈ `image` | `file` | `audio` | `video` **requis** si `attachment` présent.

**Génération d'image (Infomaniak, même `POST /api/chat`) :**
- Entrée catalogue : `theme=image`, `level`, et si besoin `model` (clé catalogue) pour choisir la variante.
- Les règles **référence image** suivent **`supports_image_input`** au niveau `chat_catalog_entries` × `ai_models`
  (`effectiveSupportsImageInput()` côté API) : si vrai, PJ image obligatoire ; si faux, PJ image refusée.
- Réponse : `chat.response_kind` = `image`, `chat.response` = URL auth `GET /api/messages/{id}/generated-image` (stockage privé ; legacy `/storage/…` encore lisible si le fichier public existe).

**`GET /api/models/capabilities`** : query `theme`, `level`, et optionnellement **`model`** ou **`model_key`**
(même clé catalogue que pour `POST /api/chat`) pour désambiguïser ; **`supports_image_input`** décrit
notamment l’entrée image pour la génération.

**Response (200):**
```json
{
  "status": "success",
  "chat": {
    "id": "01JGYYY...",
    "conversation_id": "conv-123-abc",
    "prompt": "Explique-moi la théorie de la relativité",
    "response": "La théorie de la relativité...",
    "model": "anthropic/claude-3.5-sonnet",
    "credits_used": "0.025",
    "cached": false,
    "sources": [
      {
        "type": "web",
        "title": "Exemple",
        "url": "https://example.ch/page",
        "excerpt": "Extrait Brave…"
      }
    ],
    "created_at": "2026-01-03T10:05:00Z"
  },
  "user": {
    "credits_balance": "9.975"
  }
}
```

**Métadonnées de Contexte:**

Lorsqu'un `conversation_id` est fourni, le système inclut automatiquement les messages précédents de la conversation. Le nombre de messages inclus dépend de votre plan :

| Plan | Max Messages | Max Tokens |
|------|--------------|------------|
| Basic | 5 | 2 000 |
| Plus | 15 | 8 000 |
| Pro | 30 | 16 000 |

Les champs suivants sont ajoutés à la réponse :
- `context_messages_count` : Nombre de messages de contexte utilisés
- `context_tokens_estimated` : Tokens estimés du contexte
- `chat.sources` : puces source (`type` = `web` | `project`) si recherche web et/ou RAG
  projet ont alimenté le tour ; tableau vide sinon. Même champ sur `GET /api/conversations/{id}`.

**Exemple avec Contexte:**
```json
{
  "status": "success",
  "chat": {
    "id": "01JGYYY...",
    "conversation_id": "conv-123-abc",
    "prompt": "Et Einstein, que pensait-il ?",
    "response": "Einstein, dont nous parlions précédemment...",
    "model": "anthropic/claude-3.5-sonnet",
    "credits_used": "0.035",
    "cached": false,
    "context_messages_count": 5,
    "context_tokens_estimated": 1200,
    "created_at": "2026-01-03T10:06:00Z"
  },
  "user": {
    "credits_balance": "9.940"
  }
}
```

**Error (400) — crédits insuffisants ou échec de facturation :**
```json
{
  "message": "Crédits insuffisants. Veuillez recharger votre compte."
}
```

Le contrôleur renvoie **400** (et non 402) pour les refus de crédits et les échecs de `settle` après appel IA. Aucun enregistrement `messages` n’est créé si le règlement échoue.

### Facturation des appels chat

1. Estimation haute (`estimateChatUpperBound`) selon modèle, contexte et outils.
2. **Pré-autorisation** du montant estimé (`authorizeMaximum`).
3. Appel fournisseur (sync ou stream).
4. **Règlement** au coût réel (`settle`) ou **annulation** (`void`) en cas de cache hit / modèle gratuit / échec avant consommation.

Voir [`docs/backend/billing-authorization.md`](../backend/billing-authorization.md).

### Streaming SSE (`stream: true`)

Réponse `Content-Type: text/event-stream`. Événements JSON par ligne `data: …` :

| `type` | Description |
|--------|-------------|
| `start` | Connexion établie ; champ `model`. |
| *(chunks OpenAI)* | `choices[].delta.content` — texte progressif. |
| `done` | Fin : `chat_id`, `conversation_id`, `credits_used`, soldes crédits. |
| `error` | Erreur technique ou métier (hors facturation). |
| `billing_error` | Échec de facturation après streaming (ex. `settle` insuffisant). Le client web retire la bulle assistant, conserve le prompt user, affiche l’erreur et rafraîchit les crédits. |

Exemple fin de flux :
```
data: {"type":"start","model":"mistralai/..."}

data: {"choices":[{"delta":{"content":"Bonjour"}}]}

data: {"type":"done","chat_id":"01J...","credits_used":0.05,...}
```

**Error (402) — historique / clients tiers :**
```json
{
  "error": "Crédits insuffisants",
  "credits_balance": "0.00"
}
```

**Error (500):**
```json
{
  "error": "Erreur lors de la communication avec l'API",
  "details": {}
}
```

### GET /api/models/themes
Thèmes (contextes) disponibles pour le plan courant, pour le sélecteur du chat.

**Headers :** `Authorization: Bearer {token}`

**Response (200) :** `plan`, `themes` (liste de `{ key, label }`). L’ordre des entrées suit le champ **`sort_order`** des lignes `plan_contexts` (configurable par plan dans l’admin Filament) : valeur la plus petite en premier ; en cas d’égalité, ordre alphabétique des clés de thème.

### GET /api/models/levels
Niveaux disponibles pour un thème donné (`?theme=...`).

---

### GET /api/conversations
Récupérer la liste des conversations de l'utilisateur (paginé, 20 par page).

**Headers:**
```
Authorization: Bearer {token}
```

**Query Parameters:**
- `page` (optional) : Numéro de page (défaut: 1)

**Response (200):**
```json
{
  "conversations": {
    "current_page": 1,
    "data": [
      {
        "conversation_id": "conv-123-abc",
        "last_message_at": "2026-01-03T10:05:00.000000Z",
        "message_count": 5,
        "last_prompt": "Explique-moi la théorie de la relativité",
        "last_response": "La théorie de la relativité...",
        "model": "anthropic/claude-3.5-sonnet",
        "model_theme": "general",
        "model_level": "premium"
      }
    ],
    "total": 1,
    "per_page": 20,
    "last_page": 1
  }
}
```

**Champs de réponse:**
- `conversation_id` : Identifiant unique de la conversation
- `last_message_at` : Date du dernier message
- `message_count` : Nombre total de messages dans la conversation
- `last_prompt` : Dernier prompt de l'utilisateur
- `last_response` : Dernière réponse de l'IA
- `model` : Clé du modèle OpenRouter utilisé (pour compatibilité/débogage)
- `model_theme` : Type de modèle/thème utilisé (ex: `general`, `development`, `marketing`, etc.)
- `model_level` : Niveau du modèle utilisé (ex: `light`, `standard`, `premium`, ou `null` si non trouvé)

**Note:** Les champs `model_theme` et `model_level` sont dérivés du dernier message de la conversation en recherchant le `ModelTheme` correspondant au `model_key`. Si aucun `ModelTheme` n'est trouvé, `model_theme` prend la valeur par défaut `"general"` et `model_level` est `null`.

---

### GET /api/conversations/today
Récupérer la dernière conversation du jour pour l'utilisateur connecté.

**Headers:**
```
Authorization: Bearer {token}
```

**Response (200):**
```json
{
  "conversation_id": "conv-123-abc",
  "messages": [
    {
      "id": "01JGYYY...",
      "prompt": "Bonjour",
      "response": "Bonjour ! Comment puis-je vous aider ?",
      "model": "anthropic/claude-3.5-sonnet",
      "credits_used": "0.000",
      "created_at": "2026-01-03T10:00:00.000000Z"
    }
  ]
}
```

**Comportement:**
- Si aucune conversation n'existe aujourd'hui, `conversation_id` est `null` et `messages` est un tableau vide
- Seules les conversations créées le jour même sont retournées
- Les conversations ouvertes depuis l'historique ne sont pas affectées par cette route

---

### GET /api/conversations/{conversationId}
Récupérer tous les messages d'une conversation spécifique.

**Headers:**
```
Authorization: Bearer {token}
```

**Response (200):**
```json
{
  "conversation_id": "conv-123-abc",
  "messages": [
    {
      "id": "01JGYYY...",
      "prompt": "Premier message",
      "response": "Première réponse",
      "model": "anthropic/claude-3.5-sonnet",
      "credits_used": "0.025",
      "created_at": "2026-01-03T10:00:00.000000Z"
    },
    {
      "id": "01JGZZZ...",
      "prompt": "Deuxième message",
      "response": "Deuxième réponse",
      "model": "anthropic/claude-3.5-sonnet",
      "credits_used": "0.030",
      "created_at": "2026-01-03T10:05:00.000000Z"
    }
  ]
}
```

**Note:** Cette route permet de charger une conversation depuis l'historique. Lorsqu'une conversation est chargée de cette manière, elle reste affichée dans le chat même si elle n'est pas du jour, contrairement au comportement automatique de chargement de la conversation du jour.

---

### DELETE /api/conversations/{conversationId}
Supprimer une conversation complète (tous ses messages).

**Headers:**
```
Authorization: Bearer {token}
```

**Response (200):**
```json
{
  "message": "Conversation supprimée avec succès",
  "deleted_count": 5
}
```

**Sécurité:** Un utilisateur ne peut supprimer que ses propres conversations. Les tentatives de suppression de conversations d'autres utilisateurs sont ignorées silencieusement.

---

## Projets (`/api/projects`)

Mode projets : instructions système, documents RAG et rattachement de conversations catalogue.

- `GET/POST /api/projects`, `GET/PUT/DELETE /api/projects/{project}` — CRUD (abilities `projects:read` / `projects:write`)
- `GET /api/projects/{project}/documents/{document}/citations/{chunk}` — passage RAG pour le viewer (ability `projects:read`)
- `GET /api/projects/{project}/documents/{document}/file` — téléchargement authentifié (ability `projects:read`)
- `POST /api/projects/{project}/documents` — upload document (`multipart/form-data`)
- `DELETE /api/projects/{project}/documents/{document}` — suppression document
- `PATCH /api/conversations/{conversation}/project` — corps `{ "project_id": "<ulid>" | null }`

Détail : [projects-mode.md](../backend/projects-mode.md). Schémas OpenAPI : `/api-docs` ou `public/openapi.json`.

---

## Persona (assistants Infomaniak)

### GET /api/personas/{persona}/modules
Configuration publique des modules (prompts masqués côté client utile : flags d’import, `allowed_document_mimes`, etc.). Voir [personas.md](../backend/personas.md).

### POST /api/personas/{persona}/chat
Envoi d’un message (JSON ou `multipart/form-data` si pièce jointe).

**Champs courants :**
- `persona_type` : doit être identique à `{persona}` dans l’URL (notamment si `attachment` est présent).
- `prompt` : texte utilisateur.
- `mode` (optionnel) : clé du module (ex. `general`). Si absent, le backend résout un module par défaut (voir doc backend Persona).
- `conversation_id` (optionnel) : poursuite d’une conversation existante.
- Avec fichier : `attachment`, `attachment_kind` (`image` | `file` | `audio` | `video` | `document`).
- **Réconciliation** (modules `*_reco` ou `requires_two_files`) : **`attachments[]`** avec **exactement 2** fichiers (`attachment_kind` = `file` ou `document`), pas de `attachment` unique. Traitement async ; le job utilise `type: reconciliation` (moteur déterministe, pas RAG concaténé sur les deux fichiers).

**Validation des fichiers :** les types MIME des **documents** sont imposés par la liste **`allowed_document_mimes`** du module en base (admin Filament), en complément des flags `allow_*`. Les réponses d’erreur de validation utilisent le code **422**.

Documentation détaillée : [personas.md](../backend/personas.md), réconciliation : [persona-reconciliation.md](../backend/persona-reconciliation.md).

---

## Mode agent (`/api/agent/runs`)

MVP F3.4 (SPA Sanctum, abilities `chat:read` / `chat:write`). Distinct de `/api/scheduled-agents` (F2.5). Plans **Plus / Pro / Standard / Avancé** ; Basic, Starter et rôle org **viewer** exclus.

| Méthode | Chemin | Rôle |
|---------|--------|------|
| `POST` | `/api/agent/runs` | Objectif → plan (`awaiting_approval`) |
| `PATCH` | `/api/agent/runs/{id}` | Replanifier (nouveau `objective`) |
| `POST` | `/api/agent/runs/{id}/approve` | Exécution séquentielle (catalogue et/ou persona) |
| `GET` | `/api/agent/runs/{id}` | Poll (1–2 s côté SPA tant que `running`) |
| `DELETE` | `/api/agent/runs/{id}` | Annuler ; étapes settlées inchangées |

Un seul run actif par conversation. Hors MVP : MCP client, PDF, auto-approve, parallèle.

---

## Agents planifiés (`/api/scheduled-agents`)

CRUD authentifié (abilities `chat:read` / `chat:write`, voir `routes/api.php`). Les quotas (fréquence cron minimale, nombre d’agents actifs) dépendent du plan : `config/scheduled_agents.php`.

**SSRF (webhook sortant)** : si `output_type` vaut `webhook`, l’URL doit être **HTTPS**, hôte sur la **allowlist** `SCHEDULED_AGENTS_OUTBOUND_WEBHOOK_HOSTS` (voir `config/scheduled_agents.php` et `docs/overview/api-auth-security.md`).

Champs utiles en réponse détail :

- `inbound_webhook_allowed` : booléen — `true` si le plan utilisateur autorise un déclenchement HTTP entrant (**Plus** ou **Pro**).
- `inbound_webhook_url` : URL **`POST`** sans secret (chemin `/api/hooks/scheduled-agents/{id}`) lorsqu’un jeton est défini ; sinon `null`.
- `inbound_webhook_secret` : présent **uniquement** dans la réponse immédiate d’un `PATCH` avec `regenerate_inbound_webhook_token: true` — secret à placer dans l’en-tête **`X-Webhook-Token`** (ou `Authorization: Bearer …`) lors des appels ; ne pas logger ni versionner.

### PATCH `/api/scheduled-agents/{id}` — régénérer le secret entrant

Corps JSON (en complément des autres champs optionnels) :

```json
{
  "regenerate_inbound_webhook_token": true
}
```

- **Plus / Pro** : enregistre un nouveau jeton opaque, met à jour `inbound_webhook_url` et renvoie **`inbound_webhook_secret`** une fois dans le corps JSON.
- **Basic** : **422** avec message explicite (fonctionnalité réservée).

### POST `/api/hooks/scheduled-agents/{scheduled_agent}` — déclenchement entrant (recommandé)

- **Sans auth utilisateur** ; authentification du **webhook** via en-tête :
  - **`X-Webhook-Token: <secret>`** (recommandé), ou
  - **`Authorization: Bearer <secret>`** (équivalent pour ce flux).
- Le secret est celui renvoyé à la régénération (`inbound_webhook_secret`) ou déjà stocké côté client.
- **Plans** : uniquement **Plus** et **Pro** ; sinon **403**.
- Agent **inactif** (`is_active: false`) : **422**.
- Secret invalide ou agent inconnu : **404** (pas de fuite d’information).
- **Limite** : **1** requête par minute et par agent (`429` si dépassé).

### POST `/api/hooks/scheduled-agents/{scheduled_agent}/{token}` — déprécié

- Même comportement que ci-dessus lorsque le secret est passé en **dernier segment d’URL**.
- **Déprécié** : risque d’exposition dans les journaux d’accès, historiques navigateur ou en-têtes `Referer`. La réponse peut inclure **`Deprecation: true`**. Migrer vers l’URL sans segment secret + **`X-Webhook-Token`**.

Réponse typique (**200**) :

```json
{
  "status": "queued",
  "scheduled_agent_id": "01JG..."
}
```

---

## 💳 Crédits

### GET /api/credits/balance
Consulter le solde de crédits.

**Headers:**
```
Authorization: Bearer {token}
```

**Response (200):**
```json
{
  "credits_balance": "10.00",
  "subscription": {
    "id": "01JGZZZ...",
    "plan": "plus",
    "expires_at": "2026-01-31T10:00:00.000000Z"
  }
}
```

---

### POST /api/credits/topup (DÉPRÉCIÉ)

> **Attention** : Ce endpoint est déprécié depuis l'intégration Stripe Checkout. Les achats de crédits passent désormais par `POST /api/stripe/checkout`.

**Response (410 Gone):**
```json
{
  "message": "Ce endpoint est déprécié. Utilisez Stripe Checkout via POST /api/stripe/checkout."
}
```

---

### GET /api/credits/history
Historique des achats de crédits (paginé, 20 par page).

**Headers:**
```
Authorization: Bearer {token}
```

**Response (200):**
```json
{
  "current_page": 1,
  "data": [
    {
      "id": "01JGAAA...",
      "user_id": "01JGXXX...",
      "amount": "50.00",
      "stripe_payment_id": "pi_xxx...",
      "created_at": "2026-01-15T10:10:00.000000Z"
    }
  ],
  "total": 1,
  "per_page": 20
}
```

---

## 💰 Stripe Checkout (Paiements)

### GET /api/subscribe/data
Récupérer les données des plans et packs de crédits (public, sans authentification).

**Query Parameters:**
- `locale` (optional) : Langue des traductions (fr, en, de). Défaut: fr

**Response (200):**
```json
{
  "plans": [
    {
      "key": "basic",
      "name": "Basic",
      "price_chf": 6.90,
      "credits_per_month": 100,
      "stripe_price_id": "price_xxx",
      "is_popular": false,
      "features": ["100 crédits/mois", "5 messages de contexte"]
    }
  ],
  "personas": [
    {
      "key": "bureautique",
      "name": "Bureautique",
      "description": "...",
      "price_chf": 9.90,
      "credits_per_billing_period": 200
    }
  ],
  "packs": [
    {
      "id": "01KHRQ...",
      "label": "Starter",
      "price_chf": 5.00,
      "credits": 50,
      "bonus": null,
      "popular": false
    }
  ],
  "currency": "CHF"
}
```

---

### POST /api/stripe/checkout
Créer une session Stripe Checkout et retourner l'URL de redirection.

**Headers:**
```
Authorization: Bearer {token}
Content-Type: application/json
```

**Request Body (abonnement):**
```json
{
  "type": "subscription",
  "plan_key": "plus",
  "persona_keys": ["bureautique"]
}
```

Les personas sélectionnables ont un `stripe_price_id` configuré. Le paramètre `persona_keys` est optionnel.

**Request Body (pack de crédits):**
```json
{
  "type": "pack",
  "pack_id": "01KHRQ..."
}
```

**Paramètres:**
- `type` (required) : `subscription` ou `pack`
- `plan_key` (required si type=subscription) : `basic`, `plus` ou `pro`
- `persona_keys` (optional) : tableau de clés de personas à ajouter à l'abonnement (ex: `["bureautique"]`)
- `pack_id` (required si type=pack) : ID du pack de crédits

**Response (200):**
```json
{
  "checkout_url": "https://checkout.stripe.com/c/pay/cs_test_..."
}
```

**Error (422):**
```json
{
  "error": "Plan invalide ou indisponible"
}
```

**Codes promo :** si `STRIPE_ALLOW_PROMOTION_CODES` est activé (défaut), la page Stripe Checkout affiche un champ pour saisir un code promotionnel (configuré dans le Dashboard Stripe : Coupons + codes promotionnels).

**Flux après paiement:**
1. L'utilisateur est redirigé vers Stripe Checkout
2. Après paiement, redirection vers `/subscribe/success` ou `/subscribe/cancel`
3. Stripe envoie un webhook qui finalise l'opération côté backend

---

### POST /api/stripe/billing-portal
Créer une session du **portail client Stripe** et retourner l’URL de redirection (factures, carte, annulation selon le Dashboard Stripe).

**Headers:**
```
Authorization: Bearer {token}
Content-Type: application/json
```

**Request Body:**
```json
{
  "return_url": "https://votre-app.com/subscribe"
}
```

L’utilisateur doit avoir un `stripe_customer_id` (après un checkout ou un client créé côté API).

**Sécurité :** `return_url` doit être une URL **http(s)** dont le **hôte** correspond à celui de `APP_URL` (protection contre les redirections ouvertes). En production, seul **https** est accepté. Des hôtes supplémentaires (ex. `127.0.0.1` si `APP_URL` utilise `localhost`) peuvent être listés dans `.env` : `STRIPE_BILLING_PORTAL_EXTRA_HOSTS` (CSV).

**Response (200):**
```json
{
  "url": "https://billing.stripe.com/session/..."
}
```

**Error (422):** sans client Stripe ou URL invalide.

---

## 🏢 Facturation organisation (B2B)

Routes:

- `GET /api/organizations/{organization}/billing/catalog`
- `POST /api/organizations/{organization}/billing/checkout`
- `POST /api/organizations/{organization}/billing/billing-portal`

Contraintes de sécurité:

- Auth requise (`auth:sanctum`, `user.active`).
- Ability PAT: `billing:write` pour les routes `POST`.
- Policy `OrganizationPolicy`:
  - `view` pour `catalog`,
  - `manageBilling` pour `checkout` et `billing-portal`.
- Rate limiting défensif: `throttle:20,1` sur les endpoints d’écriture billing org.

Référence d’architecture: [`docs/backend/authz-tenancy.md`](../backend/authz-tenancy.md).

---

### POST /api/stripe/webhook
Recevoir et traiter les événements Stripe (public, vérifié par signature).

**Headers:**
```
Stripe-Signature: t=...,v1=...
```

**Événements gérés:**

| Événement | Action |
|-----------|--------|
| `checkout.session.completed` (subscription) | Créer Subscription + items (plan + personas) + crédits chat + crédits persona |
| `checkout.session.completed` (payment) | Créer CreditPurchase + crédits achetés |
| `customer.subscription.updated` | Mettre à jour `expires_at` et champs Stripe (`stripe_status`, `cancel_at_period_end`, etc.) ; resynchroniser `billing_subscription_items` |
| `invoice.paid` | Renouveler crédits chat + crédits persona (idempotence via invoice_id) |
| `customer.subscription.deleted` | Détacher l’utilisateur, supprimer items et crédits persona, **vider les crédits mensuels** |

**Response:** `200` (succès) ou `400` (signature invalide)

---

## 📦 Abonnements (Legacy)

### POST /api/subscribe
Souscrire à un plan d'abonnement (legacy, utiliser `POST /api/stripe/checkout` de préférence).

**Headers:**
```
Authorization: Bearer {token}
Content-Type: application/json
```

**Request Body:**
```json
{
  "plan": "plus",
  "stripe_subscription_id": "sub_xxx..."
}
```

**Plans disponibles (CHF):**
- `basic` - 100 crédits/mois - 6.90 CHF (engagement mensuel indicatif)
- `plus` - 500 crédits/mois - 14.90 CHF
- `pro` - 2000 crédits/mois - 59.90 CHF

---

### PATCH /api/subscribe/plan
Changer de plan (upgrade/downgrade) sur l'abonnement Stripe actif.

**Headers:**
```
Authorization: Bearer {token}
Content-Type: application/json
```

**Request Body:**
```json
{
  "plan_key": "pro"
}
```

### POST /api/subscribe/personas/{personaKey}
Ajouter un persona à l'abonnement actif.

### DELETE /api/subscribe/personas/{personaKey}
Retirer un persona de l'abonnement actif. N'impacte pas le plan principal.

---

### POST /api/subscribe/cancel
Annuler l’abonnement **via Stripe** : par défaut **fin de période** (`cancel_at_period_end`), l’accès reste jusqu’à `expires_at` (synchronisé par webhooks).

**Headers:**
```
Authorization: Bearer {token}
Content-Type: application/json
```

**Request Body (optionnel):**
```json
{
  "immediate": true
}
```

- Sans `immediate` ou `immediate: false` : annulation en fin de période (recommandé).
- `immediate: true` : annulation immédiate côté Stripe (webhook `customer.subscription.deleted`).

Exige un `stripe_subscription_id` sur la subscription locale.

**Response (200)** — fin de période :
```json
{
  "message": "Votre abonnement reste actif jusqu'à la fin de la période en cours.",
  "subscription": {
    "id": "01JGXXX...",
    "plan": "plus",
    "expires_at": "2026-04-24T12:00:00+00:00",
    "stripe_status": "active",
    "cancel_at_period_end": true,
    "stripe_canceled_at": null
  }
}
```

**Response (200)** — immédiat :
```json
{
  "message": "Annulation immédiate demandée. La résiliation sera confirmée sous peu.",
  "immediate": true
}
```

**Error (422):** pas d’abonnement Stripe (`stripe_subscription_id` manquant) ou erreur API.

---

### POST /api/subscribe/resume
Annuler une **annulation planifiée** (`cancel_at_period_end`) si Stripe l’autorise encore.

**Headers:**
```
Authorization: Bearer {token}
```

**Response (200):** objet `subscription` mis à jour (comme ci-dessus).

**Error (422):** pas d’abonnement Stripe actif.

---

## 📊 Codes de statut HTTP

| Code | Signification |
|------|---------------|
| 200  | OK - Requête réussie |
| 201  | Created - Ressource créée |
| 400  | Bad Request - Signature webhook invalide |
| 401  | Unauthorized - Token manquant ou invalide |
| 402  | Payment Required - Crédits insuffisants |
| 404  | Not Found - Ressource non trouvée |
| 410  | Gone - Endpoint déprécié (ex: `topup` direct) |
| 422  | Unprocessable Entity - Validation échouée |
| 429  | Too Many Requests - Limite de débit (ex. auth publique) |
| 500  | Internal Server Error - Erreur serveur |

---

## 🔐 Sécurité

- Tous les mots de passe sont hashés avec bcrypt
- Les tokens sont générés avec Laravel Sanctum ; une **durée de vie optionnelle** (en minutes) peut être définie via `SANCTUM_TOKEN_EXPIRATION` dans `.env` (voir `.env.example`)
- Les prompts sont hashés en SHA-256 pour éviter les duplications
- Validation stricte sur toutes les entrées ; pièces jointes **Persona** : types MIME alignés sur la configuration admin (`allowed_document_mimes`, flags `allow_*`)
- Réponses HTML (Inertia) : en-têtes `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`, et **CSP avec nonces Vite / Ziggy** lorsque `APP_ENV=production` (voir `config/security.php`, `CSP_ENABLED`, `CSP_REPORT_ONLY` dans `.env.example`). Omise sur `/admin/*` (Filament). En local, la CSP est désactivée sauf `CSP_ENABLED=true` (le HMR Vite peut exiger des ajustements sur `connect-src`).
- Authentification publique : limitation de débit sur `/api/register` et `/api/login` (voir ci-dessus)

---

## 💡 Exemples avec cURL

### Inscription
```bash
curl -X POST http://localhost:8000/api/register \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Doe",
    "email": "john@example.com",
    "password": "password123",
    "password_confirmation": "password123"
  }'
```

### Chat
```bash
curl -X POST http://localhost:8000/api/chat \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Bonjour!",
    "theme": "general",
    "level": "light"
  }'
```

### Consulter le solde
```bash
curl -X GET http://localhost:8000/api/credits/balance \
  -H "Authorization: Bearer YOUR_TOKEN"
```

---

## 🧪 Tests

Pour tester l'API, vous pouvez utiliser:
- **Postman** - Importer la collection
- **Insomnia** - Tester les endpoints
- **cURL** - Ligne de commande
- **PHPUnit** - Tests automatisés

```bash
php artisan test
```

---

## 📝 Notes

- Les crédits sont calculés en fonction des tokens utilisés par OpenRouter
- Le hash du prompt évite les requêtes en double (cache potentiel)
- Les abonnements expirent automatiquement après 30 jours
- Un bonus de 10 crédits est offert à l'inscription
- Tous les montants sont en **CHF** (franc suisse)
- Les paiements passent par **Stripe Checkout** (mode subscription ou payment)
- Les plans et packs de crédits sont configurés en base de données avec traductions dynamiques
- Pour tester les paiements, utiliser la carte Stripe de test : `4242 4242 4242 4242`
- Documentation complète Stripe : [stripe-integration.md](../backend/stripe-integration.md)

## 🧠 Système de Contexte Adaptatif

DahuGPT utilise un système intelligent de gestion du contexte des conversations :

### Comment ça marche ?

Lorsque vous envoyez un message avec un `conversation_id`, le système :
1. Charge les N derniers messages de la conversation (N dépend de votre plan)
2. Vérifie que le total ne dépasse pas la limite de tokens
3. Ajuste automatiquement si nécessaire
4. Envoie tout le contexte au modèle IA

### Limites par Plan

| Plan | Messages | Tokens | Prix/mois |
|------|----------|--------|-----------|
| **Basic** | 5 | 2 000 | 6.90 CHF |
| **Plus** | 15 | 8 000 | 14.90 CHF |
| **Pro** | 30 | 16 000 | 59.90 CHF |

### Limites par Modèle

Le système respecte également les limites de contexte des modèles :

| Modèle | Contexte Max |
|--------|--------------|
| Claude 3.5 Sonnet | 200k tokens |
| GPT-4 Turbo | 128k tokens |
| Deepseek V3 | 64k tokens |
| Gemini 2.0 Flash | 1M tokens |

### Exemple d'Utilisation

**Premier message (sans contexte):**
```bash
curl -X POST http://localhost:8000/api/chat \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Qui était Einstein ?",
    "conversation_id": "conv-123"
  }'
```

**Deuxième message (avec contexte):**
```bash
curl -X POST http://localhost:8000/api/chat \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Quelles étaient ses découvertes ?",
    "conversation_id": "conv-123"
  }'
```

Le modèle IA reçoit automatiquement le premier message et sa réponse comme contexte.

### Avantages

✅ **Réponses cohérentes** : L'IA se souvient de la conversation  
✅ **Automatique** : Pas besoin de gérer manuellement l'historique  
✅ **Optimisé** : Ajustement intelligent selon votre plan  
✅ **Économique** : Cache pour réduire les coûts

Pour plus de détails, consultez [context-system.md](context-system.md).
