# Recalled - Documentation développeur

Ce fichier est la documentation Recalled complète, concaténée en un seul document markdown. Donne-le en contexte à un LLM pour obtenir un agent qui comprend l'API Recalled.

Généré le 2026-05-14.

---

<!-- Démarrage / Vue d'ensemble -->

# Recalled

**Les logs d'audit en SaaS** pour les produits B2B et B2C.

Recalled stocke "qui a fait quoi, quand, depuis où" pour chaque action de tes utilisateurs, et te donne un log signé, cherchable et exportable à montrer à tes clients, tes auditeurs et ton équipe juridique.

## Pourquoi

- **Prêt pour la conformité** : SOC 2, ISO 27001, RGPD exigent un audit trail. Recalled t'en livre un clé en main avec hébergement UE, chiffrement AES-256 au repos et une hash chain cryptographique.
- **Ne pollue pas ta base** : les logs d'audit grossissent vite et ralentissent tes requêtes en prod. Recalled stocke les events chez lui, indexés pour la recherche, avec rétention configurable.
- **UI embeddable** : un widget admin interne (composant React) pour tes ingés support, ton équipe ops, tes SRE et tes reviewers compliance. Branche-le dans le back-office que ton équipe utilise déjà pour opérer le produit, ils parcourent "qui a fait quoi" sans quitter leur workflow.

## Comment ça marche

1. Crée un projet dans le dashboard.
2. Génère une clé API.
3. Installe le [SDK npm](/docs/sdk) ou appelle l'[API REST depuis n'importe quel langage](/docs/any-language).
4. Envoie des events : `client.events.create({ action, actor, targets, metadata })`.
5. Relis-les via le dashboard, l'API, ou le composant embeddable.

## Livre ton premier event en 2 minutes

```ts
import { Recalled } from "@recalled/sdk";

const client = new Recalled({
  apiKey: process.env.RECALLED_API_KEY!,
});

await client.events.create({
  action: "invoice.deleted",
  actor: { id: "user_123", email: "alice@example.com" },
  organization: "org_abc",
  targets: [{ type: "invoice", id: "inv_42" }],
  metadata: { reason: "duplicate" },
});
```

C'est fait. Tu logges.

## Setup avec un LLM

Si tu intègres Recalled avec l'aide de Claude, Cursor, ChatGPT ou n'importe quel assistant IA de code, colle le prompt ci-dessous dans le contexte de l'assistant en premier. Il lui dit les règles de quoi logger, quoi skipper, comment nommer les actions, et quoi mettre dans metadata, pour qu'il arrête de te poser 50 questions et livre une intégration propre du premier coup.

```
Tu intègres Recalled (audit logs as a service) dans une app existante.

# Règles
1. Logue les changements d'état, pas les lectures. Un user qui lit un dashboard 50 fois n'est pas un audit event. Un user qui change son email, oui.
2. Logue les actions à conséquences, pas le bruit technique. Health checks, cache miss, réponses 304 : pas ici. Ça va dans l'APM.
3. Logue ce qui raconte une histoire. Dans 6 mois quelqu'un va demander "qui a fait ça et quand". La réponse doit venir d'un seul event avec actor, target, raison, IP, moment.

# Quoi logger

Authentification : user.signed_up, user.logged_in, user.logged_out, user.login_failed, user.password_changed, user.password_reset_requested, user.password_reset_completed, user.email_changed, user.two_factor_enabled, user.two_factor_disabled, user.session_revoked, magic_link.sent, magic_link.consumed, oauth.linked, oauth.unlinked.

Autorisation : member.invited, member.joined, member.removed, member.role_changed, team.created, team.deleted, permission.granted, permission.revoked, api_key.created, api_key.revoked, sharing.granted, sharing.revoked, ownership.transferred.

Cycle de vie sur chaque objet métier (invoice, project, document, etc.) : <objet>.created, <objet>.updated, <objet>.deleted, <objet>.archived, <objet>.restored, <objet>.published, <objet>.unpublished, <objet>.duplicated, <objet>.moved.

Argent : subscription.created, subscription.updated, subscription.canceled, subscription.plan_changed, invoice.created, invoice.paid, invoice.failed, invoice.refunded, payment.succeeded, payment.failed, refund.issued, refund.completed, coupon.applied, coupon.expired, payment_method.added, payment_method.removed, payment_method.set_default.

Actions admin (toujours, sans exception) : admin.impersonation_started, admin.impersonation_ended, admin.user_unlocked, admin.user_locked, admin.feature_toggled, admin.data_overridden, admin.support_intervention.

Exports et imports : export.started, export.completed, export.failed, import.started, import.completed, import.failed, bulk_delete.requested, bulk_delete.completed, gdpr.access_request, gdpr.erasure_request.

Intégrations : integration.connected, integration.disconnected, webhook.created, webhook.updated, webhook.deleted, webhook.delivery_failed (uniquement après que les retries soient épuisés).

Sécurité : security.brute_force_detected, security.suspicious_login, security.rate_limit_exceeded (uniquement si persistant), security.csp_violation_reported, security.api_key_leaked.

Système et jobs background (uniquement quand significatif) : cron.<nom>.completed, cron.<nom>.failed (un par run, pas par item), migration.applied, migration.rolled_back, backup.created, backup.restored.

# Quoi skipper

- Requêtes GET, page views, lectures de dashboard, scroll
- Auto-saves et drafts si sauvegardés toutes les quelques secondes
- Refresh de token, health checks, vérifs CSRF
- Vérifs de permissions (chaque requête API en fait)
- Chaque livraison de webhook réussie, chaque itération de batch, chaque invalidation de cache
- Heartbeats et liveness probes
- Tout ce qui dépasse ~2 KB en metadata

# Convention de nommage

Format : <domaine>.<sujet>.<verbe_passé>, tout en minuscules, séparé par points, snake_case dans un segment si nécessaire. Verbes au passé toujours (.created pas .create, .deleted pas .delete). Sois cohérent : ne mélange pas .deleted et .removed pour le même domaine.

# Metadata

À toujours inclure quand pertinent : source (web, mobile, api, admin_panel, automation, import, webhook), reason (texte libre si l'utilisateur en a fourni une), request_id (id de corrélation), result (success ou failure).

Pour les updates : tableau changed_fields avec les noms de champs, plus before/after uniquement pour les petits diffs.

Pour l'argent : amount_cents (entier, jamais float), currency, provider id (stripe_payment_intent_id ou équivalent).

Pour les échecs : result: "failure", reason, code.

# Antipatterns

Ne jamais mettre dans metadata : secrets en clair, mots de passe, tokens complets, numéros de carte complets, corps de documents complets, contenu de fichiers, blobs, PII non nécessaire, stack traces, requêtes SQL, quoi que ce soit de plus de ~2 KB.

# Comment ajouter l'appel SDK

Utilise @recalled/sdk :
  import { Recalled } from "@recalled/sdk";
  const client = new Recalled({ apiKey: process.env.RECALLED_API_KEY });
  client.events.emit({ action: "...", actor: {...}, organization: "...", targets: [...], metadata: {...} });

Utilise emit() (résilient, non bloquant) par défaut. Utilise create() (throw en cas d'échec) uniquement quand le log d'audit fait partie de la condition de succès de la requête.

Maintenant parcours la codebase, trouve les endroits qui matchent le catalogue ci-dessus, et ajoute les appels client.events.emit appropriés. Skip tout ce qui ne matche pas.
```

Pour le guide opinion complet sur quoi logger, voir [Quoi logger](/docs/what-to-log). Le serveur MCP expose aussi ça comme un tool : `get_setup_guide` retourne le même prompt pour que les agents IA puissent le lire à la demande.

---

<!-- Démarrage / API REST -->

# API REST

Recalled est une API HTTPS + JSON. N'importe quel langage avec un client HTTP peut envoyer des events, pas seulement Node. Le [SDK npm](/docs/sdk) est un simple wrapper au-dessus de ces mêmes endpoints.

## Base URL

```
https://api.recalled.dev/v1
```

## Headers requis

Chaque requête vers `/v1/*` porte :

```
Authorization: Bearer rec_live_<prefix>_<secret>
Content-Type: application/json
```

`Content-Type` n'est requis que sur les requêtes avec body (`POST`, `PUT`, `PATCH`). Voir [Authentification](/docs/authentication) pour le format des clés et les scopes.

## Enveloppe de réponse

Les réponses à ressource unique sont encapsulées dans `data` :

```json
{
  "data": { "id": "evt_01HX...", "action": "invoice.deleted" }
}
```

Les listes paginées ajoutent un curseur :

```json
{
  "data": [{ "id": "evt_01HX..." }],
  "nextCursor": "2026-04-14T09:12:45.000Z"
}
```

Les erreurs ont toujours la même forme :

```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "action is required",
    "details": {}
  }
}
```

