Skip to content

API reference

03 · API reference

Two endpoints. There is no update route and no delete route, deliberately and permanently — an append-only log whose API can rewrite entries isn't append-only. If a record needs correcting, append a correction; that's what an audit trail is for.

Authentication

Every request needs an API key, sent one of two ways:

Authorization: Bearer <key>preferred
X-API-Key: <key>also accepted

A missing key, an unparseable key, and a well-formed key that doesn't belong to any customer all return the identical 401 response — same status, same body. This is deliberate: distinguishing "bad key" from "key belongs to nobody" would leak whether a given key string is real.

POST /v1/events

Records one event and returns immediately with a pending receipt — before the batch it lands in has been anchored. The write path never blocks on a blockchain transaction; anchoring happens on the platform's own schedule.

Request body

fieldtyperequirednotes
eventJSON valueyesAny JSON object/array/primitive that canonicalises without error — see canonical JSON. Object fields, in particular, must not contain non-integer numbers.
idempotencyKeystringnoSee idempotency below.
curl -X POST https://api.backrun.example/v1/events \
  -H "Authorization: Bearer $BACKRUN_KEY" \
  -d '{"event":{"actor":"bob@acme.example","action":"record.delete","recordId":4471},
       "idempotencyKey":"acme-88213"}'

Response — 201 Created (new event)

{
  "status": "pending",
  "eventId": "b1f0e2a4-…",
  "leaf": "0x68081b59…",
  "canonical": "{\"action\":\"record.delete\",…}",
  "receivedAt": "2026-08-11T21:04:12.000Z"
}

canonical is the exact string that was hashed — echoing it back lets you confirm your event canonicalised the way you expected before the batch is even anchored. It becomes null later if the event is erased (see erasure & retention); the leaf and proof survive erasure, the payload does not.

Response — 200 OK (deduplicated retry)

Identical body to the 201 case, same eventId, but status 200 because nothing new was created — see idempotency.

Error responses

statuscodewhen
400invalid_jsonBody is not parseable JSON.
400invalid_requestBody isn't a JSON object; event is missing; or idempotencyKey is present but not a string.
401unauthorizedMissing, malformed, or unrecognised API key.
413payload_too_largeBody exceeds the size limit (Content-Length checked first, then the actual body re-checked after reading — a forged header can't bypass the limit). Default limit: 64 KiB.
422uncanonicalisable_eventThe event contains something canonicalisation rejects — most commonly a non-integer number. Nothing is stored; this fails at the door rather than being accepted and found unverifiable later.

Error body shape, consistent across every endpoint:

{ "error": { "code": "uncanonicalisable_event", "message": "non-integer number 0.1 cannot be canonicalised deterministically; send a string or a scaled integer instead" } }

Idempotency

Send the same idempotencyKey twice (from the same customer) and the second call returns the original event — same eventId, same leaf — with status 200 instead of 201, and nothing new is written. Retries are normal for any HTTP client, and a duplicated audit entry is itself a corrupted record: "did this happen once or twice?" is exactly the question an audit log exists to answer, so the API answers it rather than silently creating two records.

Scoping is per customer — two different customers may legitimately reuse the same key string without colliding. There is no global idempotency namespace.

GET /v1/events/:id/receipt

Returns the receipt for an event you submitted — pending if its batch hasn't anchored yet, anchored once it has. No parameters beyond the event id in the path.

curl https://api.backrun.example/v1/events/b1f0e2a4-…/receipt \
  -H "Authorization: Bearer $BACKRUN_KEY"

Response — 200, status pending

{
  "status": "pending",
  "eventId": "b1f0e2a4-…",
  "leaf": "0x68081b59…",
  "canonical": "{\"action\":…}",
  "receivedAt": "2026-08-11T21:04:12.000Z"
}

Also returned for an event whose batch was created but whose anchor transaction hasn't confirmed yet — reporting pending in that case is more honest than issuing a proof against a root that doesn't exist on-chain yet.

Response — 200, status anchored

{
  "status": "anchored",
  "version": 2,
  "eventId": "b1f0e2a4-…",
  "canonical": "{\"action\":…}",
  "salt": "0x0202…0202",
  "eventHash": "0x68081b59…",
  "leaf": "0x68081b59…",
  "proof": [ { "sibling": "0x…", "siblingIsLeft": true }, … ],
  "root": "0x9c84221263e9265b0e2e62bd356393fa6f30c01339e26f21ab645da2da652195",
  "batchId": "0xe1f0680050984951b9bd7f250e75441800000000000000000000000000000000",
  "chainId": 8453,
  "contract": "0x256080339DEA7E8F3089C49CCEB0F98547103f91",
  "publisher": "0x16EF29F63A4CB0624EF5c1079E08E1530A0b7142",
  "anchorTxHash": "0xa6f5999326a7e32173afb7b1d660830103254b11f63b9656bf41d1b6cf262f9a",
  "anchoredAt": "2026-08-11T20:07:31.000Z",
  "erasedAt": null
}

root, batchId, contract, publisher and anchorTxHash above are the real values from the first anchor on Base mainnet — see reading the anchor on-chain for how to look them up yourself.

Every field here, and why each one exists, is documented field-by-field on the receipt format page. This response is a receipt, in the exact shape a verifier needs — see the verification algorithm.

Error responses

statuscodewhen
401unauthorizedSame as above.
404not_foundThe event doesn't exist, or exists but belongs to a different customer. Both cases return the identical response.

Why 404, never 403, for another customer's event

A 403 Forbidden would confirm the id exists — leaking, to anyone who can guess or enumerate ids, which ones correspond to real (someone else's) audit events. Reporting "not found" either way means an id gives an attacker no information at all.

Everything else

requeststatuscode
PUT / PATCH / DELETE on /v1/events/:id404 or 405not_found / method_not_allowed
Wrong HTTP method on a known route405method_not_allowed
Any path outside /v1/, or an unrecognised route404not_found

These aren't unimplemented — they're structurally absent. There is no code path in the engine, the store, or the on-chain contract that mutates or removes a past event. See reading the anchor on-chain for the same guarantee at the contract level.