Skip to main content

Audit

3 operations under /v1/audit (GET POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.

5 min read
View MarkdownEdit on GitHub

Base URL: https://api.codespar.dev

Every operation below requires a Bearer token. See Authentication.

GET /v1/audit/anchors

GEThttps://api.codespar.dev/v1/audit/anchors
Deprecated

List 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

NameTypeRequiredDescription
limitintegerno—
status"pending" | "sent" | "local_only" | "failed"no—

Responses

StatusBodyDescription
200objectOK
400objectThe query did not match the schema.

Response 200

FieldTypeRequiredDescription
anchorsarray of objectyes—
Example request
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_KEY
import 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");
Example response 200
application/json
{
  "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

POSThttps://api.codespar.dev/v1/audit/anchors/verify
Deprecated

Verify 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

FieldTypeRequiredDescription
anchor_idstringyes—
anchored_atstring (date-time)yes—
head_hashstringyes—
head_sequenceintegeryes—
org_idstringyes—
signaturestringyes—
signature_alg"HMAC-SHA256"yes—
version1yes—

Responses

StatusBodyDescription
200objectOK
400objectThe document did not match the schema.
403objectThe document's org_id is not the credential's.
503objectThe anchor secret is not configured.

Response 200

FieldTypeRequiredDescription
anchor_idstringyes—
head_sequenceintegeryes—
org_idstringyes—
persisted_locallybooleanyesA row of ours exists with that anchor_id.
persisted_matchesbooleanyesAnd it matches the presented document field by field.
signature_validbooleanyesThe signature checks out against this organization's secret.
verdict"verified" | "signature_ok_but_no_local_record" | "local_record_diverges" | "signature_invalid"yesThe 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.
Example request
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);
}
Example response 200
application/json
{
  "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

GEThttps://api.codespar.dev/v1/audit/events
Deprecated

Browse 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

NameTypeRequiredDescription
actor_kind"person"noperson: 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_sequenceintegernoReturns entries strictly below this sequence. Use the previous page's next_before_sequence.
event_typestringnoExact match, or a prefix match when it ends with a dot.
fromstring (date-time)noISO 8601. Defaults to seven days ago.
limitintegernoClamped 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_idstringnoOnly 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.
tostring (date-time)noISO 8601. Defaults to now.

Responses

StatusBodyDescription
200objectOK
400objectBad 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.
403objectForbidden, 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

FieldTypeRequiredDescription
eventsarray of objectyes—
next_before_sequenceinteger,nullyesThe lowest sequence on this page when the page was full; null when it was not.
Example request
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_KEY
import 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);
}
Example response 200
application/json
{
  "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
}
Audit | CodeSpar