> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nohead.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Revisions and history

> See who changed a record, when, through what and why, compare any two versions, revert, redact values that should never have been stored, and read the organization's activity log.

Every change to a record is a revision. Revisions are numbered per record from 1, numbers are never reused, and the current one is the record's `revision` (and its `ETag`). Changes add revisions and never edit old ones: only redaction, the plan's retention and purges of deleted data remove anything from history.

In the web app, open a record's **History** tab. Through the API, reading history needs `records:read`, and reverting `records:write` (and `records:read` to preview a revert).

## What a revision records

Each revision has:

* `operation`: `create`, `update`, `publish`, `unpublish`, `delete`, `restore`, `revert`, `migrate` or `merge`
* `actor`: the person or API key that made the change (`type` is `user`, `api_key` or `system`), with its name
* `via`: the client it came through: `ui`, `api`, `cli`, `sdk`, `mcp`, `system` or `scheduler`
* `note`: the change note, if one was sent
* `changed_fields`: the keys of the fields (and `published_at` or `deleted_at`) it changed
* `draft`: for a `merge`, the draft it merged, with who started it and who edited it
* `reverted_to_revision`: for a `revert`, the revision it went back to

A single revision also has `data`, the record's values as of that revision, and `changes`, each with `before` and `after`. Changes name a field by its current key and its stable `field_id`, so history stays readable after a field is renamed.

## List and read revisions

Lists are newest first. Filter by `operation`, by `actor_id` (a `usr_` or `key_` ID) or by a `created_at` range:

```bash theme={null}
curl -g "https://api.nohead.io/v1/records/rec_01J9ZQ3F8X/revisions?filter[operation]=publish" \
  -H "Authorization: Bearer $NOHEAD_API_KEY"
```

```ts TypeScript theme={null}
for await (const revision of nohead.records.revisions.list(id)) {
  console.log(revision.revision, revision.operation, revision.actor.name, revision.via, revision.note)
}
const third = await nohead.records.revisions.get(id, 3) // data and changes
const fromWebhook = await nohead.revisions.get("rev_01J9ZQ3F8X")
```

```python Python theme={null}
for revision in nohead.records.revisions.list(record_id):
    print(revision.revision, revision.operation, revision.actor.name, revision.via, revision.note)
third = nohead.records.revisions.get(record_id, 3)  # data and changes
from_webhook = nohead.revisions.get("rev_01J9ZQ3F8X")
```

```ruby Ruby theme={null}
nohead.records.revisions.list(id).each do |revision|
  puts [revision.revision, revision.operation, revision.actor.name, revision.via, revision.note].join(" ")
end
third = nohead.records.revisions.get(id, 3) # data and changes
from_webhook = nohead.revisions.get("rev_01J9ZQ3F8X")
```

Webhooks and the activity log name a revision by its `rev_` ID (`revision_id`); `GET /v1/revisions/{revision_id}` reads it. History stays readable while a record is deleted.

## Compare two revisions

`GET /v1/records/{record_id}/diff?from=3&to=7` returns the `changes` between any two revisions, field values and `published_at` alike. In the web app, open a revision and pick **Compare with #7**.

```ts TypeScript theme={null}
const diff = await nohead.records.diff(id, { from: 3, to: 7 })
for (const change of diff.changes) console.log(change.field ?? change.attribute, change.before, change.after)
```

```python Python theme={null}
diff = nohead.records.diff(record_id, from_revision=3, to_revision=7)
for change in diff.changes:
    print(change.field or change.attribute, change.before, change.after)
```

```ruby Ruby theme={null}
diff = nohead.records.diff(id, from: 3, to: 7)
diff.changes.each { |change| puts "#{change["field"] || change["attribute"]}: #{change["before"]} -> #{change["after"]}" }
```

## Revert

Reverting writes a new `revert` revision with an earlier revision's values; nothing in between is lost. In the web app, open a revision, choose **Revert to this revision**, check the changes and choose **Revert record**. Through the API, preview first with `dry_run`, which writes nothing:

```ts TypeScript theme={null}
const preview = await nohead.records.revisions.revert(id, 3, { dry_run: true })
console.log(preview.changes)
await nohead.records.revisions.revert(id, 3, {}, { changeNote: "Undo the broken import" })
```

```python Python theme={null}
preview = nohead.records.revisions.revert(record_id, 3, dry_run=True)
print(preview.changes)
nohead.records.revisions.revert(record_id, 3, change_note="Undo the broken import")
```

```ruby Ruby theme={null}
preview = nohead.records.revisions.revert(id, 3, dry_run: true)
puts preview.changes
nohead.records.revisions.revert(id, 3, change_note: "Undo the broken import")
```

The old values are mapped onto the current schema:

* A field whose type changed since gets its old value converted, as a field migration would. A value that can't be converted fails the revert (`422`), and a field whose old and new types have no conversion keeps its current value.
* Fields added since, and fields whose old value was redacted, keep their current values. Deleted fields don't come back.
* The result is validated like any update, and whether the record is published doesn't change. Reverting a live record changes what is live.

