Skip to content

Content lifecycle

Included free on every install, with every revision kept and the last 30 days of them readable. Older revisions, content releases and editorial comment threads need a license with the content-pro feature. See pricing.

This is the Admin API behind the admin console's Content screens. It creates and edits entries, moves them between draft, published and archived, and keeps a numbered revision of every write so you can compare two and roll back. An entry can also publish and archive itself at times you set, and link to other entries.

The console side of this, the buttons and the revision panel, is in Drafts and publishing.

An entry on this API has a few fields of its own beside the content type's field values:

FieldWhat it holds
schemaThe content type the entry belongs to.
title, slugRequired. A slug is unique across your tenant, not only within one content type.
bodyThe content type's field values. They are checked against its rules on every write.
metaAny JSON you want to keep beside the entry.
statusdraft, published or archived. A new entry is a draft unless you say otherwise.
current_revThe number of the newest revision.
published_atWhen the entry last moved to published.
scheduled_publish_at, scheduled_unpublish_at, timezoneIts schedule, when one is set.

What statuses and revisions mean is explained on the data model page. Every route takes the admin or super_admin role. An admin token can call the read routes with the content:read grant and the write routes with content:write.

This creates a draft, publishes it, rolls it back and schedules it. You need a token in TOKEN. The quickstart shows how to get one.

  1. Create a post content type with draft and publish:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/schemas \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"name": "post", "with_draft_publish": true, "fields": [{"name": "title", "field_type": "text", "required": true}, {"name": "body", "field_type": "rich_text"}]}'
  2. Create an entry. The content type requires title, so it goes in body as well as beside it:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/content \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"schema": "post", "slug": "hello", "title": "Hello", "body": {"title": "Hello", "body": "<p>First post.</p>"}}'
    {
    "id": "ad39fb3d-eb5a-47ce-9cd5-e7d421acaa98",
    "schema": "post",
    "tenant_id": "default",
    "slug": "hello",
    "title": "Hello",
    "body": { "title": "Hello", "body": "<p>First post.</p>" },
    "meta": {},
    "status": "draft",
    "timezone": "UTC",
    "created_by": "12dd433d-c110-4809-b476-12ef360afe7f",
    "updated_by": "12dd433d-c110-4809-b476-12ef360afe7f",
    "current_rev": 1,
    "created_at": "2026-10-02T05:06:17.627726Z",
    "updated_at": "2026-10-02T05:06:17.627726Z"
    }

    Copy its id into ENTRY_ID.

  3. Publish it, with a note for the history:

    Terminal window
    curl -X PUT http://localhost:3001/api/admin/content/$ENTRY_ID \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"status": "published", "change_note": "First release"}'

    The answer is 200 with "status": "published", a published_at time and "current_rev": 2.

  4. List the revisions, newest first:

    Terminal window
    curl http://localhost:3001/api/admin/content/$ENTRY_ID/revisions \
    -H "Authorization: Bearer $TOKEN"

    You see revision 2 with the note First release and revision 1 with the note initial revision.

  5. Roll back to revision 1:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/content/$ENTRY_ID/rollback \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"to_revision": 1}'
    {
    "message": "rolled back to revision 1",
    "new_revision": {
    "id": "fc1659a6-afa4-48e5-9089-b786fbb45d94",
    "entry_id": "ad39fb3d-eb5a-47ce-9cd5-e7d421acaa98",
    "tenant_id": "default",
    "revision_num": 3,
    "title": "Hello",
    "body": { "title": "Hello", "body": "<p>First post.</p>" },
    "meta": {},
    "status": "draft",
    "change_note": "rolled back to revision 1",
    "created_by": "12dd433d-c110-4809-b476-12ef360afe7f",
    "created_at": "2026-10-02T05:06:34.085365Z"
    }
    }
  6. Schedule it to publish and then archive:

    Terminal window
    curl -X PUT http://localhost:3001/api/admin/content/$ENTRY_ID/entry-schedule \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"scheduled_publish_at": "2027-11-01T09:00:00+01:00", "scheduled_unpublish_at": "2027-12-01T09:00:00+01:00", "timezone": "Europe/Paris"}'

    The answer is the entry with scheduled_publish_at and scheduled_unpublish_at stored in UTC. Open Content, then Post, in the admin console to see the entry.

