Versions
Every time a content entry is published, Noma writes an immutable snapshot of that entry to a content_entry_versions table. The row in content_entries keeps being the working draft; published reads are served from the latest snapshot.
This page documents the public Content API endpoints for listing, inspecting, reverting, and labelling versions.
Model
content_entriesis the working draft. It holds live field values, and may diverge from the last published snapshot.content_entry_versionsstores one row per publish. Each row has a monotonicversion_number(per-entry), a JSONsnapshot, alocale, atranslation_group_id, and optionallabel/description.- The entry's
published_version_id/published_version_numberpoint at the currently live version, or arenullwhen the entry is in draft state (or has never been published). is_draft_dirtyistruewhen the working draft has been edited since the last publish.
Versions are per entry, not per translation group. Each locale has its own version history.
What triggers a new version?
Saves never mint versions or change publish state — they only mutate the working draft. Versions are created only by explicit publish actions (or revert, which re-publishes a prior snapshot).
| Action | Result |
|---|---|
POST /api/{collection}/{uuid}/publish (see Publish) | New version v{N+1}, is_draft_dirty = false, state = 'published' |
POST /api/{collection}/{uuid}/unpublish (see Unpublish) | state = 'draft', published_version_id cleared, versions retained |
PUT / PATCH on an entry (draft or published) | Fields updated, is_draft_dirty = true, no new version, published_version_id unchanged |
Create an entry with state: 'published' | New version v1, is_draft_dirty = false (publish-on-create convenience) |
| Revert to a prior version | New version minted from that snapshot (vNext), is_draft_dirty = false |
Retention
The number of versions retained per entry is capped by your plan:
| Plan | Versions per entry |
|---|---|
| Basic | 10 |
| Grow | 50 |
| Pro | Unlimited |
When the cap is reached, the oldest versions are pruned on the next publish. The currently published version is never pruned.
Draft vs. Published reads
The existing Get an Entry and List Entries endpoints decide their data source from the state query parameter:
state=published(default) — serves the published snapshot for each entry. Entries that have never been published are excluded (404 on get, filtered out on list).state=draft— serves the live working-draft field values fromcontent_entries.
This means a published entry is stable: editing the draft does not change what published consumers see until you publish again.
List versions
GET /api/{collection}/{uuid}/versions HTTP/1.1
Host: app.nomacms.com
project-id: <project-uuid>
Authorization: Bearer <api-token>
Accept: application/jsonRequires the read ability on the token.
Path parameters
| Param | Description |
|---|---|
collection | Collection slug |
uuid | Content entry UUID |
Response (200)
JSON object with a data array of versions, ordered by version_number descending. Each item has this shape:
| Field | Type | Description |
|---|---|---|
uuid | string | Version UUID |
version_number | integer | Monotonic per-entry version counter (1, 2, 3, …) |
label | string | null | Optional short label |
description | string | null | Optional notes |
locale | string | Locale of the snapshot |
translation_group_id | integer | null | Translation group of the entry at publish time |
published_at | string | null | ISO 8601 timestamp |
created_at | string | ISO 8601 timestamp |
updated_at | string | ISO 8601 timestamp |
created_by | object | null | { id, name } when available |
is_current_published | boolean | true when this is the entry's currently published version |
The snapshot payload is not included on list.
Get a version
GET /api/{collection}/{uuid}/versions/{version} HTTP/1.1
Host: app.nomacms.com
project-id: <project-uuid>
Authorization: Bearer <api-token>
Accept: application/jsonRequires the read ability on the token. {version} is the integer version_number.
Response (200)
Same shape as the list item, plus a snapshot object:
{
"uuid": "…",
"version_number": 3,
"label": "Launch copy",
"description": "Approved by marketing",
"locale": "en",
"translation_group_id": 42,
"published_at": "2026-04-20T10:15:00Z",
"created_at": "2026-04-20T10:15:00Z",
"updated_at": "2026-04-20T10:15:00Z",
"created_by": { "id": 7, "name": "Alex" },
"is_current_published": true,
"snapshot": {
"fields": {
"title": "Hello World",
"cover": "a1b2c3d4-…",
"tags": ["news", "launch"]
},
"meta": {
"locale": "en",
"translation_group_id": 42
}
}
}The snapshot.fields object mirrors the same shape accepted on create/update: scalars for primitive fields, UUID arrays for media fields, UUID or ID arrays for relation fields, and nested arrays for group fields. This keeps reverts and inspections symmetric with the write API.
Revert to a version
POST /api/{collection}/{uuid}/versions/{version}/revert HTTP/1.1
Host: app.nomacms.com
project-id: <project-uuid>
Authorization: Bearer <api-token>
Accept: application/jsonRequires the update ability on the token.
Reverting restores the working draft from the target snapshot and publishes it as a brand new version (vNext). The old versions are preserved (subject to retention). After revert, is_draft_dirty becomes false and published_version_number points at the new version.
Response (200)
{
"message": "Reverted to version 3 (new version v7).",
"version": { "version_number": 7, "…": "…" },
"entry": {
"uuid": "…",
"fields": { "title": "Hello World", "…": "…" }
}
}Missing media or relation targets (for example, an asset deleted after the snapshot was taken) are skipped when the draft is restored; the snapshot itself is left untouched.
Update version metadata
Labels and descriptions are editable, but the snapshot payload itself is immutable.
PATCH /api/{collection}/{uuid}/versions/{version} HTTP/1.1
Host: app.nomacms.com
project-id: <project-uuid>
Authorization: Bearer <api-token>
Accept: application/json
Content-Type: application/jsonRequires the update ability on the token.
JSON body
| Field | Type | Description |
|---|---|---|
label | string | null | Max 255 chars |
description | string | null | Max 2000 chars |
Response (200)
The updated version resource (without snapshot).
Errors
| Status | When |
|---|---|
| 400 | Missing project-id or project cannot be resolved |
| 401 | Missing or invalid bearer token |
| 403 | Token does not have the required ability |
| 404 | Collection, entry, or version not found |
Examples
import { createClient } from "@nomacms/js-sdk"
const client = createClient({
projectId: process.env.NOMA_PROJECT_ID!,
apiKey: process.env.NOMA_API_KEY!,
})
const versions = await client.content.versions.list("blog-posts", entryUuid)
const v3 = await client.content.versions.get("blog-posts", entryUuid, 3)
await client.content.versions.updateLabel("blog-posts", entryUuid, 3, {
label: "Launch copy",
description: "Approved by marketing.",
})
await client.content.versions.revert("blog-posts", entryUuid, 3)curl -sS "https://app.nomacms.com/api/blog-posts/${ENTRY_UUID}/versions" \
-H "project-id: $NOMA_PROJECT_ID" \
-H "Authorization: Bearer $NOMA_API_KEY" \
-H "Accept: application/json"
curl -sS "https://app.nomacms.com/api/blog-posts/${ENTRY_UUID}/versions/3" \
-H "project-id: $NOMA_PROJECT_ID" \
-H "Authorization: Bearer $NOMA_API_KEY" \
-H "Accept: application/json"
curl -sS -X POST "https://app.nomacms.com/api/blog-posts/${ENTRY_UUID}/versions/3/revert" \
-H "project-id: $NOMA_PROJECT_ID" \
-H "Authorization: Bearer $NOMA_API_KEY" \
-H "Accept: application/json"
curl -sS -X PATCH "https://app.nomacms.com/api/blog-posts/${ENTRY_UUID}/versions/3" \
-H "project-id: $NOMA_PROJECT_ID" \
-H "Authorization: Bearer $NOMA_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"label":"Launch copy","description":"Approved by marketing."}'Related
- Get an Entry -
state=publishedreturns the latest snapshot - List Entries -
state=publishedfilters out never-published entries - JavaScript SDK - Content -
client.content.versions.* - MCP Server - Available Tools -
list_entry_versions,get_entry_version,revert_entry_version,update_entry_version_label