curl --request PUT \
--url https://studio.weaverse.io/api/v1/content/projects/{projectId}/page-drafts/{pageId} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"items": [
{
"id": "root-1",
"type": "main",
"children": [
{
"id": "hero-1"
}
],
"data": {}
},
{
"id": "hero-1",
"type": "hero-image",
"data": {
"heading": "Summer collection",
"height": "large"
}
}
],
"expectedHeadVersionId": null,
"expectedPageUpdatedAt": "2026-10-08T10:00:00.000Z",
"expectedProjectUpdatedAt": "2026-10-08T09:00:00.000Z"
}
'const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
items: [
{id: 'root-1', type: 'main', children: [{id: 'hero-1'}], data: {}},
{
id: 'hero-1',
type: 'hero-image',
data: {heading: 'Summer collection', height: 'large'}
}
],
expectedHeadVersionId: null,
expectedPageUpdatedAt: '2026-10-08T10:00:00.000Z',
expectedProjectUpdatedAt: '2026-10-08T09:00:00.000Z'
})
};
fetch('https://studio.weaverse.io/api/v1/content/projects/{projectId}/page-drafts/{pageId}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://studio.weaverse.io/api/v1/content/projects/{projectId}/page-drafts/{pageId}"
payload = {
"items": [
{
"id": "root-1",
"type": "main",
"children": [{ "id": "hero-1" }],
"data": {}
},
{
"id": "hero-1",
"type": "hero-image",
"data": {
"heading": "Summer collection",
"height": "large"
}
}
],
"expectedHeadVersionId": None,
"expectedPageUpdatedAt": "2026-10-08T10:00:00.000Z",
"expectedProjectUpdatedAt": "2026-10-08T09:00:00.000Z"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text){
"object": "page_draft_saved",
"pageId": "clz7pg9k2m4n6p8r0t2v4x6z",
"projectId": "clz7prj0a1b2c3d4e5f6g7h8",
"versionId": "clz9drf2h4j6l8n0p2r4t6v8",
"updatedAt": "2026-10-08T10:12:00.000Z"
}{
"object": "error",
"code": "UNAUTHORIZED",
"message": "<string>",
"status": 123,
"details": "<unknown>"
}{
"object": "error",
"code": "UNAUTHORIZED",
"message": "<string>",
"status": 123,
"details": "<unknown>"
}{
"object": "error",
"code": "UNAUTHORIZED",
"message": "<string>",
"status": 123,
"details": "<unknown>"
}{
"object": "error",
"code": "STALE_PAGE",
"message": "The live page or project changed since it was read; nothing was saved — re-read and retry",
"status": 409
}Save a page draft
Saves a new draft head for the page from a full item list. Live content is never changed and no cache is invalidated. Publishing is a separate call that needs content:publish.
Workflow. Drafts and publishing work together like this:
GET /projects/{projectId}/page-drafts/{pageId}reads the editable content and its revision tokens.PUTon the same path saves a new draft head (versionId). Live content is not touched.- Publish the draft with
POST /publish-versions/{versionId}, or schedule it withPOST /versions/{versionId}/schedule. - Manage history and the schedule with
GET /projects/{projectId}/versions,/versions/{versionId}(read, reschedule, replace content, cancel),publish-nowandrestore.
Body
items: the complete item graph, 1 to 200 items. Each item needsidandtype;childrenare{ "id": ... }references anddatais an object. Every child must reference an item in the list, ids must be unique, and every item must be reachable from the root. A malformed body is 400; a broken graph (duplicate ids, dangling child, orphaned items) is 404PAGE_NOT_FOUND. Nothing is saved in either case.- The root is the item whose id matches the page’s current
rootId; otherwise the first item. name(optional): renames the draft. Defaults to the live page name.
Revision fence. All three tokens are required and must be copied exactly from the GET response:
expectedHeadVersionId: GET’sheadVersionId(nullmeans “no draft yet”).expectedPageUpdatedAt: GET’spageUpdatedAt, the exact ISO string ornull.expectedProjectUpdatedAt: GET’sprojectUpdatedAt, the exact ISO string ornull.
null is a real baseline for a legacy row and only matches a row that is still null. If the draft head, the live page or the project moved since your GET, the save is refused with 409 STALE_PAGE and nothing is written. Re-read with GET, reapply your edit and save again.
Retries. Retrying a save that already committed returns the same head instead of a conflict.
Response. versionId is the new draft head. Pass it to POST /publish-versions/{versionId} or POST /versions/{versionId}/schedule, and send it as expectedHeadVersionId on your next save or discard.
Required scope: content:write
curl --request PUT \
--url https://studio.weaverse.io/api/v1/content/projects/{projectId}/page-drafts/{pageId} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"items": [
{
"id": "root-1",
"type": "main",
"children": [
{
"id": "hero-1"
}
],
"data": {}
},
{
"id": "hero-1",
"type": "hero-image",
"data": {
"heading": "Summer collection",
"height": "large"
}
}
],
"expectedHeadVersionId": null,
"expectedPageUpdatedAt": "2026-10-08T10:00:00.000Z",
"expectedProjectUpdatedAt": "2026-10-08T09:00:00.000Z"
}
'const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
items: [
{id: 'root-1', type: 'main', children: [{id: 'hero-1'}], data: {}},
{
id: 'hero-1',
type: 'hero-image',
data: {heading: 'Summer collection', height: 'large'}
}
],
expectedHeadVersionId: null,
expectedPageUpdatedAt: '2026-10-08T10:00:00.000Z',
expectedProjectUpdatedAt: '2026-10-08T09:00:00.000Z'
})
};
fetch('https://studio.weaverse.io/api/v1/content/projects/{projectId}/page-drafts/{pageId}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://studio.weaverse.io/api/v1/content/projects/{projectId}/page-drafts/{pageId}"
payload = {
"items": [
{
"id": "root-1",
"type": "main",
"children": [{ "id": "hero-1" }],
"data": {}
},
{
"id": "hero-1",
"type": "hero-image",
"data": {
"heading": "Summer collection",
"height": "large"
}
}
],
"expectedHeadVersionId": None,
"expectedPageUpdatedAt": "2026-10-08T10:00:00.000Z",
"expectedProjectUpdatedAt": "2026-10-08T09:00:00.000Z"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text){
"object": "page_draft_saved",
"pageId": "clz7pg9k2m4n6p8r0t2v4x6z",
"projectId": "clz7prj0a1b2c3d4e5f6g7h8",
"versionId": "clz9drf2h4j6l8n0p2r4t6v8",
"updatedAt": "2026-10-08T10:12:00.000Z"
}{
"object": "error",
"code": "UNAUTHORIZED",
"message": "<string>",
"status": 123,
"details": "<unknown>"
}{
"object": "error",
"code": "UNAUTHORIZED",
"message": "<string>",
"status": 123,
"details": "<unknown>"
}{
"object": "error",
"code": "UNAUTHORIZED",
"message": "<string>",
"status": 123,
"details": "<unknown>"
}{
"object": "error",
"code": "STALE_PAGE",
"message": "The live page or project changed since it was read; nothing was saved — re-read and retry",
"status": 409
}Authorizations
Bearer authentication: a shop API key (WEAVERSE_API_KEY), a scoped API key, a delegated MCP OAuth token, or (translation-unit routes only) a project-bound translation agent token. Scopes: content:read — Read projects, pages, theme settings, versions, drafts and translation units. content:write — Save drafts and make non-destructive edits (page items, metadata, SEO, assignments, languages, glossary). Never publishes or deletes. content:publish — Publish drafts, schedule/reschedule/cancel publications, restore versions, publish global sections. content:delete — Delete pages, global sections, languages, static keys, override reverts; required with project:manage for project deletion. project:manage — Create/copy/delete projects, settings, variants, hierarchy, backups, restore-as-new, previews, hostname policy. translations:read — Read translation units only (no general content access). translations:write — Apply translation units only; never mutates page content. A legacy unscoped API key keeps read/write/translation baseline access and every pre-scope endpoint, but never gains publish/delete/project-manage implicitly.
Body
200Show child attributes
Show child attributes
The head this edit started from; null means "no draft yet"
GET pageUpdatedAt exactly as GET returned it (null = a legacy row with no revision). The save is refused with 409 STALE_PAGE when it no longer matches live.
GET projectUpdatedAt exactly as GET returned it (null = a legacy row with no revision). The save is refused with 409 STALE_PAGE when it no longer matches live.
Response
New head version id
The response is of type object.
Was this page helpful?