Skip to content
VestiarionDocs

API reference

List ledger entries

GET/api/v1/ledger

The audit chain, oldest first, as a resumable stream. Because the ledger is append-only and ascending by seq, a stored page.nextCursor is a watermark: a request from it never returns an entry before it. Persist the last non-null nextCursor only after processing every entry in the responses read, and resume from it; the last page, which had no cursor of its own, is returned again, so de-duplicate on seq.

seq is monotonic within a workspace but not gap-free; continuity is proven by the hash chain, not by seq.

Send a workspace API key as Authorization: Bearer ….

Parameters

  • limitintegeroptional

    How many items to return. Defaults to 50; a larger value is capped at 200.

    Default:50

    Allowed:1 to 200

  • cursorstringoptional

    The previous response's page.nextCursor, passed back unchanged to continue. Opaque: never decode or construct one. A cursor this endpoint could not have issued is refused with 400.

  • domainstringoptional

    Only entries in this domain.

  • actorstringoptional

    Only entries written by this actor.

Try it

The key is kept in memory only, for this page.

Code samples

cURL
curl "https://www.vestiarion.xyz/api/v1/ledger" \
  -H "Authorization: Bearer $VESTIARION_API_KEY"

Response

Example
200 · application/json
{
  "data": [
    {
      "seq": 82,
      "id": "4566714f-a9f0-42c8-bcd4-d89adf830806",
      "ts": "2026-09-24T11:55:00.370366+00:00",
      "actor": "system",
      "domain": "system",
      "action": "seed",
      "summary": "Seeded demo business: Northstar Studio",
      "detail": {
        "accounts": 3,
        "invoices": 6,
        "milestones": 3,
        "amountScale": 0.001,
        "counterparties": 7
      },
      "bodyHash": "8780d07cb3d083d359119e58fd4783f5e4b32aac14e0a8d9f7a5bb13365a4537",
      "signature": "c14f06b751d31be6566ed11676d60d2db731ab8bbc5972f79d0a0c131e9e37200303d5b3ad04993b7973b1ce7200e7214e5a9b511306fa30538f072eb0d34705",
      "prevHash": "0000000000000000000000000000000000000000000000000000000000000000",
      "hash": "b0ac72908868ddd5ed4722fd8a14ff4a844eee4dad382c5c85288bca73737669",
      "signingKeyId": null
    }
  ],
  "page": {
    "nextCursor": "eyJrIjo4Mn0",
    "hasMore": true,
    "count": 1
  }
}
Fields
  • dataarray of object
    13 fields in data
    • seqnumber

      Monotonic within a workspace, but not gap-free; the hash chain proves continuity.

    • idstring
    • tsstring
    • actorstring
    • domainstring
    • actionstring
    • summarystring
    • detailobject
    • bodyHashstring

      Present so a consumer can verify the chain itself rather than trust us.

    • signaturestring
    • prevHashstring
    • hashstring
    • signingKeyIdstring · nullable

      Which key signed the entry; null for entries written before key identity existed. A consumer verifying for itself needs this to pick the right key.

  • pageobject

    Where this page sits in the collection.

    3 fields in page
    • nextCursorstring · nullable

      Pass back as ?cursor= to continue. Null when the end is reached.

    • hasMoreboolean

      Whether another page follows this one.

    • countinteger

      How many items this response carries.

Errors

StatusCodeWhen
400invalid_requestAn invalid limit or cursor, a filter value outside its allowed values, or a request body that does not validate. The message names the parameter or the field, and lists the accepted values.
401unauthorizedNo key, or a malformed, unknown or revoked one: "A valid API key is required."
403forbiddenThe key's scopes do not cover this route: "This key cannot do that." Or, on a write, the person who created the key can no longer add records: "This key's issuer can no longer add records in this workspace."
500internalAn unexpected server error. Implementation details are not exposed.

Notes

Resuming from a cursor

Persist page.nextCursor only after processing every entry in that response, and pass it back unchanged:

text
GET /api/v1/ledger?limit=100
GET /api/v1/ledger?limit=100&cursor=<previous page.nextCursor>

Continue until hasMore is false and nextCursor is null. On the next poll, reuse the last non-null cursor you processed: it returns the entries after it, starting with the last page you read, then everything appended since, and never an entry before it. Store entries keyed by seq and skip the ones you already have. Pagination walks through it.

A malformed cursor answers 400 invalid_request. The API never silently restarts from the beginning.

seq and the hash chain

seq is a global identity column, so within one workspace it rises but is not gap-free. Continuity is proven by the hash chain, not by seq running without gaps: each entry's prevHash is the hash of the entry before it, and the first entry's prevHash is 64 zeros. Verify the ledger replays the chain and the signatures for you.

Filters

domain and actor match exactly. Unlike the enumerated filters on other endpoints, they accept any value: a value no entry has returns an empty page, not an error.

signingKeyId

Each entry names the key that signed it in signingKeyId: the first 16 hex characters of SHA-256 over the signing key's SPKI DER. Older entries carry null. The label is outside both the body hash and the chain hash: it selects which key to check against, and proves nothing by itself. Verifying a ledger entry's signature shows the check.