Audit
The audit trail, the hash chain verification and the archive export for auditors.
| Operation | Summary | Scope | Idempotency-Key |
|---|---|---|---|
GET /v1/access-log |
List what external auditors opened | admin |
|
GET /v1/audit-events |
Audit Events | read |
|
GET /v1/entities/{entity}/chain-verification |
Chain Verification | read |
GET /v1/access-log
List what external auditors opened
Every request of the organization's external auditors, newest first: when, who, what they
opened (method, path and query: a screen's data, a report, a downloaded file) and the answer's
status (403 for what was refused them). An external auditor is a person invited with an end
to their access (access_until) or with the rights to read and to audit only (the auditor
role). The log is append-only; nobody can change or delete it. Only a person with the admin
scope reads it (403 otherwise).
| Required scope | admin (admin implies every scope) |
| Operation id | access_log_v1_access_log_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | user_id |
string (uuid) or null | Only the requests of this auditor (a user id from GET /v1/users). |
|
| query | limit |
integer | Most entries returned, 1 to 1000. (min 1; max 1000; default 100) |
|
| query | before |
integer or null | Continue before this entry (its id, the last of the previous page); omit for the newest. |
Example request
curl "$ERP_URL/v1/access-log" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of AccessLogEntry |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"id": 1,
"at": "2026-09-21T09:30:00Z",
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "string",
"via": "string",
"method": "string",
"path": "string",
"query": "string",
"status": 1,
"address": "string",
"request_id": "string"
}
]
GET /v1/audit-events
Audit Events
The audit trail, newest first: who (actor) did what (action, e.g. journal.post)
to which object, with the request id and idempotency key of the call and event-specific
data. Events are append-only and cannot be changed. Narrow it with object_type and
object_id.
| Required scope | read (admin implies every scope) |
| Operation id | audit_events_v1_audit_events_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | object_type |
string or null | Only events about this kind of object, e.g. journal_entry, entity, account, period, api_key, app_user, organization. |
|
| query | object_id |
string or null | Only events about the object with this id (as text, usually a uuid). | |
| query | limit |
integer | How many events to return, newest first; at most 1000. (max 1000; default 100) |
Example request
curl "$ERP_URL/v1/audit-events" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of AuditEventOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"id": 1,
"at": "2026-09-21T09:30:00Z",
"actor": {},
"action": "string",
"object_type": "string",
"object_id": "string",
"request_id": "string",
"idempotency_key": "string",
"data": {}
}
]
GET /v1/entities/{entity}/chain-verification
Chain Verification
Recompute the hash chain of all posted entries of the entity to detect tampering.
Every posted entry holds a hash of its content and of its predecessor. The answer is
valid with the number of entries, or not valid with the number of the first entry
that breaks the chain (first_invalid) and why (reason: a gap in the sequence, a wrong
previous hash, or changed content).
| Required scope | read (admin implies every scope) |
| Operation id | chain_verification_v1_entities__entity__chain_verification_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | entity |
string | yes | Entity code, e.g. CH01 (GET /v1/entities). |
Example request
curl "$ERP_URL/v1/entities/CH01/chain-verification" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | ChainCheck |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"entity": "string",
"entries": 1,
"valid": true,
"first_invalid": "string",
"reason": "string",
"hash_versions": {}
}
Schemas
AccessLogEntry
One request an external auditor made.
| Field | Type | Required | Description |
|---|---|---|---|
id |
integer | yes | Sequence number of the entry; increases with time. |
at |
string (date-time) | yes | When the request was made (ISO 8601, UTC). |
user_id |
string (uuid) or null | yes | The auditor's user id. |
email |
string | yes | The auditor's sign-in address. |
via |
string or null | yes | The app the auditor connected over MCP that made the request (e.g. Claude (connected app)); null for the auditor's own session. |
method |
string | yes | GET for a read or a download, MCP for a tool their app called; any other method asked to change something. |
path |
string | yes | What was opened, e.g. /v1/entities/CH01/trial-balance, or the tool's name, e.g. trial_balance. |
query |
string or null | yes | The query string of the request, or the tool's arguments as JSON; null for none. |
status |
integer or null | yes | The answer's HTTP status: 2xx served, 403 refused (for example a change an auditor may not make). |
address |
string or null | yes | The client address the request came from. |
request_id |
string or null | yes | The request's id, as in the audit trail. |
ChainCheck
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Code of the entity that was checked. |
entries |
integer | yes | Number of posted entries in the chain. |
valid |
boolean | yes | True when every entry's sequence number, previous hash and content hash check out. |
first_invalid |
string or null | Number of the first entry that breaks the chain; null when valid. | |
reason |
string or null | gap in chain sequence, previous hash mismatch or content hash mismatch; null when valid. |
|
hash_versions |
map of integer | Entries verified per hash version (1: content; 2: every column). |