# Verify the ledger

`GET /api/v1/ledger/verify`

Replays signatures, body hashes and hash-chain continuity for the workspace the calling key belongs to.

`valid` has three values, not two. `true` verified and `false` broken are findings about the chain; `null` means no verdict was produced, because there was no key to check authorship against. `reason` says which case it is. A key that is stored but cannot be read is a configuration problem, reported in `warnings`, not a finding about the chain.

Send a [workspace API key](https://www.vestiarion.xyz/docs/get-started/authentication) as `Authorization: Bearer <key>`.

## Parameters

No parameters.

## Code samples

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

## Response

Example, `200` `application/json`:

```json
{
  "data": {
    "valid": true,
    "checkedEntries": 99
  }
}
```

**Fields**

- `data` (object, required): The verdict of replaying the workspace's ledger: signatures, body hashes and hash continuity.
  - `valid` (boolean, nullable, required): `true` verified, `false` broken, and `null` not checked, which is a third answer, not a soft failure. A workspace holding no public key has produced no evidence either way.
  - `checkedEntries` (number, required)
  - `brokenAt` (number, optional): The `seq` of the first entry that failed. Absent unless `valid` is `false`.
  - `reason` (string, optional): Why the verdict is what it is, when it is not a plain `true`.
  - `warnings` (array of string, optional): Configuration problems found on the way to this verdict; not about the chain.

## Errors

| Status | Code | When |
| --- | --- | --- |
| 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

### `valid` has three values

`true` (verified) and `false` (broken) are findings about the chain. **`null` means no verdict was produced**: the workspace has no ledger public key to check against, so authorship was never checked. A consumer that treats `null` as a failure will report a tampered audit trail when a key was never set up for the workspace. `reason` says which case it is, and `brokenAt` is absent whenever `valid` is `null`.

```json
{"data":{"valid":null,"checkedEntries":99,"reason":"no ledger public key is configured, so authorship was not checked"}}
```

### A key that cannot be read

A key that is stored but cannot be read, such as a PEM whose newlines were lost, is a **configuration problem, not a finding about the chain**, and the two are kept apart. Verification never fails over a bad key: it verifies with whatever it could read, and reports what it could not in `warnings`:

```json
{"data":{"valid":true,"checkedEntries":105,"warnings":["The ledger signing key is not a readable private key: error:1E08010C:DECODER routines::unsupported"]}}
```

When a broken key is the reason no key is available at all, `reason` names it, instead of saying "no ledger public key is configured".

### Key identity and rotation

Every entry written since signing key ids were introduced carries `signingKeyId`; older entries carry `null`. Verification accepts a **keyring**: the workspace's current key plus the earlier public keys the deployment still accepts. A labeled entry is checked against the key it names, and an unlabeled one against any key in the ring. So a rotated key keeps the history it signed verifiable, and a label the ring does not know is reported as its own case, again with `valid: null`:

```json
{"data":{"valid":null,"checkedEntries":112,"reason":"entry #104 was signed by key 4f2a9c1e88b30d57, which is not in this deployment's keyring (a71b0e6640cc2f93), so its authorship was not checked"}}
```

That message names the missing key and the ones the deployment has: the difference between a suspected forgery and a retired key that was never added to the keyring.

A rotation is recorded in the ledger itself, as a `system` entry with `action: "ledger_key_rotated"`, signed by the new key. When an owner rotates the key from Settings, the entry has `actor: "human"` and its `detail` is `{"from": "<old id>", "to": "<new id>", "by": "<user id>"}`, where `by` is the owner who rotated. When the key changed any other way, the first append under the new key writes the entry automatically, with `actor: "system"` and `detail` `{"from": "<old id>", "to": "<new id>"}`. Either way, a consumer following the ledger sees the change of authority as an entry.

From that entry on, `signingKeyId` is the new key's id. Earlier entries keep the old id, and keep verifying against the retired key, which stays in the workspace's keyring.

### The public key

This endpoint reports whether the chain checks out; it does not return the public key. To check signatures yourself, take the PEM and its key id from the workspace's Audit page, `/o/<slug>/audit`, under "Ledger signing public key". [Verifying a ledger entry's signature](https://www.vestiarion.xyz/docs/webhooks/verify#verifying-a-ledger-entry-s-ed25519-signature) shows how.

The Audit page itself calls a separate route, `GET /api/ledger/verify?org=<slug>`, which takes a signed-in member's session rather than an API key and answers in a legacy bare shape. It is not part of the v1 API; integrations use this endpoint.