GET /api/admin/content lists entries, newest change first, 50 at a time by default and at most 500. Narrow it with ?schema=post and ?status=published, and page with limit and offset. This route filters by content type and status only: filters[...] answers 400 and points you to the Content API, which filters by field.

Read one entry by id with GET /api/admin/content/{id}, or by slug with GET /api/admin/content/slug/{slug}. With localization, each read takes ?locale=fr and answers the translated fields.

PUT /api/admin/content/{id} changes only the top-level fields you send, so {"status": "published"} leaves the title and body as they are. A body you send replaces the whole body, so send every field value, not only the one that changed. Every write saves a new revision. Send change_note to describe it. The note is updated when you leave it out.

Publishing, unpublishing and archiving are changes of status on the same route:

ToSend
Publish{"status": "published"}
Take back to draft{"status": "draft"}
Archive{"status": "archived"}

You can also create an entry as published by sending "status": "published" on the create. DELETE /api/admin/content/{id} removes the entry and all its revisions, and answers 204.

MethodPathResult
GET/api/admin/content/{id}/revisionsThe revisions you can read, newest first, with limit and offset.
GET/api/admin/content/{id}/revisions/{number}One revision: its title, body, meta, status and change note.
GET/api/admin/content/{id}/diff?from=1&to=3The fields that changed between two revisions, with their old and new values, as text in diff.
POST/api/admin/content/{id}/rollback{"to_revision": 2}. The entry takes that revision's title, body, meta and status, saved as a new revision.

Every revision of every entry is kept. Without content-pro, you list, read, compare and roll back to the revisions of the last 30 days, plus the newest revision of each entry whatever its age, so a page nobody has touched in a year still shows its current state. The list says what it left out:

{"data": [...], "total_count": 4, "limit": 50, "offset": 0, "hidden_older": 12, "window_days": 30}

hidden_older counts the older revisions, and total_count counts only the ones you can page through. Reading, comparing or rolling back to an older revision answers 402 naming feature:content-pro and writes nothing. The Content API's revision list and restore follow the same window.

With content-pro, hidden_older is 0, window_days is null, and every revision reads at once, however old. A license that lapses deletes nothing, and the older history comes back with it.

PUT /api/admin/content/{id}/entry-schedule sets scheduled_publish_at, scheduled_unpublish_at, or both, with an optional timezone:

  • Times are RFC 3339 and must be in the future. timezone is an IANA name such as Europe/Paris, kept with the schedule for display.
  • At scheduled_publish_at a draft becomes published. At scheduled_unpublish_at a published entry becomes archived.
  • Due changes are applied every 30 seconds, so an entry can change up to 30 seconds after its time.

DELETE on the same path clears the schedule. GET /api/admin/content/{id}/preview?at=2027-11-02T00:00:00Z answers the status the entry will have at that moment, as effective_status, with will_publish and will_unpublish. POST /api/admin/content/{id}/schedule-conflicts checks proposed times against other entries and answers {"has_conflicts": true, "conflicts": [...]}. GET /api/admin/content/scheduled lists every entry with a schedule.

To queue one change on its own, POST /api/admin/content/{id}/schedule takes {"target_status": "published", "publish_at": "2027-11-01T08:00:00Z"}, where target_status is published or archived. GET /api/admin/content/{id}/schedules lists them, and DELETE /api/admin/content/{id}/schedules/{schedule_id} cancels one.

A relationship links two entries directly, without a relation field in the content type. Use it for links an editor decides case by case, such as "see also". For links every entry of a type has, such as a post's author, use a relation field.

Terminal window
curl -X POST http://localhost:3001/api/admin/content/$ENTRY_ID/relationships \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"target_id": "<another entry id>", "field_name": "see_also", "rel_type": "many_to_many", "cascade_policy": "none"}'
FieldValues
target_idThe linked entry. Required.
field_nameThe name of the link. Required.
rel_typeone_to_one, one_to_many or many_to_many.
bidirectionaltrue to make the link visible from both ends.
cascade_policynone, or cascade to delete the target when the source is deleted.
sort_orderPosition among links with the same name.

