Publishing stories
Review a draft, publish now or schedule its release, and understand what unpublishing changes.
Creating a story and making it available to readers are separate operations. Prepare and review the draft first, then choose whether to publish immediately or release it later. Publishing a story does not send a campaign or a pitch.
The examples below change real publication state when run against Prezly. Choose the operation you intend to perform; publishing, scheduling, and unpublishing are alternatives, not a script to run from top to bottom.
Prepare access and check the destination
Load PREZLY_API_TOKEN using Your first API request. A personal token needs stories:publish for publish, schedule, and unpublish, plus stories:read for inspection/search. Reading the newsroom, its configured locales, and categories needs sites:read. stories:write alone is not a publishing permission. Your user also needs permission to manage the story and access to its newsroom.
Use Newsrooms, languages, and categories to confirm the newsroom UUID, public availability, and configured locale. Then create or update the draft. Check the title, content, media, categories, language, and intended audience before changing lifecycle state. The API rejects publication of a story with no title; that validation is not an editorial review.
Find the draft and read it again
Use List Stories for URL-based filters, or Search Stories when it is more convenient to send JSON in the request body. Both are reads, even though Search Stories uses POST.
Save this as story-search.json, replacing YOUR_NEWSROOM_UUID:
{
"query": {
"newsroom.uuid": { "$in": ["YOUR_NEWSROOM_UUID"] },
"status": { "$in": ["draft"] }
},
"sort": "-updated_at",
"limit": 5,
"offset": 0
}curl --fail-with-body --silent --show-error --request POST 'https://api.prezly.com/v2/stories/search' \
--header "Authorization: Bearer $PREZLY_API_TOKEN" \
--header 'Content-Type: application/json' \
--header 'Accept: application/json; content-format=v6' \
--data-binary @story-search.jsonRead the stories and pagination response fields. The first page is not necessarily the full selection; follow Pagination and sorting if you need more results. Query filters narrow your authorized records and do not grant access. See Filtering results for current field/operator names.
Select the exact returned title and UUID. Immediately before acting, retrieve that story rather than relying on an earlier search result. Replace YOUR_STORY_UUID:
curl --fail-with-body --silent --show-error 'https://api.prezly.com/v2/stories/YOUR_STORY_UUID' \
--header "Authorization: Bearer $PREZLY_API_TOKEN" \
--header 'Accept: application/json; content-format=v6'Check story.newsroom.uuid, story.culture.code, story.status, and story.visibility, together with the content. These REST lifecycle endpoints do not accept the MCP tools' expected_* inputs or provide the content-update version check. A fresh read helps you catch an unexpected target or state; it is not an atomic compare-and-set. Coordinate concurrent editors and publishing jobs in your integration.
Understand status and visibility
status describes the story's lifecycle. visibility describes its audience. A public draft is still a draft; a published private story is still unlisted.
| Choice | Effect |
|---|---|
| Draft | Content is prepared for review rather than published on the newsroom. |
Publish with public visibility | Make the story available for public presentation now, subject to the newsroom being online. |
Publish with private visibility | Publish an unlisted story that uses a secret link. |
Publish with confidential visibility | Use confidential visibility, which also restricts access within Prezly; review the account's permissions. |
Schedule with public visibility | Release publicly at a future time; before release the status is scheduled. |
Schedule with embargo visibility | Allow advance access through the embargo link, then release publicly; the status is embargo until Prezly processes the release. |
Treat private, confidential, preview, and embargo URLs as sensitive links. For a custom public website, filter for both status: published and visibility: public, rather than displaying every story your server credential can read.
Publish immediately
This request deliberately makes the selected story public:
curl --fail-with-body --silent --show-error --request POST 'https://api.prezly.com/v2/stories/YOUR_STORY_UUID/publish' \
--header "Authorization: Bearer $PREZLY_API_TOKEN" \
--header 'Content-Type: application/json' \
--header 'Accept: application/json; content-format=v6' \
--data '{"visibility":"public"}'A successful request returns 200 OK with story. Set visibility explicitly: omitting it on Publish does not convert a private draft into a public story.
Publish accepts optional published_at, a present or past ISO 8601 timestamp. When omitted, it uses the current time rounded down to the minute. This field sets the publication date; it does not schedule a future release. A future value is rejected, and embargo is not a Publish visibility option. Use Schedule instead.
Schedule a release instead
Schedule uses publish_at, not published_at or scheduled_at. It must be a future ISO 8601 timestamp with an explicit timezone. visibility can be public or embargo; when omitted, Schedule defaults to public, even if the draft was private. Private/confidential publication uses Publish rather than Schedule.
This Node.js 22+ example checks that the selected story is still a draft in the expected newsroom, then writes a request file for a public release approximately 24 hours from now. It does not send the scheduling request. Replace both UUIDs, save it as prepare-schedule.mjs, and run node prepare-schedule.mjs:
import { writeFile } from 'node:fs/promises';
const token = process.env.PREZLY_API_TOKEN;
if (!token) throw new Error('Set PREZLY_API_TOKEN before running this script.');
const storyId = 'YOUR_STORY_UUID';
const newsroomId = 'YOUR_NEWSROOM_UUID';
const response = await fetch(`https://api.prezly.com/v2/stories/${encodeURIComponent(storyId)}`, {
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json; content-format=v6' },
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) throw new Error(`Prezly returned HTTP ${response.status}.`);
const { story } = await response.json();
if (story.uuid !== storyId || story.newsroom.uuid !== newsroomId || story.status !== 'draft') {
throw new Error('Expected the selected draft in the selected newsroom. Read its current state.');
}
const release = new Date(Date.now() + 24 * 60 * 60 * 1000);
release.setUTCSeconds(0, 0);
const payload = { publish_at: release.toISOString(), visibility: 'public' };
await writeFile('schedule-story.json', JSON.stringify(payload, null, 2));
console.log(JSON.stringify({ uuid: story.uuid, title: story.title, ...payload }, null, 2));Review the printed title, UUID, visibility, and release time. Replace the generated time with your intended release before sending, keeping an explicit Z or numeric offset and a future minute. A local wall-clock time without an offset is ambiguous; use the newsroom's timezone for display and convert to an explicit instant for the request. The server drops seconds, so do not choose a time so close that rounding puts it in the past.
Then send the reviewed request:
curl --fail-with-body --silent --show-error --request POST 'https://api.prezly.com/v2/stories/YOUR_STORY_UUID/schedule' \
--header "Authorization: Bearer $PREZLY_API_TOKEN" \
--header 'Content-Type: application/json' \
--header 'Accept: application/json; content-format=v6' \
--data-binary @schedule-story.jsonA successful schedule returns 200 OK with story. Use story.status and story.scheduled_at to inspect the plan. To use an embargo, deliberately change the reviewed file's visibility to embargo; readers with its link can access the story before public release. Re-read the actual state after release time rather than assuming the embargo-to-public transition has already been processed.
The scheduling endpoint can also reschedule a story. Moving its release later can conflict with linked campaigns scheduled to send before the new release. Inspect those campaigns and resolve their timing first; do not blindly retry a conflict. The preparation example above intentionally accepts only a draft.
Unpublish or cancel a scheduled release
Use Unpublish to return a published, scheduled, or embargoed story to draft. There is no separate REST unschedule endpoint in this reference. This example also chooses private visibility for the resulting draft:
curl --fail-with-body --silent --show-error --request POST 'https://api.prezly.com/v2/stories/YOUR_STORY_UUID/unpublish' \
--header "Authorization: Bearer $PREZLY_API_TOKEN" \
--header 'Content-Type: application/json' \
--header 'Accept: application/json; content-format=v6' \
--data '{"visibility":"private"}'Review the consequences before acting: unpublishing a finalized story resets its linked unsent campaigns to draft and clears their scheduled send times. It also regenerates preview and private/confidential link tokens, so old links can stop working. Content is retained; sent email cannot be recalled by changing story state.
Re-read the returned story and refresh any saved links. If your own website caches story content or pages, remove/revalidate that cached presentation too; changing Prezly state cannot remove a copy your integration continues to serve.
Verify the result and recover deliberately
After a successful change, inspect story.status, visibility, published_at, scheduled_at, and links.newsroom_view. Confirm the intended page and timing separately from the mutation response: API publication alone does not take an offline newsroom online, deploy a custom website, or invalidate your application's cache.
- 401/403: check the token's publishing scope, user permissions, and newsroom access. Changing scopes cannot grant access the owner lacks.
- 409: inspect the reported conflict, such as a missing title or incompatible campaign timing, then resolve the cause.
- 422: check the field name, ISO timestamp, allowed visibility, and other validation details. A future release belongs in
publish_aton Schedule. - Timeout, lost response, or unexpected server error: get the story and reconcile its current state before repeating a write. Reissuing Publish without
published_atcan change the publication date; do not assume repeating it is harmless.
See Handling errors. Never build a retry loop that repeatedly publishes, schedules, or unpublishes to discover whether the first call worked.