Catalogue complet : [Codes d'erreur](/docs/errors).

## Endpoints

| Méthode | Path | Rôle |
|---|---|---|
| `POST` | `/v1/events` | Ingérer un nouvel event |
| `GET` | `/v1/events` | Lister les events, curseur + filtres |
| `GET` | `/v1/events/search` | Recherche full-text |
| `GET` | `/v1/events/:id` | Lire un event |
| `GET` | `/v1/events/verify` | Vérifier hash chain et signatures |
| `GET` | `/v1/exports` | Télécharger un export CSV ou JSON |
| `DELETE` | `/v1/actors/:id` | Effacement RGPD d'un acteur |
| `POST` | `/v1/embed/token` | Créer un embed token court-vivant |

Chaque endpoint est documenté en détail dans [API Events](/docs/events), [RGPD](/docs/gdpr) et [UI embeddable](/docs/embed).

## Essaie depuis ton terminal

```bash
curl -X POST https://api.recalled.dev/v1/events \
  -H "Authorization: Bearer $RECALLED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "user.login",
    "actor": { "id": "user_123" },
    "organization": "org_acme"
  }'
```

## Pagination

Les endpoints de liste et de recherche utilisent une **pagination par curseur** sur `occurred_at`. La réponse contient `nextCursor` ; repasse-le à l'appel suivant jusqu'à ce qu'il revienne à `null`.

```bash
curl "https://api.recalled.dev/v1/events?limit=50&cursor=2026-04-14T09:12:45.000Z" \
  -H "Authorization: Bearer $RECALLED_API_KEY"
```

## Rate limits

- `POST /v1/events` : 1200 requêtes par minute et par clé API
- Autres endpoints : 1500 requêtes par minute et par IP

Chaque réponse porte les headers IETF `RateLimit-Limit`, `RateLimit-Remaining` et `RateLimit-Reset`. Voir [Rate limits](/docs/rate-limits).

## Pour continuer

- [Utiliser depuis n'importe quel langage](/docs/any-language), snippets prêts à l'emploi en Python, Go, PHP, Ruby, Java, Rust et plus
- [Authentification](/docs/authentication), format des clés, scopes, embed tokens
- [API Events](/docs/events), chaque champ et query param documenté

---

<!-- Démarrage / Utiliser depuis n'importe quel langage -->

# Utiliser depuis n'importe quel langage

L'API REST, c'est du HTTPS + JSON pur. N'importe quel langage avec un client HTTP peut envoyer des events. Ci-dessous, le même appel `POST /v1/events` écrit de manière idiomatique dans plusieurs langages. Insère ta clé API, lance, terminé.

Chaque exemple cible :

```
POST https://api.recalled.dev/v1/events
Authorization: Bearer $RECALLED_API_KEY
Content-Type: application/json
```

Avec le body :

```json
{
  "action": "invoice.deleted",
  "actor": { "id": "user_123", "email": "alice@example.com" },
  "organization": "org_acme",
  "targets": [{ "type": "invoice", "id": "inv_42" }],
  "metadata": { "reason": "duplicate" }
}
```

## curl

```bash
curl -X POST https://api.recalled.dev/v1/events \
  -H "Authorization: Bearer $RECALLED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "invoice.deleted",
    "actor": { "id": "user_123", "email": "alice@example.com" },
    "organization": "org_acme",
    "targets": [{ "type": "invoice", "id": "inv_42" }],
    "metadata": { "reason": "duplicate" }
  }'
```

## Node.js (fetch, sans SDK)

Si tu ne veux pas du SDK et préfères un `fetch` brut :

```js
const response = await fetch("https://api.recalled.dev/v1/events", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RECALLED_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    action: "invoice.deleted",
    actor: { id: "user_123", email: "alice@example.com" },
    organization: "org_acme",
    targets: [{ type: "invoice", id: "inv_42" }],
    metadata: { reason: "duplicate" },
  }),
});

if (!response.ok) throw new Error(await response.text());
const { data: event } = await response.json();
```

## Python (requests)

```python
import os
import requests

response = requests.post(
    "https://api.recalled.dev/v1/events",
    headers={
        "Authorization": f"Bearer {os.environ['RECALLED_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "action": "invoice.deleted",
        "actor": {"id": "user_123", "email": "alice@example.com"},
        "organization": "org_acme",
        "targets": [{"type": "invoice", "id": "inv_42"}],
        "metadata": {"reason": "duplicate"},
    },
    timeout=10,
)
response.raise_for_status()
event = response.json()["data"]
```

## Go (net/http)

```go
package main

import (
    "bytes"
    "encoding/json"
    "net/http"
    "os"
    "time"
)

func main() {
    body, _ := json.Marshal(map[string]any{
        "action":       "invoice.deleted",
        "actor":        map[string]any{"id": "user_123", "email": "alice@example.com"},
        "organization": "org_acme",
        "targets":      []map[string]any{{"type": "invoice", "id": "inv_42"}},
        "metadata":     map[string]any{"reason": "duplicate"},
    })

    req, _ := http.NewRequest("POST", "https://api.recalled.dev/v1/events", bytes.NewReader(body))
    req.Header.Set("Authorization", "Bearer "+os.Getenv("RECALLED_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
}
```

## PHP (curl)

```php
<?php
$body = json_encode([
    "action" => "invoice.deleted",
    "actor" => ["id" => "user_123", "email" => "alice@example.com"],
    "organization" => "org_acme",
    "targets" => [["type" => "invoice", "id" => "inv_42"]],
    "metadata" => ["reason" => "duplicate"],
]);

$ch = curl_init("https://api.recalled.dev/v1/events");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $body,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("RECALLED_API_KEY"),
        "Content-Type: application/json",
    ],
]);
$response = curl_exec($ch);
curl_close($ch);
```

## Ruby (Net::HTTP)

```ruby
require "net/http"
require "json"
require "uri"

uri = URI("https://api.recalled.dev/v1/events")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV['RECALLED_API_KEY']}"
request["Content-Type"] = "application/json"
request.body = {
  action: "invoice.deleted",
  actor: { id: "user_123", email: "alice@example.com" },
  organization: "org_acme",
  targets: [{ type: "invoice", id: "inv_42" }],
  metadata: { reason: "duplicate" },
}.to_json

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end
```

## Java (java.net.http)

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

String body = """
{
  "action": "invoice.deleted",
  "actor": { "id": "user_123", "email": "alice@example.com" },
  "organization": "org_acme",
  "targets": [{ "type": "invoice", "id": "inv_42" }],
  "metadata": { "reason": "duplicate" }
}
""";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.recalled.dev/v1/events"))
    .header("Authorization", "Bearer " + System.getenv("RECALLED_API_KEY"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response = HttpClient.newHttpClient()
    .send(request, HttpResponse.BodyHandlers.ofString());
```

## Rust (reqwest)

```rust
use serde_json::json;

let client = reqwest::Client::new();
let response = client
    .post("https://api.recalled.dev/v1/events")
    .header(
        "Authorization",
        format!("Bearer {}", std::env::var("RECALLED_API_KEY")?),
    )
    .header("Content-Type", "application/json")
    .json(&json!({
        "action": "invoice.deleted",
        "actor": { "id": "user_123", "email": "alice@example.com" },
        "organization": "org_acme",
        "targets": [{ "type": "invoice", "id": "inv_42" }],
        "metadata": { "reason": "duplicate" }
    }))
    .send()
    .await?
    .error_for_status()?;
```

## Gestion des erreurs

Toutes les réponses non-2xx ont la même forme :

```json
{
  "error": {
    "code": "PLAN_LIMIT_REACHED",
    "message": "Monthly event quota exceeded",
    "details": { "limit": 5000 }
  }
}
```

- `400` : `VALIDATION_ERROR`, le body a échoué la validation (les champs fautifs sont dans `details`)
- `401` : `UNAUTHORIZED`, `INVALID_API_KEY` ou `REVOKED_API_KEY`, clé manquante, invalide ou révoquée
- `403` : `FORBIDDEN`, la clé est valide mais la feature est gated par le plan
- `429` : `RATE_LIMITED` (regarde `RateLimit-Reset`) ou `PLAN_LIMIT_REACHED` (quota mensuel)
- `5xx` : retry avec backoff

Liste complète dans [Codes d'erreur](/docs/errors).

## Stratégie de retry

Retry sur `408`, `429`, `502`, `503`, `504` avec backoff exponentiel (1s à 10min). Pas de retry sur `400`, `401`, `403`, `404`, ce sont des erreurs permanentes.

Le [SDK npm](/docs/sdk) implémente tout ça pour toi, y compris une queue en mémoire de 24 h pour `emit()`. Si tu es sur Node, utilise le SDK. Sinon, enrobe ton client HTTP dans une boucle de retry.

---

<!-- Concepts de base / Authentification -->

# Authentification

Toutes les requêtes `/v1/*` exigent une **clé API Bearer** dans le header `Authorization`.

```
Authorization: Bearer rec_live_aBcD1234EfGh5678...
```

## Format de clé

- `rec_live_<random>`, clé de production, envoie de vrais events.
- `rec_test_<random>`, clé de test, même comportement, facile à distinguer.

Génère les clés depuis le dashboard (une par environnement). Les clés sont hashées (SHA-256) côté serveur, **le secret complet n'est affiché qu'une seule fois à la création**. Si tu le perds, révoque et génère une nouvelle clé.

## Scopes

Chaque clé peut être limitée à un sous-ensemble d'actions :

- `events:write`, ingérer de nouveaux events
- `events:read`, lister, chercher, lire
- `exports:read`, télécharger des exports CSV/JSON
- `actors:delete`, droit à l'effacement RGPD
- `embed:write`, créer des embed tokens à courte durée

## Embed tokens

Pour le widget admin interne `<RecalledFeed />`, tu crées un token court-vivant côté serveur et tu le passes au navigateur. Le navigateur parle à `/v1/embed/events` avec ce token plutôt qu'avec la clé API, ta clé ne quitte donc jamais ton serveur. Par défaut le token donne accès à tout le projet (vue admin) ; passe `organization` si tu veux restreindre une instance de widget à un seul tenant.

```ts
const { token } = await client.embed.createToken({
  organization: "org_abc",
  ttlSeconds: 900,
});
// renvoie `token` au navigateur
```

---

<!-- Concepts de base / API Events -->

# API Events

L'API events est la manière dont ton app pousse ses enregistrements d'audit dans Recalled et les relit.

Les exemples ci-dessous montrent les payloads JSON. Pour des snippets prêts à l'emploi en curl, Python, Go, PHP, Ruby, Java et Rust, voir [Utiliser depuis n'importe quel langage](/docs/any-language).

## Créer un event

`POST /v1/events`

```json
{
  "action": "invoice.deleted",
  "actor": {
    "type": "user",
    "id": "user_123",
    "name": "Alice",
    "email": "alice@example.com"
  },
  "organization": "org_abc",
  "targets": [{ "type": "invoice", "id": "inv_42" }],
  "metadata": { "reason": "duplicate" },
  "occurred_at": "2026-04-14T09:12:45.000Z"
}
```

**Requis** : `action`. **Recommandés** : `actor.id`, `organization`. Le reste est de la métadonnée optionnelle.

Le serveur calcule un hash SHA-256 chaîné au précédent event du projet ET une signature HMAC-SHA256 du payload canonique, avec une clé qui vit hors base de données. La chaîne détecte les réordonnancements et les trous ; la signature détecte les réécritures de contenu. Appelle [`GET /v1/events/verify`](#verifier-la-chaine) pour auditer les deux en un seul appel.

## Référence des champs

Chaque champ que tu peux envoyer sur `POST /v1/events`, avec son type, s'il est requis et à quoi il sert.

### `action`, requis

String, 1 à 255 caractères. Le nom de l'action au style verbe/événement. **C'est le seul champ obligatoire.**

Recalled n'impose aucune convention de nommage mais on recommande `domaine.sujet.verbe` séparé par des points, au passé :

- Bien : `user.logged_in`, `invoice.deleted`, `billing.subscription.updated`, `api_key.rotated`
- À éviter : `click`, `error`, `something happened`, `User Login`

Un nommage cohérent paie plus tard : c'est ce qui rend possible le filtrage exact (`?action=user.delete`), les règles de rétention wildcard (`user.*`) et la recherche full-text.

Pour la convention complète, la liste des verbes standards et le catalogue catégorie par catégorie de quoi logger, voir [Quoi logger](/docs/what-to-log).

### `organization`, optionnel

String, max 128 caractères. L'**identifiant du tenant dans ton propre produit**, pas un concept Recalled.

Si ton SaaS est multi-tenant, mets l'ID interne de ton client/tenant ici (ex. `org_acme`, `tenant_42`). Recalled l'utilise pour :

- Filtrer les events dans le dashboard et l'API (`?organization=org_acme`)
- Restreindre un embed token pour que `<RecalledFeed />` serve de drill-down par tenant dans ton panneau admin
- Router les suppressions RGPD par organisation si besoin

Si ton app est mono-tenant ou si l'event n'est pas lié à un client spécifique (cron, tâches système), laisse vide.

### `actor`, objet optionnel

Qui a fait l'action. Tous les sous-champs sont optionnels mais `actor.id` est fortement recommandé dès qu'un humain déclenche l'action.

| Sous-champ | Type | Contrainte | Rôle |
|---|---|---|---|
| `actor.id` | string | 1-255 caractères | ID utilisateur stable de ta DB. Permet le filtrage par user et la suppression RGPD via `DELETE /v1/actors/:id` |
| `actor.type` | string | 1-64 caractères | `user`, `service`, `api_key`, `system`, etc. Distingue humain et automatisé |
| `actor.name` | string | max 255 caractères | Nom d'affichage, visible dans le dashboard et le feed embed |
| `actor.email` | string | max 255 caractères, email valide | Optionnel, affiché dans le dashboard |

Omets `actor` complètement pour les events système (cron, migration, tâches au démarrage).

### `targets`, tableau optionnel

Liste des ressources sur lesquelles l'action a porté. Max 20 entrées par event, et le JSON sérialisé du tableau entier doit rester **sous 4 KB**. Chaque entrée a :

| Sous-champ | Type | Contrainte | Rôle |
|---|---|---|---|
| `type` | string | 1-64 caractères, requis | Type de ressource (`invoice`, `project`, `api_key`) |
| `id` | string | 1-255 caractères, requis | ID de la ressource dans ta DB |
| `name` | string | max 255 caractères, optionnel | Nom d'affichage |

Exemple, un user déplace 2 éléments dans un dossier :

```json
{
  "action": "folder.items.moved",
  "actor": { "id": "user_1" },
  "targets": [
    { "type": "item", "id": "item_a", "name": "Facture T1" },
    { "type": "item", "id": "item_b", "name": "Facture T2" },
    { "type": "folder", "id": "folder_archive", "name": "Archive" }
  ]
}
```

### `metadata`, objet optionnel

JSON libre. Mets tout ce que tu veux garder sur le contexte :

```json
{
  "metadata": {
    "reason": "duplicate",
    "source": "admin_panel",
    "diff": { "before": "draft", "after": "paid" }
  }
}
```

Aucun schéma imposé, donc c'est flexible mais pas cherchable par clé interne. Le JSON sérialisé doit rester **sous 8 KB**, un event typique pèse bien moins d'1 KB. Au-delà, l'API rejette l'event en HTTP 413.

### `occurred_at`, optionnel, ISO 8601

Quand l'action s'est réellement produite, du point de vue de ton app. Format `2026-04-14T09:12:45.000Z`.

**Si tu l'omets, le serveur timestampe l'event à l'ingest.** C'est ce que tu veux pour du logging temps réel. Ne le mets explicitement que pour rejouer un historique ou quand il y a un délai significatif entre l'action et l'appel API.

## Limites de taille par event

Le payload de chaque event est capé à l'ingest. Ces limites s'appliquent à `POST /v1/events` uniquement.

| Champ | Limite |
|---|---|
| `action` | 255 caractères |
| `metadata` | 8 KB de JSON sérialisé |
| `targets` | 4 KB de JSON sérialisé, 20 entrées max |
| `actor.id`, `actor.name`, `actor.email` | 255 caractères chacun |

Un event typique pèse **moins de 500 octets au total**. Les caps sont environ 20× la taille moyenne d'un `metadata`, assez large pour absorber un event richement taggé sans laisser la porte ouverte à un client qui dumperait par accident une stack trace, un body de requête ou un document complet dans un seul event.

Quand un payload dépasse, l'API renvoie :

```http
HTTP/1.1 413 Payload Too Large
Content-Type: application/json

{
  "error": {
    "code": "EVENT_TOO_LARGE",
    "message": "metadata is too large: 12453 bytes, limit is 8192",
    "details": {
      "field": "metadata",
      "size": 12453,
      "limit": 8192
    }
  }
}
```

Si tu butes dessus régulièrement dans des cas légitimes, tu veux probablement scinder la donnée : logue un event léger qui pointe vers une ressource externe (clé S3, URL blob storage) au lieu d'inliner le payload lui-même.

## Champs que le serveur remplit

Tu n'envoies jamais ceux-là, Recalled les ajoute à l'ingest :

| Champ | Signification |
|---|---|
| `id` | UUID attribué à l'ingest |
| `project_id` | Déduit de la clé API |
| `ip_address` | IP de la requête d'ingest |
| `user_agent` | Header User-Agent de la requête d'ingest |
| `hash` | SHA-256 de `prev_hash` concaténé au payload canonique. Preuve de chaînage |
| `prev_hash` | `hash` de l'event précédent du projet, `null` pour le tout premier |
| `signature` | HMAC-SHA256 du payload canonique, préfixé par la version de clé (ex. `v1:...`). Secret jamais stocké en base |
| `anonymized_at` | Timestamp ISO posé quand les PII ont été scrubées via effacement RGPD. `null` sinon |

## Lister les events

`GET /v1/events?limit=50&cursor=<iso>`

Paramètres de requête :
- `limit` (par défaut 50, max 200)
- `cursor`, timestamp ISO récupéré depuis `nextCursor` de la page précédente
- `organization`, filtre tenant
- `actor_id`, filtre sur un acteur précis
- `action`, filtre exact sur une seule action
- `actions`, liste d'actions à **inclure**, séparées par virgules (ex : `user.login,user.logout`). Max 50 entrées.
- `actions_exclude`, liste d'actions à **exclure**, séparées par virgules. Max 50 entrées.
- `ip_address`, filtre sur une IP précise
- `date_from`, `date_to`, bornes ISO

Retourne `{ data: Event[], nextCursor: string | null }`.

## Rechercher

`GET /v1/events/search?q=<term>`

Recherche full-text sur `action`, `actor_name`, `actor_email`, `actor_id`. Pagination cursor-based comme list.

Paramètres de requête :
- `q`, requis, le terme recherché (1-255 caractères)
- `limit`, `cursor`, pagination identique à list
- `organization`, `actor_id`, `actions`, `actions_exclude`, `ip_address`, `date_from`, `date_to`, mêmes filtres que list, appliqués par-dessus la recherche textuelle

## Lire un event

`GET /v1/events/:id`

Retourne un event unique (même forme que list items), limité au projet de la clé API.

## Export

`GET /v1/exports?format=csv` ou `format=json`

Stream les events filtrés dans un fichier téléchargeable. Mêmes filtres que list.

## Vérifier la chaîne

`GET /v1/events/verify`

Parcourt tous les events du projet dans l'ordre occurred_at et vérifie :

- **Lien de chaîne** : chaque `prev_hash` doit égaler le `hash` de la ligne précédente.
- **Hash stocké** : recompute `sha256(prev_hash || payload_canonique)` et compare à `hash`.
- **Signature HMAC** : recompute `hmac-sha256(secret, payload_canonique)` et compare à `signature`.

Paramètres optionnels `?from=<ISO>` et `?to=<ISO>` pour restreindre la vérification à une fenêtre.

La réponse renvoie toujours HTTP 200. Le payload dit ce qui s'est passé :

```json
{
  "data": {
    "ok": true,
    "verified": 1284,
    "anonymized": 3,
    "unsigned": 0,
    "gaps": [
      { "at": "2026-03-01T00:00:00.000Z", "reason": "plan_retention", "purged_count": 112 }
    ],
    "failure": null
  }
}
```

En cas d'échec, `ok` passe à `false` et `failure` pointe la ligne fautive :

```json
{
  "data": {
    "ok": false,
    "verified": 842,
    "anonymized": 0,
    "unsigned": 0,
    "gaps": [],
    "failure": {
      "event_id": "01HX...",
      "reason": "signature_mismatch",
      "at": "2026-04-12T14:07:13.000Z"
    }
  }
}
```

Raisons d'échec :

- `hash_mismatch` : le payload d'une ligne ne correspond plus à son `hash` stocké.
- `signature_mismatch` : le payload ne correspond plus à sa `signature` HMAC.
- `chain_broken` : le `prev_hash` pointe dans le vide et aucun `retention_checkpoint` n'explique le trou.

Les lignes anonymisées sont reportées dans `anonymized` (skip propre). Les lignes antérieures au rollout HMAC sont reportées dans `unsigned` ; le script de backfill les signe rétroactivement.

## Receipts : une preuve portable et citable pour un event

`GET /v1/events/:id/receipt`

Retourne un reçu JSON auto-suffisant pour un event, avec deux URLs que tu peux donner à n'importe qui :

```json
{
  "data": {
    "type": "recalled.receipt.v1",
    "event_id": "01HX...",
    "action": "file.deleted",
    "actor": { "type": "agent", "id": "claude-sonnet-4.6" },
    "target": { "type": "file", "id": "f_42" },
    "occurred_at": "2026-05-02T17:42:00.000Z",
    "hash": "...",
    "prev_hash": "...",
    "signature": "v1:...",
    "verification_url": "https://api.recalled.dev/v1/receipts/01HX...",
    "view_url": "https://recalled.dev/receipts/01HX..."
  }
}
```

`view_url` est une page publique qui confirme que l'event existe et que la chaîne est intacte, sans clé API. `verification_url` est la version JSON brute du même check. Utilise ça quand un agent IA doit citer une action qu'il a prise, ou quand tu veux prouver à un client qu'un event a eu lieu sans lui donner accès au dashboard. Voir le guide [Audit d'agents](/docs/agent-audit) pour le pattern complet.

---

<!-- Concepts de base / Audit d'agents -->

# Audit d'agents

Recalled est pensé pour **les actions humaines et celles des agents IA, côte à côte**. En 2026, la moitié des actions d'un SaaS typique sont prises par des **agents IA** (Claude, GPT, agents custom branchés en tool calls), et le même audit log les enregistre avec la même chaîne de hash, les mêmes signatures, le même dashboard. Deux petites conventions en font le système de référence pour ce que tes agents ont fait, quand, et au nom de qui.

## Le pattern

L'agent lui-même n'appelle pas Recalled. **Ton backend** orchestre l'agent, exécute les tool calls, et logue les actions résultantes. Du point de vue de Recalled, un agent est juste un acteur avec un `actor.type` différent.

Deux conventions :

1. `actor.type` est mis à `"agent"` (ou `"ai_agent"`, choisis-en un et tiens-toi-y).
2. `metadata.triggered_by_user` porte l'id du user humain qui a démarré la conversation menant à cette action.

```ts
client.events.emit({
  action: "file.deleted",
  actor: {
    type: "agent",
    id: "claude-sonnet-4.6",
    name: "Support Triage Agent",
  },
  organization: "acme_corp",
  targets: [{ type: "file", id: "f_42", name: "old-report.pdf" }],
  metadata: {
    triggered_by_user: "user_123",
    conversation_id: "conv_xyz",
    tool_call_id: "call_abc",
    reasoning: "user asked to clean up old uploads",
    confidence: "high",
    model: "claude-sonnet-4.6",
    tokens_used: 1240,
    result: "success",
  },
});
```

## Trois events par tool call

Pour la traçabilité complète, logue trois events autour de chaque tool call d'agent :

```ts
// 1. L'agent a décidé d'appeler un tool
client.events.emit({
  action: "agent.tool_called",
  actor: { type: "agent", id: "claude-sonnet-4.6", name: "Support Agent" },
  targets: [{ type: "tool", id: "delete_file" }],
  metadata: {
    triggered_by_user: "user_123",
    conversation_id: "conv_xyz",
    tool_args: { file_id: "f_42" },
  },
});

// 2. L'action elle-même (comme une action humaine)
client.events.emit({
  action: "file.deleted",
  actor: { type: "agent", id: "claude-sonnet-4.6", name: "Support Agent" },
  targets: [{ type: "file", id: "f_42" }],
  metadata: { triggered_by_user: "user_123", on_behalf_of: "user_123" },
});

// 3. Le tool a renvoyé à l'agent
client.events.emit({
  action: "agent.tool_returned",
  actor: { type: "agent", id: "claude-sonnet-4.6", name: "Support Agent" },
  targets: [{ type: "tool_call", id: "call_abc" }],
  metadata: {
    triggered_by_user: "user_123",
    result: "success",
    duration_ms: 142,
  },
});
```

Si la cardinalité devient un problème à grande échelle, drop `tool_called` et `tool_returned` et garde uniquement l'action elle-même avec une metadata riche. L'event d'action seul suffit pour la responsabilité.

## Filtrer : humains vs agents

Une fois `actor.type=agent` posé partout, tu peux filtrer le dashboard ou l'API pour voir uniquement l'activité agents, uniquement l'activité humaine, ou tout pour un user donné :

- Toute l'activité agents ce mois : `?actor_type=agent`
- Toutes les actions qu'un user humain a déclenchées, y compris celles exécutées par des agents en son nom : filtre par `metadata.triggered_by_user`
- Audit trail pour un agent spécifique : `?actor_id=claude-sonnet-4.6`

## Receipts : preuve rejouable

Quand un agent dit "j'ai supprimé ce fichier pour toi", tu veux qu'il appuie sa réponse avec une preuve. Recalled émet un **reçu** pour n'importe quel event :

```bash
curl https://api.recalled.dev/v1/events/$EVENT_ID/receipt \
  -H "Authorization: Bearer rec_live_..."
```

Tu obtiens un objet JSON que l'agent peut coller dans sa réponse, avec un `view_url` (page publique) et un `verification_url` (endpoint JSON sans auth). Le destinataire peut vérifier cryptographiquement que l'event a eu lieu, dans quel ordre, sans altération, **sans clé API**.

Dans Claude Desktop, Cursor ou n'importe quel client MCP connecté à Recalled, le tool `get_event_receipt` retourne le même objet pour que l'agent le cite sans passer par HTTP lui-même.

## Ce que les auditeurs veulent vraiment voir

Si un client ou un auditeur conteste une action d'agent trois mois plus tard, voici ce qui rend le dossier solide :

1. L'action est dans l'audit log avec `actor.type=agent`.
2. Le receipt vérifie vert : chaîne intacte, signature HMAC valide.
3. `metadata.triggered_by_user` lie l'action à l'humain qui l'a initiée.
4. `metadata.reasoning`, `metadata.confidence`, `metadata.model` donnent le contexte.
5. `metadata.conversation_id` permet de rejouer toute la conversation si besoin.

## Page de vérification publique

Chaque receipt a une URL publique du genre `https://recalled.dev/receipts/<event-id>`. La page masque la PII de l'acteur et la metadata (le viewer public ne voit jamais qui ni quoi), et affiche uniquement :

- Le nom de l'action
- Le type d'acteur (agent, user, service, etc.)
- Le timestamp
- Les hashes et la signature
- Un bandeau vert ou rouge selon que la preuve cryptographique tient

Donne cette URL plutôt qu'un accès au dashboard. N'importe qui peut confirmer qu'un event a existé, sans accéder à ton projet.

## Privacy

La page receipt publique n'expose jamais :

- Nom, email, id, IP, user agent de l'acteur
- Metadata de l'event
- Les autres events de la chaîne

Elle prouve uniquement que l'event id spécifique, avec ce verbe d'action et ce timestamp précis, a été enregistré par Recalled et n'a pas été altéré.

---

<!-- Concepts de base / Quoi logger -->

# Quoi logger

Le plus dur en intégrant Recalled, ce n'est pas l'appel SDK, c'est de décider **ce qui mérite d'être loggué**. Trop peu, ton audit trail ne sert à rien. Trop, tu fais exploser ton quota, tu ralentis ton app, et tu noies le signal dans le bruit.

Cette page est le guide opinion. Suis ces règles et tu auras un audit log propre, utile, conforme, sans trop y penser.

## Les 3 règles

**1. Logue les changements d'état, pas les lectures.**
Un user qui consulte son dashboard 50 fois par jour, ce n'est pas un audit event. Un user qui change son adresse email une fois, oui. Si l'action ne mute rien dans ton système ou dans le compte de l'utilisateur, ça n'a pas sa place dans Recalled.

**2. Logue les actions à conséquences, pas le bruit technique.**
Un health check raté, un cache miss, une réponse 304, un purge CDN : ce ne sont pas des audit events. Ça va dans ton APM. Recalled, c'est pour les actions dont un humain ou un auditeur se souciera dans 6 mois.

**3. Logue ce qui raconte une histoire.**
Imagine quelqu'un dans 6 mois qui demande "qui a supprimé cette facture et pourquoi". La réponse doit venir d'un seul event : l'acteur, la cible, la raison s'il y en a une, l'IP, le moment. Si ton entrée de log ne permet pas de reconstruire l'histoire, elle est incomplète.

## Arbre de décision

Quand tu t'apprêtes à ajouter un `client.events.create()`, demande-toi :

1. **Est-ce que ça change un état dans notre système ?** Non → ne logue pas.
2. **L'équipe légale, support ou sécu pourrait-elle un jour demander "qui a fait ça et quand" ?** Non → ne logue pas.
3. **Le volume est-il raisonnable ?** (moins de 10 events par user actif par session) Oui → logue. Non → considère du sampling ou de l'agrégation côté client.

Si tu réponds oui à 1 et 2 et que le volume tient, logue. Sinon, skip.

## Catalogue par catégorie

Liste standard de ce que la plupart des SaaS B2B et B2C devraient logger. Choisis les catégories qui s'appliquent à ton produit, copie les noms d'actions, adapte à ton domaine. Chaque onglet contient les events à logger, ceux à skipper, et un indice metadata.

### Authentification

Toute action qui change qui est connecté ou comment.

**À logger:**
- user.signed_up (création de compte)
- user.logged_in (auth réussie, toute méthode)
- user.logged_out (logout explicite, y compris révocation de session)
- user.login_failed (mauvais mot de passe, token expiré, IP bloquée). Critique pour la sécu.
- user.password_changed
- user.password_reset_requested et user.password_reset_completed (deux events distincts)
- user.email_changed
- user.two_factor_enabled et user.two_factor_disabled
- user.session_revoked (force-logout admin, se déconnecter de tous les appareils)
- magic_link.sent et magic_link.consumed
- oauth.linked et oauth.unlinked (Google, GitHub, etc.)

**À ne pas logger:**
- Page views ou user a navigué vers /dashboard
- Refresh de token (toutes les 5 min, c'est du bruit, logue plutôt les révocations)
- Health checks d'auth
- Vérifications CSRF réussies

**Metadata utile:** auth_provider (email, google, magic_link), ip, user_agent, mfa_used (true/false), success (pour les échecs, inclus la raison : bad_password, account_locked, mfa_required).

### Autorisation

Tout ce qui change qui peut faire quoi.

**À logger:**
- member.invited, member.joined, member.removed
- member.role_changed (toujours inclure avant/après en metadata)
- team.created, team.deleted
- permission.granted, permission.revoked
- api_key.created, api_key.revoked. Ne pas logger chaque utilisation, garder last_used_at sur la clé suffit.
- sharing.granted, sharing.revoked (quand un user partage une ressource avec un autre user ou en externe)
- ownership.transferred (surtout en B2B)

**À ne pas logger:**
- Les vérifications de permissions (chaque requête API en fait, c'est du domaine APM)
- Les accès en lecture seule s'ils auto-expirent rapidement
- Les étapes internes de calcul RBAC

**Metadata utile:** role_before, role_after, scope, granted_by, expires_at.

### Cycle de vie

Le cœur de l'audit logging. Chaque objet métier que tes users créent, modifient, suppriment, partagent, déplacent.

**À logger:**
- <objet>.created (invoice.created, project.created, document.created, etc.)
- <objet>.updated (avec un changed_fields en metadata, pas le diff complet s'il est gros)
- <objet>.deleted (toujours, soft-delete et hard-delete)
- <objet>.archived et <objet>.restored
- <objet>.published, <objet>.unpublished
- <objet>.duplicated (par qui, source dans targets)
- <objet>.moved (changement de dossier, transfert d'ownership, etc.)

**À ne pas logger:**
- Les auto-saves et drafts si ton app sauvegarde toutes les 10 secondes. Logue le save explicite à la place.
- Les lectures, ouvertures, vues (ça va dans le product analytics, pas l'audit)
- Les jobs internes de dénormalisation qui touchent la même ligne

**Metadata utile:** changed_fields (tableau de noms de champs), reason (si le user en a fourni une), source (manual, api, import, automation).

### Argent

Chaque changement d'état monétaire. Sois exhaustif ici, les comptables et ton toi-du-futur te remercieront.

**À logger:**
- subscription.created, subscription.updated, subscription.canceled
- subscription.plan_changed (avec from et to slugs en metadata)
- invoice.created, invoice.paid, invoice.failed, invoice.refunded
- payment.succeeded, payment.failed (inclus la raison du refus)
- refund.issued, refund.completed
- coupon.applied, coupon.expired
- payment_method.added, payment_method.removed, payment_method.set_default

**À ne pas logger:**
- Le ping webhook Stripe lui-même (logue plutôt l'event que représente le webhook)
- Les vérifications de conversion de devise
- Les pré-validations de paiement qui n'ont mené à aucune tentative de débit

**Metadata utile:** amount_cents, currency, stripe_payment_intent_id, provider (stripe, paddle, etc.), reason pour les refunds.

### Actions admin

Tout ce qu'un membre de ton équipe interne fait au nom d'un user. Toujours logger ça, sans exception. C'est ce que les auditeurs regardent en premier.

**À logger:**
- admin.impersonation_started, admin.impersonation_ended
- admin.user_unlocked, admin.user_locked
- admin.feature_toggled (par user ou globalement)
- admin.data_overridden (quand le support corrige manuellement une valeur)
- admin.support_intervention (catch-all pour les corrections ponctuelles)
- N'importe quoi qui passe par ton back-office interne et mute des données client

**Metadata utile:** admin_id, reason (toujours exiger une raison dans l'UI back-office), ticket_id si tu link à ton helpdesk.

### Exports & imports

Données qui sortent ou entrent en masse.

**À logger:**
- export.started, export.completed, export.failed
- import.started, import.completed, import.failed
- bulk_delete.requested, bulk_delete.completed
- gdpr.access_request et gdpr.erasure_request

**Metadata utile:** format (csv, json), row_count, destination pour les exports, source pour les imports, size_bytes.

### Intégrations

Les events qui impliquent des tiers.

**À logger:**
- integration.connected, integration.disconnected
- webhook.created, webhook.updated, webhook.deleted
- webhook.delivery_failed (après que les retries soient épuisés, pas chaque retry)
- oauth.token_refreshed seulement si ça représente un event significatif (forced re-auth), sinon skip

**À ne pas logger:**
- Chaque livraison de webhook réussie
- Les health checks contre les intégrations

### Sécurité

Ce dont ton équipe sécu a besoin de visibilité.

**À logger:**
- security.brute_force_detected (après le seuil)
- security.suspicious_login (nouveau pays, nouveau device, etc.)
- security.rate_limit_exceeded (uniquement si ça persiste, pas chaque hit)
- security.csp_violation_reported
- security.api_key_leaked (si tu as un pipeline de détection)

**À ne pas logger:**
- Chaque requête rate-limitée individuellement
- Chaque violation CSP (sample ou agrège par heure)

### Système & jobs

Utile pour le debug ops, mais attention à la cardinalité.

**À logger:**
- cron.<nom>.completed et cron.<nom>.failed (un par run, pas par item traité)
- migration.applied et migration.rolled_back
- backup.created, backup.restored

**À ne pas logger:**
- Chaque itération de boucle, chaque ligne traitée par un batch job
- Les heartbeats, les liveness probes
- Les déplacements internes de queue


## Conventions de nommage

Suis ce schéma partout : `<domaine>.<sujet>.<verbe_passé>` ou `<domaine>.<verbe_passé>` quand il n'y a pas de sujet distinct.

Règles :

1. **Tout en minuscules**, séparé par points, snake_case si nécessaire à l'intérieur d'un segment.
2. **Verbes au passé** : `.created` pas `.create`, `.updated` pas `.update`, `.deleted` pas `.delete`.
3. **Domaine d'abord**, sujet ensuite, verbe à la fin : `invoice.line_item.added` plutôt que `added.invoice.line_item`.
4. **Sois cohérent** : choisis un verbe et tiens-toi-y. Ne mélange pas `.deleted` et `.removed` pour le même domaine. Ne mélange pas `.failed` et `.errored`.
5. **Stable dans le temps** : les noms d'actions finissent dans tes dashboards, retention rules et filtres de notifications. Les renommer plus tard casse l'historique.

Verbes standards à utiliser, classés par fréquence :

| Verbe | À utiliser pour |
|-------|-----------------|
| `.created` | Nouvelle ressource créée |
| `.updated` | Ressource existante modifiée |
| `.deleted` | Ressource supprimée (soft ou hard) |
| `.archived` / `.restored` | Changements d'état soft |
| `.published` / `.unpublished` | Toggles de visibilité |
| `.shared` / `.unshared` | Octrois et révocations d'accès |
| `.signed_up` / `.logged_in` / `.logged_out` | Cycle d'auth |
| `.invited` / `.joined` / `.removed` | Membership |
| `.started` / `.completed` / `.failed` | Actions async longues |
| `.requested` / `.granted` / `.revoked` | Flows de permissions |

## Schéma metadata

`metadata` est un objet JSON libre. Garde-le petit (quelques centaines d'octets, c'est très bien), structuré, et utile dans 6 mois. Les patterns ci-dessous marchent pour la plupart des catégories.

### À toujours inclure quand pertinent

- `source` : d'où vient l'action. Valeurs : `web`, `mobile`, `api`, `admin_panel`, `automation`, `import`, `webhook`.
- `reason` : raison textuelle libre si le user ou l'admin en a fourni une (`metadata.reason: "duplicate"`).
- `request_id` : ton propre id de corrélation pour remonter à tes logs.
- `result` : `success` ou `failure`. Default à `success` pour pouvoir filtrer les échecs plus tard.

### Pour les updates

```json
{
  "metadata": {
    "changed_fields": ["status", "due_date"],
    "before": { "status": "draft" },
    "after": { "status": "paid" }
  }
}
```

Inclus uniquement les champs modifiés, pas l'objet complet. Si le diff est énorme, garde juste `changed_fields` et skip `before`/`after`.

### Pour l'argent

Toujours sérialiser les montants en **entiers de cents** (ou plus petite unité de la devise), jamais en floats :

```json
{
  "metadata": {
    "amount_cents": 4200,
    "currency": "EUR",
    "stripe_payment_intent_id": "pi_..."
  }
}
```

### Pour les échecs

```json
{
  "metadata": {
    "result": "failure",
    "reason": "card_declined",
    "code": "insufficient_funds"
  }
}
```

## Antipatterns

Ne mets jamais ça dans metadata :

- **Secrets en clair, mots de passe, tokens complets, numéros de carte complets.** Si ça ne devrait pas être dans tes logs, ça ne devrait pas être dans Recalled.
- **Corps de documents complets, fichiers, blobs, images.** Recalled n'est pas un store de documents. Mets l'id de la ressource dans `targets` et laisse le consommateur fetch le contenu s'il en a besoin.
- **PII dont tu n'as pas besoin.** Si tu logues une action sur un user, tu as déjà `actor.id`. Tu n'as pas aussi besoin de son adresse complète, son téléphone, etc. Moins de PII = moins d'exposition RGPD.
- **Données de debug serveur.** Stack traces, requêtes SQL, IDs internes qui ne veulent rien dire pour un humain auditeur. Envoie ça dans ton APM.
- **Quoi que ce soit de plus de ~2 KB.** Une grosse `metadata` ralentit les list queries et inflate ta facture de stockage.

## Volume

Recalled facture au nombre d'events par mois. Le bon ratio pour la plupart des SaaS B2B :

- **1 à 5 events par user actif par session** (login, quelques actions, logout)
- **0 event** quand un user lit juste
- **Quelques-uns par jour** pour les actions système (cron, billing, backups)

Si tu te retrouves à logger plus de 10 events par user actif par jour, tu over-logges probablement des lectures ou tu captures du bruit technique. Relis les 3 règles en haut.

## Exemple complet

Un SaaS B2B fictif de facturation. Voici la liste **complète** des events que son équipe pousserait dans Recalled. Note comme ça reste focalisé.

```
# Auth (5)
user.signed_up
user.logged_in
user.logged_out
user.login_failed
user.password_changed

# Équipe (4)
member.invited
member.joined
member.removed
member.role_changed

# Factures, l'objet métier central (6)
invoice.created
invoice.updated
invoice.sent
invoice.paid
invoice.refunded
invoice.deleted

# Clients (3)
customer.created
customer.updated
customer.deleted

# Facturation pour le SaaS lui-même (4)
subscription.created
subscription.plan_changed
subscription.canceled
payment_method.added

# Admin (2)
admin.impersonation_started
admin.impersonation_ended

# Exports (2)
export.started
export.completed
```

Soit **26 actions distinctes**, largement assez pour un audit trail propre. Ta vraie liste sera de taille similaire.

## Étape suivante

Une fois ta liste prête, file dans [Events API](/docs/events) et commence à envoyer. Le tool [list_actions_summary du dashboard](/docs/mcp) te montre en temps réel quelles actions tu logues le plus, pour repérer du sur-logging ou des trous.

---

<!-- Concepts de base / UI embeddable -->

# UI embeddable

Le sous-export `@recalled/sdk/react` livre un composant `<RecalledFeed />` pensé pour vivre **à l'intérieur du panneau admin, de la console support ou du back-office que ton équipe utilise déjà pour opérer ton produit**. Son public : tes ingés support, ton équipe ops et SRE, tes reviewers compliance et audit, les gens internes qui doivent répondre à "qui a fait quoi, quand" sans changer d'outil.

Ce **n'est pas** un composant customer-facing. Les end users de ton SaaS ne voient jamais Recalled. Et ce n'est pas du white-label non plus, il n'y a rien à revendre à tes clients. Vois ça comme un widget d'observabilité interne, dans la même catégorie qu'un panel Grafana ou un feed Sentry que tu embarquerais dans ton admin.

Par défaut, le widget a accès à **tout le projet**, chaque event émis, tous tenants confondus, ce qui est exactement ce que tu veux pour une vue admin. Tu peux optionnellement restreindre une instance de widget à un seul tenant si tu construis un drill-down par client.

> **Prérequis plan.** Créer un embed token via `client.embed.createToken()` nécessite le plan **Pro** ou **Scale**. Un compte Free reçoit une erreur `FORBIDDEN` sur cet endpoint. La lecture d'events avec un token déjà émis n'est pas gated, tant que le token est valide, le widget fonctionne.

## Installation

```bash
npm install @recalled/sdk
```

## Crée un token côté serveur

Le widget est piloté par un **embed token** court. Crée-le depuis ton backend pour que ta clé API ne touche jamais le navigateur, seul le token circule. Garde la route qui génère ce token derrière ton auth admin, pour que seuls les membres autorisés de ton équipe puissent y accéder.

```ts
// ton backend, par ex. dans une route admin protégée
import { Recalled } from "@recalled/sdk";

const client = new Recalled({ apiKey: process.env.RECALLED_API_KEY! });

// Vue admin par défaut : tous les events du projet, tous tenants confondus.
const { token } = await client.embed.createToken({ ttlSeconds: 900 });

// Optionnel : restreins cette instance précise du widget à un seul tenant (drill-down).
const { token: scoped } = await client.embed.createToken({
  organization: "org_acme",
  ttlSeconds: 900,
});
// envoie le token à ta page admin (props, réponse fetch, etc.)
```

## Render en React

Branche le composant dans la page admin que ton équipe utilise déjà au quotidien. Le token vit sur ton backend admin, jamais dans le bundle navigateur, donc l'accès est gaté par ton auth admin existante.

```tsx
import { RecalledFeed } from "@recalled/sdk/react";

export function AdminAuditLogPage({ token }: { token: string }) {
  return (
    <RecalledFeed
      embedToken={token}
      baseUrl="https://api.recalled.dev/v1"
      pageSize={50}
    />
  );
}
```

Le composant gère sa propre pagination et son refresh. Style-le avec ton propre CSS, il utilise des classes neutres sans couleurs hardcodées.

---

<!-- Concepts de base / RGPD & rétention -->

# RGPD & rétention

## Droit à l'effacement

`DELETE /v1/actors/:id`

Anonymise tous les events d'un acteur donné dans le projet appelant. Les lignes sont conservées pour préserver le lien de chaîne vers les events voisins, mais `actor_name`, `actor_email`, `metadata`, `ip_address` et `user_agent` sont mis à `null`. `actor_id` est remplacé par `[deleted]` et `anonymized_at` est estampillé avec l'heure d'effacement.

```bash
curl -X DELETE "https://api.recalled.dev/v1/actors/user_123" \
  -H "Authorization: Bearer $RECALLED_API_KEY"
```

Passe un query param `organization` optionnel pour restreindre l'effacement à un seul tenant :

```bash
curl -X DELETE "https://api.recalled.dev/v1/actors/user_123?organization=org_acme" \
  -H "Authorization: Bearer $RECALLED_API_KEY"
```

La vérification est au courant : les lignes avec `anonymized_at` sont ignorées pendant la recomputation du hash (le payload ne correspond plus au hash stocké, par design), mais le lien de chaîne depuis l'event AVANT et celui APRÈS reste vérifié. L'effacement RGPD satisfait l'Article 17 sans créer d'angle mort pour le tampering.

## Rétention

Chaque plan a une rétention par défaut :

- **Free**, 7 jours
- **Starter**, 90 jours
- **Pro**, 1 an (configurable par event avec des règles custom)
- **Scale**, illimitée (configurable par projet)

Sur les plans **Pro** et **Scale**, tu peux définir **des règles de rétention par pattern d'event** depuis les paramètres du projet. `user.delete` à 10 ans, `user.login` à 30 jours, `*` à 1 an, selon ce que ton équipe compliance demande. Sur Pro, ce qui n'est matché par aucune règle retombe sur la rétention plan d'1 an. Sur Scale, ce qui n'est matché reste pour toujours.

## Preuve de purge

À chaque fois qu'un batch d'events est supprimé, une ligne est écrite dans `retention_checkpoints` avec le dernier hash effacé, le compteur, la plage de dates et la raison (`plan_retention`, `rule:<id>`, ou `project_deleted` quand un projet entier est supprimé). L'endpoint verify lit ces checkpoints pour que les trous de chaîne créés par des purges légitimes ne ressemblent pas à du tampering.

---

<!-- Référence / Codes d'erreur -->

# Codes d'erreur

Toutes les erreurs renvoient du JSON dans la forme :

```json
{
  "error": {
    "code": "ERROR_CODE",
    "message": "Message lisible",
    "details": { "optional": "context" }
  }
}
```

| Code | HTTP | Signification |
|---|---|---|
| `UNAUTHORIZED` | 401 | Credentials manquants ou invalides |
| `INVALID_API_KEY` | 401 | Clé API non reconnue |
| `REVOKED_API_KEY` | 401 | Clé API révoquée |
| `FORBIDDEN` | 403 | Authentifié mais pas autorisé |
| `NOT_FOUND` | 404 | Ressource inexistante |
| `VALIDATION_ERROR` | 400 | Le body de la requête a échoué la validation |
| `EVENT_TOO_LARGE` | 413 | Un champ du payload (typiquement `metadata` ou `targets`) a dépassé sa limite. `details` contient le champ fautif, sa taille et la limite |
| `PLAN_LIMIT_REACHED` | 429 | Quota mensuel d'events atteint |
| `RATE_LIMITED` | 429 | Trop de requêtes dans la fenêtre courante |
| `DATABASE_ERROR` | 500 | Erreur de la base de données |
| `INTERNAL_ERROR` | 500 | Erreur serveur non gérée |

---

<!-- Référence / Rate limits -->

# Rate limits

Deux buckets s'appliquent à `/v1/*` :

## Limite d'ingest par clé

`POST /v1/events` : **1200 requêtes par minute** par clé API (par défaut). Quand tu dépasses, le serveur renvoie `429 RATE_LIMITED`.

Cette limite est généreuse parce que l'ingest est le hot path. Upgrade ton plan ou contacte le support si tu la touches régulièrement.

## Limite globale par IP

Tous les autres endpoints : **1500 requêtes par minute** par IP. Même forme de réponse `429`.

## Headers de réponse

On renvoie les headers IETF `RateLimit` standards sur chaque réponse :

```
RateLimit-Limit: 1200
RateLimit-Remaining: 973
RateLimit-Reset: 45
```

Utilise-les pour faire du back-off gracieux. Le SDK fait du backoff exponentiel automatique sur `429`.

---

<!-- Référence / Webhooks sortants -->

# Webhooks sortants

Recalled peut POSTer chaque event ingéré vers une URL que tu contrôles, signée en HMAC-SHA256. Utilise-le pour router les events vers ton back-end, un data warehouse, Zapier, n8n, un SIEM, ou n'importe quel outil qui parle HTTPS.

> **Prérequis plan + rôle.** Les webhooks génériques sont disponibles sur **Pro** (1 webhook) et **Scale** (jusqu'à 10). Seul le **owner du projet** peut les créer, les modifier ou les supprimer. Les admins et viewers invités ne peuvent ni les voir ni les gérer, même s'ils peuvent éditer le reste des paramètres.

## Comment ça marche

1. Tu configures une URL cible dans les paramètres notifications du projet.
2. À la sauvegarde, Recalled génère un secret HMAC et l'affiche **une seule fois**. Stocke-le dans ton env à côté de tes clés API.
3. Chaque fois qu'un event matchant ton filtre est ingéré, Recalled envoie un `POST` vers ton URL avec un body JSON.
4. La requête porte des headers `X-Recalled-Timestamp` et `X-Recalled-Signature` que tu vérifies de ton côté avec le secret.
5. 2xx = acquittement. 5xx / timeout = transient, retenté avec backoff. 4xx = fatal, droppé.
6. Après **7 échecs consécutifs**, le webhook est automatiquement désactivé pour ne pas marteler un endpoint cassé. Corrige ton endpoint puis réactive-le depuis le dashboard.

## Format du payload

Chaque livraison est enveloppée pour qu'on puisse ajouter de nouveaux types de webhook events plus tard sans casser les consommateurs :

```json
{
  "id": "whd_abc123xyz",
  "type": "event.created",
  "createdAt": "2026-04-15T14:23:00.123Z",
  "projectId": "proj_xyz",
  "event": {
    "id": "evt_abc",
    "action": "invoice.refunded",
    "actor": { "id": "user_1", "email": "alice@acme.co", "name": "Alice" },
    "organization": "org_acme",
    "targets": [{ "type": "invoice", "id": "inv_42" }],
    "metadata": { "amount": 4200, "currency": "eur" },
    "occurredAt": "2026-04-15T14:22:58.000Z",
    "hash": "sha256:..."
  }
}
```

## Headers de requête

| Header | Exemple | Rôle |
|---|---|---|
| `Content-Type` | `application/json` | Toujours JSON |
| `User-Agent` | `Recalled-Webhooks/1.0` | UA stable pour ton allowlist |
| `X-Recalled-Timestamp` | `1713183780` | Unix seconds de la signature |
| `X-Recalled-Signature` | `v1=9f86d081...` | HMAC-SHA256 de `${timestamp}.${rawBody}` |
| `X-Recalled-Event-Id` | `evt_abc` | Clé de dédup pour l'idempotence |
| `X-Recalled-Delivery-Id` | `whd_abc123xyz` | Unique par tentative |

Les headers custom que tu configures sur le channel (ex : `Authorization: Bearer ...` pour un endpoint privé) sont mergés avec les autres. Les headers réservés Recalled gagnent toujours.

## Vérifier la signature (Node.js)

```ts
import crypto from "node:crypto";

const SECRET = process.env.RECALLED_WEBHOOK_SECRET!;
const MAX_SKEW_SECONDS = 5 * 60;

export function verifyRecalledSignature(req: {
  rawBody: string;
  headers: Record<string, string | undefined>;
}): boolean {
  const timestamp = req.headers["x-recalled-timestamp"];
  const signature = req.headers["x-recalled-signature"];
  if (!timestamp || !signature) return false;

  // Refuse les livraisons expirées pour bloquer le rejeu.
  const skew = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (!Number.isFinite(skew) || skew > MAX_SKEW_SECONDS) return false;

  const expected =
    "v1=" +
    crypto
      .createHmac("sha256", SECRET)
      .update(`${timestamp}.${req.rawBody}`, "utf8")
      .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}
```

## Vérifier la signature (Python)

```python
import hmac, hashlib, os, time

SECRET = os.environ["RECALLED_WEBHOOK_SECRET"]
MAX_SKEW = 5 * 60

def verify(raw_body: bytes, headers: dict) -> bool:
    ts = headers.get("x-recalled-timestamp")
    sig = headers.get("x-recalled-signature")
    if not ts or not sig:
        return False
    if abs(int(time.time()) - int(ts)) > MAX_SKEW:
        return False
    msg = f"{ts}.{raw_body.decode('utf-8')}".encode("utf-8")
    expected = "v1=" + hmac.new(SECRET.encode("utf-8"), msg, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)
```

## Politique de retry

| Tentative | Délai depuis la précédente | Cumulé |
|---|---|---|
| 1 | immédiat | 0 |
| 2 | 1 min | 1 min |
| 3 | 5 min | 6 min |
| 4 | 30 min | 36 min |
| 5 | 2 h | 2 h 36 |
| 6 | 6 h | 8 h 36 |
| 7 | 12 h | 20 h 36 |

Les erreurs **transientes** (408, 429, 5xx, network, timeout) sont retentées. Les erreurs **fatales** (4xx hors 408/429, 401/403/404) sont droppées immédiatement.

Après **7 échecs consécutifs** sur des events distincts, le webhook passe en `enabled = false` et ne reçoit plus de livraisons jusqu'à réactivation manuelle.

## Idempotence

Les livraisons sont **at-least-once** : le même event peut arriver deux fois si une première tentative a timeout chez nous alors qu'elle a réussi chez toi. Déduplique sur `X-Recalled-Event-Id` ou le champ `event.id` du body. Les deux sont stables par event.

## Protection SSRF

À la création et avant chaque livraison, Recalled refuse les URLs qui résolvent vers des plages IP privées (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, loopback, carrier-grade NAT, link-local, IPv6 ULA), les ports bloqués (SSH, SQL, Redis, endpoints metadata), et les redirections ne sont pas suivies. Ta cible doit être joignable depuis internet en HTTPS.

## Test

Clique **Envoyer une notification de test** dans le dashboard. Recalled envoie un event `test.ping` avec des champs synthétiques pour que tu puisses tester ton code de vérification de signature sans attendre une vraie action utilisateur.

---

<!-- Référence / SDK npm -->

# SDK npm

```bash
npm install @recalled/sdk
```

## Client

```ts
import { Recalled } from "@recalled/sdk";

const client = new Recalled({
  apiKey: process.env.RECALLED_API_KEY!,
  // baseUrl par défaut : https://api.recalled.dev/v1
});
```

## Events

```ts
// create (strict, throw en cas d'échec)
await client.events.create({
  action: "user.password.changed",
  actor: { id: "user_1" },
});

// list
const { data, nextCursor } = await client.events.list({
  limit: 50,
  organization: "org_abc",
});

// search
const { data } = await client.events.search({ q: "invoice" });

// get one
const event = await client.events.retrieve("evt_xxx");
```

## `emit()` résilient (recommandé)

`emit()` retourne immédiatement et livre l'event en arrière-plan. Si l'API est injoignable, le SDK garde l'event dans une **queue en mémoire** et retente automatiquement avec backoff exponentiel pendant 24 heures maximum. Ton request path ne voit jamais d'exception en cas de panne passagère, ce qui est ce que tu veux pour des logs d'audit dans 99 % des cas.

```ts
client.events.emit({
  action: "invoice.paid",
  actor: { id: user.id, email: user.email },
  organization: user.organizationId,
  metadata: { amount, currency },
});
// retourne immédiatement, pas besoin d'await
```

Les résultats de livraison sont exposés via des callbacks passés au constructeur `Recalled` :

```ts
const client = new Recalled({
  apiKey: process.env.RECALLED_API_KEY!,
  resilience: {
    onDelivered: (input, event) => {},
    onError:     (err, input) => {},
    onDrop:      (input, reason, err) => {
      // reason : "ttl_expired" | "fatal_error" | "queue_full"
    },
  },
});
```

### Flush avant de sortir

Les process courts (CLI, cron, Lambda) doivent `await client.flush()` avant de sortir, sinon les events en attente sont perdus à la fermeture.

```ts
client.events.emit({ action: "job.completed" });
await client.flush(); // attend jusqu'à 30s que la queue se vide
```

### Options de résilience

| Option | Défaut | Description |
|---|---|---|
| `maxQueueSize` | `5000` | Events stockés en mémoire |
| `maxAgeMs` | `24h` | TTL avant de drop un event |
| `minBackoffMs` | `1000` | Premier délai de retry |
| `maxBackoffMs` | `10min` | Cap du backoff |

Passe `resilience: false` pour désactiver le buffering (`emit()` devient un fire-and-forget brut).

### `create` vs `emit`

| | `create()` | `emit()` |
|---|---|---|
| Retour | `Promise<Event>` | `void` |
| Throw en cas d'échec | oui | non |
| Buffer en cas de panne | non | oui |
| Bloque le request path | oui | non |
| À utiliser quand | l'échec doit remonter | l'échec doit être invisible |

## Embed

```ts
const { token } = await client.embed.createToken({
  organization: "org_abc",
  ttlSeconds: 900,
});
```

## Actors (RGPD)

```ts
await client.actors.delete({ id: "user_123" });
```

## Exports

```ts
// Retourne le body brut de l'export (string CSV ou JSON selon le format).
const body = await client.exports.fetch({
  format: "csv",
  organization: "org_abc",
});
```

## Types

Chaque méthode est entièrement typée et le package livre ses propres `.d.ts`. Importe les types directement quand tu en as besoin :

```ts
import type { Event, CreateEventInput } from "@recalled/sdk";
```

---

<!-- Référence / Serveur MCP -->

# Serveur MCP

Recalled expose un serveur **Model Context Protocol** officiel pour que les agents IA (Claude Desktop, Cursor, ChatGPT, agents custom basés sur les SDKs MCP officiels) puissent lire et agir sur ton audit trail directement.

> **Plan requis.** L'endpoint MCP est disponible sur **Starter, Pro et Scale**. Les comptes Free peuvent toujours créer des clés API pour le SDK et l'API REST, mais `/v1/mcp` renvoie une erreur JSON-RPC qui pointe vers la page d'upgrade. Upgrade depuis [recalled.dev/dashboard/billing](/dashboard/billing).

L'endpoint MCP réutilise les clés API que tu as déjà. Même authentification, même scoping par projet, aucune infra à mettre en place. Si tu sais appeler `/v1/events`, tu sais utiliser le MCP, à condition que le propriétaire du projet soit sur un plan payant.

## Endpoint

```
POST https://api.recalled.dev/v1/mcp
```

L'endpoint parle le transport **Streamable HTTP** (spec MCP 2025-03-26). Il tourne en mode stateless : chaque requête JSON-RPC obtient un serveur frais, isolé à la clé API qui l'a authentifiée. Aucun token de session à gérer côté client.

## Authentification

Même Bearer token que l'API REST :

```
Authorization: Bearer rec_live_<prefix>_<secret>
Content-Type: application/json
```

Le MCP scope tout au projet propriétaire de la clé API. Une session MCP liée au projet A ne peut jamais lire les events du projet B.

## Connecter Claude Desktop

Édite `claude_desktop_config.json` et ajoute :

```json
{
  "mcpServers": {
    "recalled": {
      "url": "https://api.recalled.dev/v1/mcp",
      "headers": {
        "Authorization": "Bearer rec_live_xxx_yyy"
      }
    }
  }
}
```

Redémarre Claude Desktop. Recalled devrait apparaître dans la liste des outils.

## Connecter Cursor

Ouvre Cursor Settings, MCP, puis ajoute un nouveau serveur :

```json
{
  "name": "recalled",
  "transport": "http",
  "url": "https://api.recalled.dev/v1/mcp",
  "headers": {
    "Authorization": "Bearer rec_live_xxx_yyy"
  }
}
```

## Tools disponibles

| Tool | Ce qu'il fait |
|------|---------------|
| `get_project_info` | Identification du projet auquel cette session est liée, et scopes de la clé API courante. |
| `get_recent_events` | Events les plus récents, plus récents en premier, jusqu'à 100. |
| `search_events` | Recherche full text sur action, nom d'acteur, email d'acteur, id d'acteur. Pagination cursor. |
| `list_events` | Filtres structurés (action, acteur, organisation, IP, plage de dates). Pagination cursor. |
| `retrieve_event` | Récupère un event par id avec tous les détails. |
| `get_event_receipt` | Retourne un reçu portable et citable pour un event, avec verification_url et view_url publics. |
| `list_actions_summary` | Top actions sur une fenêtre de N jours, avec count et part en pourcentage. |
| `verify_chain` | Recalcule chaque hash et signature HMAC, retourne un rapport d'intégrité. |
| `usage_summary` | Compteur d'events du mois en cours vs limite du plan, pourcentage utilisé. |
| `delete_actor` | Effacement RGPD article 17. Nécessite `confirm: true` pour s'exécuter. |
| `audit_actor_plan` | Retourne un plan pas à pas pour auditer un acteur. L'assistant l'exécute ensuite en appelant les autres tools de données. |
| `investigate_incident_plan` | Retourne un plan d'investigation pour les events autour d'un timestamp. |
| `compliance_check` | Retourne un plan d'audit RGPD / SOC 2 / ISO 27001 que l'assistant exécute en chaînant les tools de données. |
| `get_setup_guide` | Retourne le prompt opinion pour ajouter Recalled à une codebase. EN ou FR. |

## Resources disponibles

Les mêmes données exposées par `get_project_info`, `usage_summary` et `get_recent_events` sont aussi publiées comme resources MCP, pour les clients qui préfèrent le modèle resources aux tool calls. Tools et resources restent synchronisés, choisis ce que ton client supporte le mieux.

| URI | Ce qu'elle expose |
|-----|-------------------|
| `recalled://project/info` | Métadonnées du projet et id, nom, prefix, scopes de la clé API. |
| `recalled://usage/current` | Usage du mois courant, limite du plan, pourcentage utilisé. |
| `recalled://events/recent` | Les 50 events les plus récents pour un contexte rapide. |

## Prompts disponibles

Les prompts sont des recettes réutilisables qui combinent tools et resources pour répondre à des questions courantes. Ils apparaissent comme des actions rapides dans les clients qui supportent les prompts MCP (Cursor, ChatGPT, agents custom). Claude Desktop ignore actuellement les prompts et ne voit que les tools, donc chaque prompt est aussi exposé sous forme de tool `*_plan` avec le même contenu.

| Prompt | Tool équivalent | Ce qu'il produit |
|--------|------------------|------------------|
| `/audit_actor` | `audit_actor_plan` | Audit l'activité d'un acteur sur une fenêtre choisie. Entrées : `actor_id`, `days` optionnel. |
| `/investigate_incident` | `investigate_incident_plan` | Enquête sur les events autour d'un timestamp, propose un récit de cause racine. Entrées : `at` (ISO), `window_minutes` optionnel, `focus` optionnel. |
| `/compliance_check` | `compliance_check` | Évaluation de conformité RGPD, SOC 2, ISO 27001 basée sur les données présentes dans le projet. |

## Exemple de session

Une fois connecté, tu peux poser tes questions à l'assistant en langage naturel et il choisira les bons outils.

```
Toi : qui a supprimé des factures ces 7 derniers jours ?
Assistant : (appelle list_events avec action="invoice.deleted", date_from il y a 7 jours)
Assistant : 4 factures ont été supprimées par 2 acteurs distincts ces 7 derniers jours. La plus récente date d'il y a 3 heures, par user_42.
```

```
Toi : lance un audit de conformité sur ce projet.
Assistant : (appelle /compliance_check, puis verify_chain, list_actions_summary, lit project_info)
Assistant : RGPD vert, SOC 2 amber (changements de rôle admin pas tous loggués), ISO 27001 amber. ...
```

## Tarification

Le MCP est **disponible sur tous les plans**, y compris Free. Il ne consomme pas ton quota d'events : il lit les events existants et expose les mêmes actions que l'API REST. Les actions effectuées par un tool (par exemple ingestion via REST, effacement RGPD) restent facturées et rate limitées comme tout autre appel.

## Modèle de sécurité

- Bearer token uniquement, dans le header `Authorization`. Jamais en query string.
- HTTPS uniquement.
- Pas de "token passthrough" : le serveur MCP valide la clé contre ton projet avant toute action.
- Chaque requête ouvre son propre serveur scopé et le ferme en réponse. Aucun état partagé entre tenants.
- Les tools destructifs (`delete_actor`) exigent un argument `confirm` explicite pour qu'un agent ne les exécute pas par accident.

## Dépannage

**"Invalid API key"** : la clé n'a pas été trouvée, a été révoquée, ou le prefix ne correspond pas. Génère une nouvelle clé dans le dashboard.

**"Method Not Allowed"** sur une requête GET : l'endpoint est POST uniquement en mode stateless. Les clients MCP doivent envoyer du JSON-RPC en POST.

**Rate limited** : le MCP partage le rate limit global de `/v1/*`. Si ton agent boucle agressivement, regroupe ses appels ou ajoute du backoff.

---