A deleted record must be restored before it can be reverted (`409`). Send `If-Match` to revert only if nobody changed the record since you looked. To review an old version before it goes live, start a [draft](https://docs.nohead.io/guides/publishing.md#hold-changes-back-in-a-draft) from it with `from_revision` instead.

## Change notes and attribution

Every change is attributed to the person or API key that made it. Give each tool, agent and CI job a key of its own, so history tells them apart. To say why, send `Nohead-Change-Note` when you create, update, publish, unpublish, delete, restore or revert a record, change records in a batch, merge a draft, or change a schema (fields, collections, field migrations). The note is percent-encoded UTF-8, as `encodeURIComponent` encodes it, at most 500 characters once decoded; plain ASCII can be sent as it is.

```bash theme={null}
curl -X PATCH "https://api.nohead.io/v1/records/rec_01J9ZQ3F8X" \
  -H "Authorization: Bearer $NOHEAD_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Nohead-Change-Note: Fix%20the%20price%20of%20the%20large%20size" \
  -d '{"data": {"price": 24}}'
```

The SDKs encode it for you:

```ts TypeScript theme={null}
await nohead.records.update(id, { data: { price: 24 } }, { changeNote: "Fix the price of the large size" })
```

```python Python theme={null}
nohead.records.update(record_id, data={"price": 24}, change_note="Fix the price of the large size")
```

```ruby Ruby theme={null}
nohead.records.update(id, data: { price: 24 }, change_note: "Fix the price of the large size")
```

The SDKs also name themselves in `Nohead-Client`, so their changes show `via: sdk`. Changes made by a schedule show `actor.type: system` and `via: scheduler`. The [MCP server](https://docs.nohead.io/guides/mcp.md) requires a note on every revision it makes.

## Redact a field's past values

When a value should never have been stored, such as personal data or a secret, owners and admins can remove it from history in the web app. On the record's **History** tab, choose **Redact…**, pick the **Field** and the **Values to remove** (every past value, or the value set in one revision), check the preview, type the field's key and choose **Redact permanently**.

> [!WARNING]
>
> Redaction can't be undone.

* Each removed value reads `{"$redacted": true}` wherever it appears in revision snapshots and changes, and those revisions list the field in `redacted_fields`.
* Copies in stored webhook payloads are removed too.
* The current value is never touched. If it is the problem, change the record first, then redact.
* Reverting to a redacted revision keeps the field's current value.
* Redactions are recorded in the activity log, and sent to webhooks, as `record.redacted`.

Redaction is not part of the API: API keys can't redact.

## How long history is kept

How long revisions are kept depends on the organization's [plan](https://nohead.io/pricing). Once a day, older revisions are removed, but every record keeps its first revision and its last 10. A record's `history_pruned_through` is the highest revision number removed (`null` while nothing was), and the **History** tab says which revisions were removed. For 30 days after a move to a plan that keeps less, nothing is removed.

> [!NOTE]
>
> History doesn't outlive deletion: when a deleted record is purged, its revisions go with it, and when a deleted field is purged, its values are removed from every revision.

Published-only API keys can't read history (`403`), since it holds unpublished content.

## Activity log

Revisions hold what a record's content was. The activity log holds everything else that changed in the organization, and who changed it, through what: every record, draft, schema, asset, webhook, API key and search token change in its projects, and changes to the projects, the organization, its members, invitations and plan. Each event names its type (such as `record.published` or `member.role_changed`), its actor and its `via`, with small references such as a `revision_id`, but never content. Reads aren't recorded.

Admins and owners read it on the organization's **Activity** page. Filter it by project, by what changed (**Show**: **Records**, **Drafts**, **Schema**, **Members** and so on) and by **Who**: a member, **All API keys** or **System**; choosing a name in the list shows only their activity. **Client** shows what each change came through: **Web app**, **API**, **CLI**, **SDK**, **MCP**, **System** or **Scheduler**. A record's or collection's event links to it.

An API key with **Read the activity log** (`audit:read`) reads its own project's events with `GET /v1/projects/{project_id}/audit-events`, newest first. Filter by `type`, exactly or by prefix (`filter[type]=record.*`), by `resource_type` and `resource_id`, by `actor_type` and `actor_id`, and by a `created_at` range. `$PROJECT_ID` is your key's project: `GET /v1/me` returns it as `api_key.project_id`.

```bash theme={null}
curl -g "https://api.nohead.io/v1/projects/$PROJECT_ID/audit-events?filter[type]=record.*&filter[actor_type]=api_key" \
  -H "Authorization: Bearer $NOHEAD_API_KEY"
```

```ts TypeScript theme={null}
for await (const event of nohead.auditEvents.list({ filter: { type: "record.*", created_at: { gte: "2026-10-01T00:00:00Z" } } })) {
  console.log(event.created_at, event.type, event.actor.name, event.via)
}
```

```python Python theme={null}
for event in nohead.audit_events.list(filter={"type": "record.*", "created_at": {"gte": "2026-10-01T00:00:00Z"}}):
    print(event.created_at, event.type, event.actor.name, event.via)
```

```ruby Ruby theme={null}
nohead.audit_events.list(filter: { type: "record.*", created_at: { gte: "2026-10-01T00:00:00Z" } }).each do |event|
  puts [event.created_at, event.type, event.actor.name, event.via].join(" ")
end
```

```bash CLI theme={null}
nohead audit --type "record.*" --since 2026-10-01T00:00:00Z
```

Only admins and owners can create a key with this scope, and published-only keys can't have it. Events about the organization itself, such as members and billing, are on the **Activity** page only. [Webhooks](https://docs.nohead.io/guides/webhooks.md#event-types) get many of the same events, with the same types, as they happen.

How long the activity log is kept depends on the organization's [plan](https://nohead.io/pricing); older events are removed once a day. As with history, nothing is removed for 30 days after a move to a plan that keeps less. Every endpoint is in the [API reference](https://docs.nohead.io/api-reference/overview.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.