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.
Créer un event
POST /v1/events
{
"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` 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.
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 :
{
"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 :
{
"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/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é depuisnextCursorde la page précédenteorganization, filtre tenantactor_id, filtre sur un acteur précisaction, filtre exact sur une seule actionactions, 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écisedate_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 à listorganization,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_hashdoit égaler lehashde 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é :
{
"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 :
{
"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 à sonhashstocké.signature_mismatch: le payload ne correspond plus à sasignatureHMAC.chain_broken: leprev_hashpointe dans le vide et aucunretention_checkpointn'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 :
{
"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 pour le pattern complet.