# Verify the audit log's hash chains

> The audit_log.verify operation: GET /api/v1/audit_log/verify in the REST API and the audit_log_verify tool in the MCP server.

Checks the organization's audit log hash chains now and returns their status. The log is append-only and
hash-chained: each entry's `hash` covers the previous entry's hash and the entry itself, in 16 chains per
organization (an entry's chain is chosen by its target). A changed, deleted, reordered or forged entry breaks its
chain.

Each chain is checked over its newest 100 entries (links, hashes, the digests of the
personal data kept outside the hash, and the chain's head); a shorter chain is checked from its first entry, and
then `complete` is true. `chains` says, per chain, how many entries it holds and which positions verified
(`verifiedFrom` to `verifiedTo`), or where it broke; `brokenAt` names the first break, in chain order (or
`entryId: null` when the chain's head is the problem).

Every entry of every chain, including those older than your plan's visibility window, is also verified each night
by Ownrecord, whose staff are alerted to a break (`AUDIT_CHAIN_BROKEN`). `nightly` is the latest of those
verifications: when it ran, how many entries verified, and where it found a break (null before the first one).
`status` is `broken` when the check now or the latest nightly verification found a break.

## At a glance

- **REST:** `GET /api/v1/audit_log/verify`
- **MCP tool:** `audit_log_verify`
- **Operation ID:** `audit_log.verify`
- **Scope:** Organization
- **Permission:** `audit:read`
- **Risk tier:** `read`
- **Dry run:** Not supported
- **Idempotency:** Not needed (read only)
- **Modes:** Test and live
- **Authentication:** API key with the `audit:read` scope
- **Since:** M10

## Request

### Query parameters

This operation takes no query parameters.

## Response

`200 OK` with a JSON object:

| Field                   | Type                               | Description |
| ----------------------- | ---------------------------------- | ----------- |
| `object`                | `"audit_log_verification"`         |             |
| `status`                | one of `intact`, `broken`          |             |
| `complete`              | boolean                            |             |
| `window`                | integer (at least 1)               |             |
| `entries`               | integer (at least 0)               |             |
| `checkedEntries`        | integer (at least 0)               |             |
| `checkedAt`             | string                             |             |
| `brokenAt`              | object or null                     |             |
| `chains`                | array of object                    |             |
| `chains[].shard`        | integer (at least 0)               |             |
| `chains[].chain`        | string                             |             |
| `chains[].entries`      | integer (at least 0)               |             |
| `chains[].verifiedFrom` | integer (at least 0) or null       |             |
| `chains[].verifiedTo`   | integer (at least 0) or null       |             |
| `chains[].status`       | one of `intact`, `broken`, `empty` |             |
| `chains[].brokenAt`     | string or null                     |             |
| `chains[].reason`       | string or null                     |             |
| `nightly`               | object or null                     |             |

## Examples

### An intact audit log

Every chain is longer than the window, so the newest 100 entries of each were checked.

```http
GET /api/v1/audit_log/verify
Authorization: Bearer or_test_…
```

Response (`200`):

```json
{
  "object": "audit_log_verification",
  "status": "intact",
  "complete": false,
  "window": 100,
  "entries": 3720,
  "checkedEntries": 1600,
  "checkedAt": "2026-11-02T09:20:00.000Z",
  "brokenAt": null,
  "chains": [
    {
      "shard": 0,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3",
      "entries": 180,
      "verifiedFrom": 81,
      "verifiedTo": 180,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 1,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#1",
      "entries": 187,
      "verifiedFrom": 88,
      "verifiedTo": 187,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 2,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#2",
      "entries": 194,
      "verifiedFrom": 95,
      "verifiedTo": 194,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 3,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#3",
      "entries": 201,
      "verifiedFrom": 102,
      "verifiedTo": 201,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 4,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#4",
      "entries": 208,
      "verifiedFrom": 109,
      "verifiedTo": 208,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 5,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#5",
      "entries": 215,
      "verifiedFrom": 116,
      "verifiedTo": 215,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 6,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#6",
      "entries": 222,
      "verifiedFrom": 123,
      "verifiedTo": 222,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 7,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#7",
      "entries": 229,
      "verifiedFrom": 130,
      "verifiedTo": 229,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 8,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#8",
      "entries": 236,
      "verifiedFrom": 137,
      "verifiedTo": 236,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 9,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#9",
      "entries": 243,
      "verifiedFrom": 144,
      "verifiedTo": 243,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 10,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#10",
      "entries": 250,
      "verifiedFrom": 151,
      "verifiedTo": 250,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 11,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#11",
      "entries": 257,
      "verifiedFrom": 158,
      "verifiedTo": 257,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 12,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#12",
      "entries": 264,
      "verifiedFrom": 165,
      "verifiedTo": 264,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 13,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#13",
      "entries": 271,
      "verifiedFrom": 172,
      "verifiedTo": 271,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 14,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#14",
      "entries": 278,
      "verifiedFrom": 179,
      "verifiedTo": 278,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 15,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#15",
      "entries": 285,
      "verifiedFrom": 186,
      "verifiedTo": 285,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    }
  ],
  "nightly": {
    "status": "intact",
    "checkedAt": "2026-11-02T03:00:04.512Z",
    "entries": 3708,
    "brokenAt": null
  }
}
```

### A break in older entries, found at night

The newest entries verify now, but last night's verification of every entry found a changed older entry.

```http
GET /api/v1/audit_log/verify
Authorization: Bearer or_test_…
```

Response (`200`):

```json
{
  "object": "audit_log_verification",
  "status": "broken",
  "complete": false,
  "window": 100,
  "entries": 3720,
  "checkedEntries": 1600,
  "checkedAt": "2026-11-02T09:20:00.000Z",
  "brokenAt": null,
  "chains": [
    {
      "shard": 0,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3",
      "entries": 180,
      "verifiedFrom": 81,
      "verifiedTo": 180,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 1,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#1",
      "entries": 187,
      "verifiedFrom": 88,
      "verifiedTo": 187,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 2,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#2",
      "entries": 194,
      "verifiedFrom": 95,
      "verifiedTo": 194,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 3,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#3",
      "entries": 201,
      "verifiedFrom": 102,
      "verifiedTo": 201,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 4,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#4",
      "entries": 208,
      "verifiedFrom": 109,
      "verifiedTo": 208,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 5,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#5",
      "entries": 215,
      "verifiedFrom": 116,
      "verifiedTo": 215,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 6,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#6",
      "entries": 222,
      "verifiedFrom": 123,
      "verifiedTo": 222,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 7,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#7",
      "entries": 229,
      "verifiedFrom": 130,
      "verifiedTo": 229,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 8,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#8",
      "entries": 236,
      "verifiedFrom": 137,
      "verifiedTo": 236,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 9,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#9",
      "entries": 243,
      "verifiedFrom": 144,
      "verifiedTo": 243,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 10,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#10",
      "entries": 250,
      "verifiedFrom": 151,
      "verifiedTo": 250,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 11,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#11",
      "entries": 257,
      "verifiedFrom": 158,
      "verifiedTo": 257,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 12,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#12",
      "entries": 264,
      "verifiedFrom": 165,
      "verifiedTo": 264,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 13,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#13",
      "entries": 271,
      "verifiedFrom": 172,
      "verifiedTo": 271,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 14,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#14",
      "entries": 278,
      "verifiedFrom": 179,
      "verifiedTo": 278,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    },
    {
      "shard": 15,
      "chain": "org_01m4ttwd3veh9apbewxheq5xh3#15",
      "entries": 285,
      "verifiedFrom": 186,
      "verifiedTo": 285,
      "status": "intact",
      "brokenAt": null,
      "reason": null
    }
  ],
  "nightly": {
    "status": "broken",
    "checkedAt": "2026-11-02T03:00:04.512Z",
    "entries": 0,
    "brokenAt": {
      "entryId": "aud_01m2q8x7c1e9vbs3k0a5f7h2jd",
      "reason": "hash mismatch: the entry was changed after it was written"
    }
  }
}
```

## Errors

| Code                                                                                   | HTTP status | When                                                                                                                                        | What to do                                                                      |
| -------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied) | 403         | You can't read the organization's audit log, or you're limited to some of its entities or brands (the chains are the whole organization's). | Ask an organization owner or admin for a role or scope that allows this action. |

It can also return these [common errors](https://ownrecord.io/docs/reference.md#common-errors): [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed), [`api_version_unsupported`](https://ownrecord.io/docs/reference/errors.md#api_version_unsupported), [`authentication_required`](https://ownrecord.io/docs/reference/errors.md#authentication_required), [`api_key_invalid`](https://ownrecord.io/docs/reference/errors.md#api_key_invalid), [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found), [`dry_run_not_supported`](https://ownrecord.io/docs/reference/errors.md#dry_run_not_supported), [`rate_limited`](https://ownrecord.io/docs/reference/errors.md#rate_limited), [`internal_error`](https://ownrecord.io/docs/reference/errors.md#internal_error).
