Skip to content

Content releases

Requires a license with the content-pro feature. See pricing.

A release is a named set of entries that change together. Use one for a launch: the new product pages go live and the old ones go back to draft in one action. Mark each entry publish or unpublish, then publish the release now or schedule it for a set time. If any entry fails, every entry is put back where it was.

A release changes its entries in two stages:

StageWhat happens
In the adminEvery entry changes status in one transaction. Either all of them show the new status or none does.
In what the Content API and GraphQL serveThe entries follow one at a time, usually within seconds. During that window a reader can see some entries of the release changed and others not yet.
  • If an entry fails in the second stage, every entry is put back where it was, in the admin and in what the APIs serve. The release is marked failed with the reason, and you can fix it and publish again.
  • An entry someone changed again after the release started is left as they left it.
  • If the instance stops partway, the release finishes going out a few minutes after it starts again.
  • Each entry that changes gets a revision and an audit entry, and sends the same event a single publish sends, so webhooks, flows and search see every entry.

A release moves through these states:

StatusMeaning
draftBeing built. Entries can be added and removed.
scheduledGoes out at scheduled_at.
publishingGoing out now.
publishedEvery entry changed. Each item carries its previous_status and its outcome.
failedNothing changed. The release carries the reason.
canceledCanceled before it went out.

Run this on an install whose license carries content-pro. You need an admin token in TOKEN. The quickstart shows how to get one. Create two entries first, as in Content lifecycle, and copy their ids into PAGE_A and PAGE_B.

  1. Build a release that publishes both:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/content/releases \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"name": "Spring launch", "items": [{"entry_id": "'"$PAGE_A"'", "action": "publish"}, {"entry_id": "'"$PAGE_B"'", "action": "publish"}]}'

    The answer is 201 with "status": "draft" and "item_count": 2. Copy its id into RELEASE_ID.

  2. Publish it now:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/content/releases/$RELEASE_ID/publish \
    -H "Authorization: Bearer $TOKEN"

    The answer is the release with "status": "published", and each item reports "previous_status": "draft" and "outcome": "applied". Both entries now read "status": "published" on GET /api/admin/content/{id}.

  3. Build and publish a second release that takes PAGE_A down:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/content/releases \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"name": "Retire page A", "items": [{"entry_id": "'"$PAGE_A"'", "action": "unpublish"}]}'

    Publish it with .../publish as in step 2. PAGE_A is a draft again and leaves the Content API list.

  4. See a conflict. Schedule a release that holds PAGE_B:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/content/releases/$OTHER_RELEASE_ID/schedule \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"publish_at": "2027-11-01T09:00:00Z"}'

    Then build a third release that also holds PAGE_B and read its conflicts:

    Terminal window
    curl http://localhost:3001/api/admin/content/releases/$THIRD_RELEASE_ID/conflicts \
    -H "Authorization: Bearer $TOKEN"
    {"data": [{"entry_id": "059b65ff-...", "kind": "other_release", "at": "2027-11-01T09:00:00Z", "release_id": "68dee079-...", "detail": "another scheduled release moves the entry"}]}

    Scheduling or publishing the third release answers 409 until you take the entry out of one of them.

  5. In the admin console, open Delivery > Releases to see every release, or use the Release card on an entry's page to add it to one.

name is required, at most 255 characters, and description is optional. items is optional too, and a release holds up to 500 entries. Add an entry later, or change the action of one already in the release:

Terminal window
curl -X POST http://localhost:3001/api/admin/content/releases/$RELEASE_ID/items \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"entry_id": "'"$ANOTHER"'", "action": "publish"}'

DELETE .../items/{entryID} takes an entry out. Entries can change only while the release is a draft or scheduled.

Terminal window
curl -X POST http://localhost:3001/api/admin/content/releases/$RELEASE_ID/schedule \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"publish_at": "2027-11-01T09:00:00Z"}'

publish_at must be in the future, and the release reports it as scheduled_at. DELETE .../schedule returns a scheduled release to draft, and .../cancel cancels it before it goes out.

Before a release is scheduled or published, it is checked against anything else that would move its entries. Each one is a conflict:

kindMeaning
entry_scheduleThe entry has its own publish or unpublish date.
scheduled_transitionA scheduled status change is pending on the entry.
other_releaseAnother scheduled release holds the same entry.
entry_missingThe entry was deleted after it was added. It is skipped and does not block.

Any conflict but entry_missing refuses the request with 409:

{
"error": "the release has conflicts with other schedules",
"conflicts": [
{"entry_id": "...", "kind": "other_release", "at": "2027-11-01T09:00:00Z", "release_id": "...", "detail": "another scheduled release moves the entry"}
]
}

GET .../conflicts lists them at any time, so you can clear them first.

Reading, canceling and deleting releases stay free, so a lapse never locks you out of releases you already have. A release you scheduled while licensed still goes out at its time. Building, changing, scheduling and publishing need content-pro again.

Every route needs the admin or super_admin role. An admin token can call the read routes with the content:read grant and the others with content:write.

Release routes
MethodPathPurposeNeeds content-pro
GET/api/admin/content/releasesList releases, newest first. Filter with ?status=, page with limit and offset.No
POST/api/admin/content/releasesCreate a release. Answers 201.Yes
GET/api/admin/content/releases/{id}A release with its entries.No
PUT/api/admin/content/releases/{id}Change name or description.Yes
DELETE/api/admin/content/releases/{id}Delete a release. Its entries keep their status. Answers 204.No
POST/api/admin/content/releases/{id}/itemsAdd an entry or change its action.Yes
DELETE/api/admin/content/releases/{id}/items/{entryID}Take an entry out.Yes
GET/api/admin/content/releases/{id}/conflictsWhat else would move the entries.No
POST/api/admin/content/releases/{id}/scheduleSchedule it.Yes
DELETE/api/admin/content/releases/{id}/scheduleReturn it to draft.Yes
POST/api/admin/content/releases/{id}/publishPublish it now.Yes
POST/api/admin/content/releases/{id}/cancelCancel it before it goes out.No

The ones you are most likely to meet:

  • 409 with conflicts when another schedule would move an entry. See Conflicts.
  • 400 with the release has no entries when you publish an empty release.
  • 402 when the license does not carry content-pro.
Every error the release routes return
StatusMessageCause
400invalid JSON bodyThe body does not parse.
400unknown release status?status= names no status above.
400publish_at is required, publish_at must be in the futureThe schedule time is missing or has passed.
400the release has no entriesAdd an entry before you publish or schedule.
400a release holds at most 500 entriesSplit it into two releases.
400an entry in the release does not existCheck the entry_id.
402payment_required, naming feature:content-proA write that needs content-pro.
404release not foundNo release with that id in your tenant.
409the release has conflicts with other schedulesSee Conflicts.
409the release has already gone out or was canceledOnly a draft or scheduled release changes.
409the release is being publishedWait for it to finish.
422name is required and at most 255 charactersFix the name.
422each item needs an entry_id and an action of publish or unpublishAn item in a create is incomplete.
422entry_id and an action of publish or unpublish are requiredThe item you add is incomplete.
422an entry appears twice in the releaseList each entry once.
503the release could not be published and every entry was put backEvery entry is back where it was. Read the reason on the release and publish again.

The 402 body in full:

{"error": "payment_required", "plugin": "content", "feature": "feature:content-pro", "upgrade_url": ""}