REST API
Every /api/v1 endpoint, with authentication, required scopes, parameters, and responses.
Everything the SDK does is a plain HTTP call against /api/v1, so any language can use
Audkit. The default host is https://audkit.dev.
Authentication
Every endpoint takes a project API key as a bearer token:
Authorization: Bearer <project-api-key>Keys are minted per project under Settings → API Keys and are scope-enforced per endpoint:
| Scope | Endpoints |
|---|---|
log:write | POST /api/v1/log |
log:read | GET /api/v1/events, GET /api/v1/legal-holds/:holdId/proof |
log:export | GET /api/v1/export |
log:verify | GET /api/v1/verify, /root, /proof/inclusion, /proof/consistency, /anchors |
The key determines the project — there is no project id in any path. Common failures:
| Status | Body | Cause |
|---|---|---|
401 | { "error": "Missing API key" } | No bearer token. |
401 | { "error": "Invalid API key" } | Unknown, revoked, or expired key. |
403 | { "error": "API key missing <scope> scope" } | Key lacks the endpoint's scope. |
Endpoints that address a stream accept stream=events (default) or
stream=project-audit; anything else is a 400.
POST /api/v1/log
Ingest one event. Scope: log:write.
Body
| Field | Type | Notes |
|---|---|---|
action | string | Required, 1–256 chars. |
actor | { type, id, display? } | Required. type ≤ 128, id ≤ 512. |
status | enum | success (default), failed, pending, approved, denied. |
target | { type, id, display? } | |
risk | enum | low, medium, high, critical. |
context, metadata | object | Arbitrary JSON records. |
requestId, sessionId, ipAddress, userAgent | string | Request attribution. |
agentId, model, toolName, toolCallId, approvalId, policyVersion | string | Agent attribution. |
inputHash, outputHash | string | Commit to large payloads without storing them. |
id | string | Signed ingests only — the id the signature commits to. |
nonce | string | Signed ingests only — 32-byte lowercase hex blinding nonce. |
signingKeyId, signature | string | Customer Ed25519 attestation; must be sent together. |
Response
202 Accepted{ "id": "evt_…", "status": "sealed" }202 here is a promise about durable state, not about a queue. Before it is sent, the
event has been sequenced under the project row lock, encrypted, committed as an RFC 6962
leaf, and written to Postgres. Only order-independent mirror work runs afterwards.
Errors
| Status | Cause |
|---|---|
400 | Invalid JSON, schema failure (details carries the field errors), signingKeyId/signature sent apart, signed event missing id or nonce, unknown or revoked signing key, signature that does not verify, or an unsigned event on a project that requires signing. |
429 | Per-key rate limit (500 requests / 10 seconds) — carries Retry-After — or a billing suspension, which also returns a reason. |
500 | The seal failed; nothing was recorded. |
GET /api/v1/events
Query stored events. Scope: log:read.
Filter mode
| Parameter | Notes |
|---|---|
action, status, actorId, targetId | Exact-match filters. |
from, to | Time bounds. |
limit | Default 50, capped at 500. |
cursor | Opaque offset token; pass back the nextCursor from the previous page. |
{ "events": [ /* … */ ], "nextCursor": "50" }nextCursor is present only when the page came back full.
Entity mode
Pass entityType and entityId to get an entity-centred timeline instead:
| Parameter | Notes |
|---|---|
entityType, entityId | Both required to enter this mode. |
direction | outgoing (what it did), incoming (what happened to it), or all (default). |
limit | Same default and cap. |
Entity mode returns { "events": [ … ] } with no cursor.
GET /api/v1/export
Stream matching events as a downloadable file. Scope: log:export.
| Parameter | Notes |
|---|---|
format | csv (default) or json (newline-delimited JSON). |
action, status, actorId, targetId, from, to | Same filters as /events. |
max | Optional hard cap on exported rows. |
The body is streamed page-by-page from Postgres, so large exports never buffer in memory,
and Content-Disposition: attachment marks it as a download. Feed the json form to
npx audkit verify full for an offline rebuild.
GET /api/v1/verify
Rebuild the stream's tree from row content and return a service-signed receipt. Scope:
log:verify.
| Parameter | Notes |
|---|---|
stream | events (default) or project-audit. |
{
"receiptVersion": 2,
"stream": "events",
"projectId": "prj_…",
"valid": true,
"checkedCount": 18442,
"treeSize": 18442,
"rootHash": "a8d772…",
"firstBreak": null,
"verifiedAt": "2026-08-23T09:14:02.118Z",
"servicePublicKey": "…",
"signature": "…"
}firstBreak is null when valid, otherwise { sequence, reason, detail } — see the
reason list.
GET /api/v1/root
The current signed tree head. Scope: log:verify.
| Parameter | Notes |
|---|---|
stream | events (default) or project-audit. |
{
"sth": {
"v": 1,
"projectId": "prj_…",
"stream": "events",
"treeSize": 18442,
"rootHash": "a8d772…",
"timestamp": "2026-08-23T09:14:02.118Z",
"signature": "…"
},
"servicePublicKey": "…"
}The head is Ed25519-signed by the service key. This is the reference point the SDK's auto-verification hangs off.
GET /api/v1/proof/inclusion
RFC 6962 inclusion proof: the audit path linking one leaf to the root at treeSize.
Scope: log:verify.
| Parameter | Notes |
|---|---|
eventId | Address the leaf by event id. |
leafIndex | Or by 0-based index. Ignored when eventId is given. |
treeSize | Defaults to the live tree size; must be in [1, liveSize]. |
stream | events (default) or project-audit. |
{ "stream": "events", "leafIndex": 17, "treeSize": 18442, "leafHash": "…", "proof": ["…"] }404 when the event id is unknown; 400 when treeSize or leafIndex is out of range.
Verify the path locally — a forged one cannot reproduce a root you pinned.
GET /api/v1/proof/consistency
RFC 6962 consistency proof: that the tree at oldSize is a prefix of the tree at
newSize. Scope: log:verify.
| Parameter | Notes |
|---|---|
oldSize | Required, non-negative integer. |
newSize | Defaults to the live tree size; must be in [oldSize, liveSize]. |
stream | events (default) or project-audit. |
{ "stream": "events", "oldSize": 12000, "newSize": 18442, "proof": ["…"] }The trivial cases — oldSize of 0, or oldSize === newSize — return an empty proof,
which verifies.
GET /api/v1/anchors
The project's attested tree heads, newest first. Scope: log:verify.
| Parameter | Notes |
|---|---|
stream | events (default) or project-audit. |
limit | Default 20, capped at 100. |
{
"stream": "events",
"heads": [
{
"treeSize": 18442,
"rootHash": "a8d772…",
"timestamp": "2026-08-23T09:00:00.000Z",
"signature": "…",
"anchorType": "rekor",
"anchorRef": "…",
"anchorProof": { "…": "…" },
"anchoredAt": "2026-08-23T09:00:11.402Z"
}
]
}Anchor fields are null until the head is published. Resolve anchorRef at the
transparency log yourself — https://search.sigstore.dev/?uuid=<anchorRef> or
rekor-cli get --uuid <anchorRef> — and compare the anchored root against your own
rebuild.
GET /api/v1/legal-holds/:holdId/proof
Prove that one event was captured under a legal hold. Scope: log:read.
| Parameter | Notes |
|---|---|
eventId | Required query parameter. |
Returns a Merkle inclusion proof against the hold's committed membership root, which is
itself anchored in the project's project_audit stream. Verify it offline against that
root.
| Status | Cause |
|---|---|
400 | eventId missing. |
404 | Unknown hold, or the event is not part of it. |
Related
- SDK reference — typed wrappers over every endpoint here.
- Verification — what to do with the proofs once you have them.