Audit
3 operations under /v1/audit (GET POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
Base URL: https://api.codespar.dev
Every operation below requires a Bearer token. See Authentication.
GET /v1/audit/anchors
https://api.codespar.dev/v1/audit/anchorsList audit-chain anchors (deprecated path)
DEPRECATED alias of GET /v1/audit-events/anchors (ent#979), kept for two releases. Same handler, same scope.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit | integer | no | — |
status | "pending" | "sent" | "local_only" | "failed" | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | The query did not match the schema. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
anchors | array of object | yes | — |
curl -X GET https://api.codespar.dev/v1/audit/anchors \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/audit/anchors HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/audit/anchors",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/audit/anchors", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/audit/anchors");{
"anchors": [
{
"id": "obj_0000000000000000",
"head_sequence": 0,
"head_hash": "string",
"signature": "string",
"signature_alg": "string",
"target": "string",
"target_status": "string",
"target_response": "string",
"delivery_attempts": 0,
"anchored_at": "2026-01-15T12:00:00.000Z",
"delivered_at": "2026-01-15T12:00:00.000Z"
}
]
}POST /v1/audit/anchors/verify
https://api.codespar.dev/v1/audit/anchors/verifyVerify an anchor document against our record (deprecated path)
DEPRECATED alias of POST /v1/audit-events/anchors/verify (ent#979), kept for two releases. Same handler, same scope, same four verdicts.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
anchor_id | string | yes | — |
anchored_at | string (date-time) | yes | — |
head_hash | string | yes | — |
head_sequence | integer | yes | — |
org_id | string | yes | — |
signature | string | yes | — |
signature_alg | "HMAC-SHA256" | yes | — |
version | 1 | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | The document did not match the schema. |
403 | object | The document's org_id is not the credential's. |
503 | object | The anchor secret is not configured. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
anchor_id | string | yes | — |
head_sequence | integer | yes | — |
org_id | string | yes | — |
persisted_locally | boolean | yes | A row of ours exists with that anchor_id. |
persisted_matches | boolean | yes | And it matches the presented document field by field. |
signature_valid | boolean | yes | The signature checks out against this organization's secret. |
verdict | "verified" | "signature_ok_but_no_local_record" | "local_record_diverges" | "signature_invalid" | yes | The first three are NOT the same thing, and the distinction is the product: verified is a good signature with our record identical; signature_ok_but_no_local_record is a document we signed and of which we no longer hold a copy; local_record_diverges is a good signature with our row DIFFERENT, the case an auditor cares about. |
curl -X POST https://api.codespar.dev/v1/audit/anchors/verify \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"version": 1,
"anchor_id": "anchor_0000000000000000",
"org_id": "org_0000000000000000",
"head_sequence": 0,
"head_hash": "string",
"anchored_at": "2026-01-15T12:00:00.000Z",
"signature_alg": "HMAC-SHA256",
"signature": "string"
}'POST /v1/audit/anchors/verify HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"version": 1,
"anchor_id": "anchor_0000000000000000",
"org_id": "org_0000000000000000",
"head_sequence": 0,
"head_hash": "string",
"anchored_at": "2026-01-15T12:00:00.000Z",
"signature_alg": "HMAC-SHA256",
"signature": "string"
}import os
import requests
res = requests.post(
"https://api.codespar.dev/v1/audit/anchors/verify",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"version": 1,
"anchor_id": "anchor_0000000000000000",
"org_id": "org_0000000000000000",
"head_sequence": 0,
"head_hash": "string",
"anchored_at": "2026-01-15T12:00:00.000Z",
"signature_alg": "HMAC-SHA256",
"signature": "string"
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/audit/anchors/verify", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"version": 1,
"anchor_id": "anchor_0000000000000000",
"org_id": "org_0000000000000000",
"head_sequence": 0,
"head_hash": "string",
"anchored_at": "2026-01-15T12:00:00.000Z",
"signature_alg": "HMAC-SHA256",
"signature": "string"
}),
});
const data = await res.json();const r = await cs.api.response("post", "/v1/audit/anchors/verify", {
body: {
version: 1,
anchor_id: "anchor_0000000000000000",
org_id: "org_0000000000000000",
head_sequence: 0,
head_hash: "string",
anchored_at: "2026-01-15T12:00:00.000Z",
signature_alg: "HMAC-SHA256",
signature: "string"
}
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
console.log(r.data);
}{
"anchor_id": "anchor_0000000000000000",
"org_id": "org_0000000000000000",
"head_sequence": 0,
"signature_valid": true,
"persisted_locally": true,
"persisted_matches": true,
"verdict": "verified"
}GET /v1/audit/events
https://api.codespar.dev/v1/audit/eventsBrowse the audit chain (alias path)
A read-only page of the caller's own tamper-evident chain, newest first.
THIS IS AN ALIAS OF GET /v1/audit-events, NOT A SECOND RESOURCE. One handler serves both paths, so the query, the body and every refusal are identical and there is nothing to choose between them. It is described separately only because a generated client sees two operations; pick one spelling and stay on it. Note that the OTHER audit operations — the chain's health report, its incidents, its configuration — have no twin under this prefix and live only under /v1/audit-events.
BROWSE, NOT EXPORT. This emits no manifest, no signature and no chain entry of its own, so paging through it leaves no trace and proves nothing offline. It is for showing an operator what happened, not for handing an auditor evidence.
Scoped to the credential's organization with no way to widen it, and there is no cross-org refusal to handle because there is no organization segment in the path to disagree with.
A KEY READS ITS OWN PROJECT ONLY. An API key or an OAuth token gets the entries whose payload names the credential's project, plus the organization-level entries, which name no project; a policy_evaluation recorded without a project is the exception and is not returned to a key. Naming another project in project_id is refused with 403 audit_project_out_of_scope. Service auth reads the whole organization and the x-codespar-project header does not narrow it. A key's page is therefore NOT contiguous in the chain: sequence_number skips the entries of other projects.
event_type IS AN EXACT MATCH UNLESS IT ENDS IN A DOT, in which case it is a prefix match — so tool_call. returns the succeeded and the failed variants in one pass without two queries. The default window is the last seven days.
PAGINATION WALKS BACKWARDS. Pass the previous response's next_before_sequence as before_sequence. The cursor is null only when the page came back SHORTER than limit, so a full page that happens to end at the chain's floor still hands you a non-null cursor and the last call is the one that returns fewer rows. An out-of-range limit is clamped rather than refused.
Scope: audit:read.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
actor_kind | "person" | no | person: only entries that name a member of the organization as their actor (see actor_person_id). Any other value is refused with invalid_actor_kind. |
before_sequence | integer | no | Returns entries strictly below this sequence. Use the previous page's next_before_sequence. |
event_type | string | no | Exact match, or a prefix match when it ends with a dot. |
from | string (date-time) | no | ISO 8601. Defaults to seven days ago. |
limit | integer | no | Clamped to 1..200. Defaults to 50, and falls back to 50 when the value is 0 or is not a number; a negative value becomes 1. |
project_id | string | no | Only entries whose payload names this project (project_id, else projectId). The chain has no project column, so an organization-level entry, which names no project, is not returned under this filter. A malformed id is refused with invalid_project_id. An API key or an OAuth token may name its own project only: any other is refused with 403 audit_project_out_of_scope. |
to | string (date-time) | no | ISO 8601. Defaults to now. |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | Bad Request, in a BARE body. invalid_before_sequence when the cursor is not a number, invalid_iso_8601 when from or to is not a date this server can parse, from_after_to when the window is inverted, invalid_actor_kind and invalid_project_id when those filters are malformed. |
403 | object | Forbidden, in a BARE body. audit_project_out_of_scope when an API key or an OAuth token names in project_id a project that is not its own. Service auth is never refused here. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
events | array of object | yes | — |
next_before_sequence | integer,null | yes | The lowest sequence on this page when the page was full; null when it was not. |
curl -X GET https://api.codespar.dev/v1/audit/events \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/audit/events HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/audit/events",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/audit/events", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const r = await cs.api.response("get", "/v1/audit/events");
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
console.log(r.data);
}{
"events": [
{
"sequence_number": 0,
"event_type": "string",
"happened_at": "2026-01-15T12:00:00.000Z",
"payload": {},
"prev_hash": "string",
"entry_hash": "string",
"actor_person_id": "actorperson_0000000000000000"
}
],
"next_before_sequence": 0
}Events
2 operations under /v1/events (GET POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
Audit Events
10 operations under /v1/audit-events (GET POST PATCH): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.