# 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](https://www.vestiarion.xyz/docs/get-started/authentication) as `Authorization: Bearer <key>`.

## Parameters

| Name | In | Type | Required | Default | Allowed values | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `limit` | query | integer | Optional | `50` | 1 to 200 | How many items to return. Defaults to 50; a larger value is capped at 200. |
| `cursor` | query | string | Optional | — | — | 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`. |
| `domain` | query | string | Optional | — | — | Only entries in this domain. |
| `actor` | query | string | Optional | — | — | Only entries written by this actor. |

## Code samples

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

## Response

Example, `200` `application/json`:

```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**

- `data` (array of object, required)
  - `seq` (number, required): Monotonic within a workspace, but not gap-free; the hash chain proves continuity.
  - `id` (string, required)
  - `ts` (string, required)
  - `actor` (string, required)
  - `domain` (string, required)
  - `action` (string, required)
  - `summary` (string, required)
  - `detail` (object, required)
  - `bodyHash` (string, required): Present so a consumer can verify the chain itself rather than trust us.
  - `signature` (string, required)
  - `prevHash` (string, required)
  - `hash` (string, required)
  - `signingKeyId` (string, nullable, required): 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.
- `page` (object, required): Where this page sits in the collection.
  - `nextCursor` (string, nullable, required): Pass back as `?cursor=` to continue. Null when the end is reached.
  - `hasMore` (boolean, required): Whether another page follows this one.
  - `count` (integer, required): How many items this response carries.

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `invalid_request` | An 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. |
| 401 | `unauthorized` | No key, or a malformed, unknown or revoked one: "A valid API key is required." |
| 403 | `forbidden` | The 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." |
| 500 | `internal` | An 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](https://www.vestiarion.xyz/docs/get-started/pagination#the-ledger-cursor-is-a-watermark) 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](https://www.vestiarion.xyz/docs/api/verify-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](https://www.vestiarion.xyz/docs/webhooks/verify#verifying-a-ledger-entry-s-ed25519-signature) shows the check.
