Verify the audit log's integrity

View as Markdown
Recomputes the workspace's tamper-evident hash chain and reports the first row that does not match. Each audit row stores `row_hash`, a hash of its content and of the previous row's hash. Changing, inserting or deleting a row in the middle breaks the chain from that point on. Rows are walked oldest first (`created_at`, then `id`) inside the optional `[from, to]` window, both ends inclusive. Without a window, the whole retained history is checked, which can take a few seconds on large workspaces. Rows older than the hash chain (no `row_hash`) are skipped. Redacted rows are accepted when their redaction attestation (`redaction_hmac`) is valid. That covers GDPR erasure of `changes` and the 90-day IP/user-agent sweep. They are counted in `redactedRows`, so a green result still tells you a redaction happened. `anchoredMidChain: true` is normal with a `from` bound, or after retention deleted older rows: the first row links to one outside the window, and that link was not checked. The result is returned at the top level, not wrapped in `data`. **Idempotency.** A read with no side effects. Safe to retry. The result only changes when new rows are written or a redaction happens. **Access** - **Required scope:** `full`. Human users need the **owner** role; admins are refused. - **Rate limit:** Audit — 20 requests/min per workspace, shared with the other audit routes, plus the general API limit (120–600/min by plan). See [Rate limits](/rate-limits). - **Plan:** Available on every plan.

Authentication

AuthorizationBearer
Workspace API key: `jl_` followed by 64 lowercase hex characters, created by the workspace owner in the dashboard (**Settings → API Keys**) and sent as `Authorization: Bearer jl_...`. The plaintext is shown once, at creation; Jelliu stores only a SHA-256 hash. A workspace can hold up to 25 active keys. | Scope | GET / HEAD | POST / PUT / PATCH / DELETE | Admin-only routes | | --- | --- | --- | --- | | `read` | Yes | No | No | | `write` | Yes | Yes | No | | `full` | Yes | Yes | Yes | Operations restricted to admins or owners reject keys without the `full` scope with `403`, and say so in their description. No key, whatever its scope, can mint or revoke API keys or rotate a webhook secret — that requires a signed-in owner session. A revoked key stops authenticating within about 10 seconds. See [Authentication](/authentication).

Query parameters

fromdatetimeOptional

Inclusive start of the window. ISO 8601 datetime in UTC with a Z suffix; an offset such as -05:00 or a bare date answers 400.

todatetimeOptional

Inclusive end of the window. ISO 8601 datetime in UTC with a Z suffix.

Response

Verification result (not wrapped in data).

validboolean

true when every signed row in the window matches its hash and links to its predecessor.

totalCheckedinteger
Rows examined in the window.
redactedRowsinteger

Redacted rows in the window with a valid attestation (exact count).

redactedRowIdslist of strings

Ids of the first redacted rows (truncated list; the count above is exact).

anchoredMidChainboolean

The first signed row in the window links to a row outside it (normal with a bounded from or after retention); that link was not checked.

firstInvalidRowstringOptionalformat: "uuid"

Id of the first broken row. Present only when valid is false. Inspect it with GET /api/audit/{id}.

reasonenumOptional

Why the first broken row failed. Present only when valid is false. hash_mismatch: the row’s content was changed after it was written. link_broken: the row does not point at the previous row’s hash (a row was inserted or deleted). unattested_redaction: the row is marked redacted but the redaction signature is missing or invalid.

Errors

400
Bad Request Error
401
Unauthorized Error
403
Forbidden Error
429
Too Many Requests Error