GET /api/admin/content/{id}/relationships lists the links from an entry, filtered with ?field=. .../relationships/reverse lists the links to it, and .../graph?depth=3 answers the linked entries as nodes and edges, up to depth steps.

GET /api/admin/content/{id}/audit lists every change to the entry, newest first. Each item names the action (created, updated, published, unpublished, archived, rollback, scheduled_publish or scheduled_unpublish), the actor_id, the revision_num and, for edits, a diff. The instance-wide record is the audit log.

With a license that carries content-pro, a release groups up to 500 entries that publish or unpublish together, now or at a set time, and puts every entry back if any fails. A release scheduled while the license covered it still goes out after a lapse. See Content releases.

With content-pro, editors can also keep comment threads on an entry, reply, resolve and reopen them, and mention colleagues by email. Reading, resolving and reopening stay free after a lapse. See Editorial comments.

Content lifecycle has no settings of its own. It runs on every install. If you set LYEVE_PLUGINS to choose which features start, include content in it. See licensing and tiers.

Entries, revisions, schedules, relationships and history
MethodPathPurposeGrant
GET/api/admin/contentList entries. ?schema=, ?status=, limit, offset.content:read
POST/api/admin/contentCreate an entry.content:write
GET/api/admin/content/{id}One entry.content:read
GET/api/admin/content/slug/{slug}One entry by slug.content:read
PUT/api/admin/content/{id}Change an entry and save a revision.content:write
DELETE/api/admin/content/{id}Delete an entry and its revisions.content:write
GET/api/admin/content/{id}/revisionsRevisions, newest first.content:read
GET/api/admin/content/{id}/revisions/{number}One revision.content:read
GET/api/admin/content/{id}/diffCompare two revisions: from, to.content:read
POST/api/admin/content/{id}/rollbackRestore a revision: to_revision.content:write
PUT, DELETE/api/admin/content/{id}/entry-scheduleSet or clear the publish and archive times.content:write
GET/api/admin/content/{id}/previewThe status at a moment: at.content:read
POST/api/admin/content/{id}/schedule-conflictsCheck proposed times against other entries.content:read
GET/api/admin/content/scheduledEntries with a schedule.content:read
POST/api/admin/content/{id}/scheduleQueue one change of status.content:write
GET/api/admin/content/{id}/schedulesQueued changes.content:read
DELETE/api/admin/content/{id}/schedules/{schedule_id}Cancel a queued change.content:write
GET/api/admin/content/{id}/auditEvery change to the entry.content:read
GET, POST/api/admin/content/{id}/relationshipsList or add links from the entry.read or write
GET/api/admin/content/{id}/relationships/reverseLinks to the entry.content:read
GET, PUT, DELETE/api/admin/content/{id}/relationships/{rel_id}Read, change or remove one link. PUT changes cascade_policy and sort_order.read or write
GET/api/admin/content/{id}/graphLinked entries as a graph: depth.content:read

The ones you meet most:

  • 400 slug is required or title is required on a create.
  • 409 when the slug is taken. The message names the content type that holds it, such as slug is already taken by content type "page"; a slug is unique per tenant, not per content type.
  • 422 with an errors list when body breaks the content type's rules, such as a required field left out.
Every error these routes return
StatusMessage
400invalid JSON body
400slug is required, title is required, slug must not contain control characters, slug must not contain a percent sign
400field filters are not supported on this route; ...
400from and to query params required (positive integers)
400invalid revision number, to_revision must be positive
402payment_required, naming feature:content-pro, for a revision older than 30 days without content-pro
400target_status is required, publish_at is required, publish_at must be in the future, target_status must be published or archived
400at least one of scheduled_publish_at or scheduled_unpublish_at is required
400scheduled_publish_at must be in the future, scheduled_unpublish_at must be in the future
400invalid timezone: <name>
400`at` query parameter is required (ISO 8601 timestamp)
400target_id is required, field_name is required, invalid target_id
404content not found, revision not found, relationship not found, schedule not found or already fired
409slug is already taken by another entry of this content type, or by another content type as above
422An errors list naming each field that breaks the content type's rules
422scheduled_publish_at or scheduled_unpublish_at is required on schedule-conflicts

The release and comment errors are on Content releases and Editorial comments.