Graine AI
API referenceAudit logs

Verify the audit chain

Prove the audit trail has not been edited or truncated.

Prove the audit trail has not been edited or truncated.

Each row's hash covers its own contents AND its position in the chain, so three different tamperings produce three different findings:

  • editing a row -> hash_mismatch on that row;
  • deleting a row from the middle -> broken_link on its successor AND missing_record for the gap, because re-linking would require rewriting every later hash and each rewrite is itself a mismatch;
  • deleting the newest rows -> missing_record against the chain head.

Legitimate retention pruning is NOT a discrepancy: the sweep records how far it pruned and the surviving chain links to that record, so a trail that starts mid-sequence still verifies clean.

BOUNDED AND RESUMABLE. At most limit rows per call; when next_seq is non-null there is more to check, so pass it back as start_seq. A year of rows is verified in pages rather than in one query that times out.

Declared before GET /v2/audit-logs/{event_id}: registered the other way round, this path would arrive there as event_id="verify".

Errors: 401, 403 (missing audit:read), 429, 503.

Rate limit: 1000 requests per minute per organization (bucket default). Exceeding it returns 429 with Retry-After; the X-RateLimit-* response headers report your remaining allowance on every call.

GET
/v2/audit-logs/verify
/v2/audit-logs/verify?start_seq=4321&limit=500

The Authorization access token

Authorization

Authorization
Required
Bearer <token>

Your Graine API key. Create one in the dashboard under Developers, or via POST /v2/api-keys. Send it as Authorization: Bearer <key>.

In: header

Query Parameters

start_seqStart Seq

Resume from this sequence number. Use the next_seq from the previous call.

Minimum: 1

limitLimit

Rows to walk in this call.

Default: 500Minimum: 1Maximum: 2000

organization_idOrganization Id

Must match the organisation the API key belongs to.

Response Body

200

Every discrepancy found in the window that was walked.

organization_id
Required
Organization Id

The organization whose chain was walked.

valid
Required
Valid

True when this window contained no discrepancies at all.

checked
Required
Checked

Rows examined in this call.

first_seqFirst Seq | null

First sequence number examined in this call.

last_seqLast Seq | null

Last sequence number examined in this call.

head_seqHead Seq | null

The newest sequence number the chain head knows about.

pruned_through_seqPruned Through Seq | null

Rows at or below this sequence were removed by the retention sweep, not by tampering. A chain that starts just above it and links to the recorded pruned hash verifies clean.

problems
Required
Problems

Empty when 'valid' is true.

next_seqNext Seq | null

Pass as start_seq to continue. Null when the whole chain has been walked.

verified_atVerified At | null

ISO-8601 UTC.

401

1100 — missing, unknown or inactive API key. 1101 — a browser session token was presented.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

403

1102 — the key is valid but lacks the audit:read scope, or names another organisation.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

422

1001 — a query parameter failed validation; the message names the field.

error
Required
integer

Stable integer code from the error table. Branch on this, not on the message.

message
Required
string

One human-readable sentence. Wording may change; the code will not.

429

1300 — per-organisation rate limit exceeded. Carries Retry-After.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

503

1501 — the audit log could not be read. Deliberately NOT an empty page.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

curl -X GET "https://api.graine.ai/v2/audit-logs/verify?start_seq=4321&limit=500&organization_id=string" \
  -H "Authorization: Bearer <token>"

Every discrepancy found in the window that was walked.

{
  "organization_id": "organization-live-EXAMPLE",
  "valid": true,
  "checked": 500,
  "first_seq": 4321,
  "last_seq": 4820,
  "head_seq": 4821,
  "pruned_through_seq": 3999,
  "problems": [
    {
      "problem": "hash_mismatch",
      "seq": 4817,
      "event_id": "c8d41e0b-6a92-4f37-b5de-9a70c2f18e43",
      "expected_seq": 4817,
      "found_seq": 4819,
      "missing_count": 2,
      "expected_prev_hash": "4b7e9a1c2d3f4051627384950a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d",
      "found_prev_hash": "9f2c1d84a7e30b5f6c8d1e2a3b4c5d6e7f8091a2b3c4d5e6f7081920a3b4c5d6",
      "detail": "Sequence jumped from 4816 to 4819; two rows are missing."
    }
  ],
  "next_seq": 4821,
  "verified_at": "2026-08-27T09:30:00.000Z"
}