recalled.dev
Concepts de base

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

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` 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-champTypeContrainteRôle
actor.idstring1-255 caractèresID utilisateur stable de ta DB. Permet le filtrage par user et la suppression RGPD via DELETE /v1/actors/:id
actor.typestring1-64 caractèresuser, service, api_key, system, etc. Distingue humain et automatisé
actor.namestringmax 255 caractèresNom d'affichage, visible dans le dashboard et le feed embed
actor.emailstringmax 255 caractères, email valideOptionnel, 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-champTypeContrainteRôle
typestring1-64 caractères, requisType de ressource (invoice, project, api_key)
idstring1-255 caractères, requisID de la ressource dans ta DB
namestringmax 255 caractères, optionnelNom 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.

ChampLimite
action255 caractères
metadata8 KB de JSON sérialisé
targets4 KB de JSON sérialisé, 20 entrées max
actor.id, actor.name, actor.email255 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 :

ChampSignification
idUUID attribué à l'ingest
project_idDéduit de la clé API
ip_addressIP de la requête d'ingest
user_agentHeader User-Agent de la requête d'ingest
hashSHA-256 de prev_hash concaténé au payload canonique. Preuve de chaînage
prev_hashhash de l'event précédent du projet, null pour le tout premier
signatureHMAC-SHA256 du payload canonique, préfixé par la version de clé (ex. v1:...). Secret jamais stocké en base
anonymized_atTimestamp 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 pour le pattern complet.