Creating and updating stories
Create a private draft with structured content, then replace its content with a version-checked update.
Use POST /stories to create a story and PATCH /stories/{story_id} to change one you already have. This walkthrough creates a private v6 draft, keeps its UUID, and updates its headline without replacing unrelated content.
Prepare your access and destination
Load a credential into PREZLY_API_TOKEN using the hidden prompt in Your first API request. The read-only token from that walkthrough is not enough: a personal token needs stories:write for create/update and stories:read for the read before updating. Listing newsrooms also needs sites:read.
Use Newsrooms, languages, and categories to choose a newsroom you can manage and a language configured for it. Copy its UUID from List Newsrooms. Set the destination explicitly; omitting it lets the API select a default newsroom, which is a poor assumption for an import or multi-site integration.
1. Build the create request
For v6, the headline and subtitle belong inside the content document. The outer content field must be a JSON string, not a nested object. Use JSON.stringify instead of hand-escaping an entire article in a shell command.
Replace YOUR_NEWSROOM_UUID. Save this Node.js 22+ script as prepare-story.mjs and run node prepare-story.mjs:
import { writeFile } from 'node:fs/promises';
const document = {
type: 'document',
version: '2.6',
children: [
{ type: 'heading-one', role: 'title', children: [{ text: 'Acme opens its new studio' }] },
{ type: 'heading-two', role: 'subtitle', children: [{ text: 'A new home for the team' }] },
{ type: 'paragraph', children: [{ text: 'The studio opens next month.' }] },
],
};
const payload = {
newsroom: 'YOUR_NEWSROOM_UUID',
culture: 'en',
format_version: 6,
visibility: 'private',
content: JSON.stringify(document),
};
await writeFile('create-story.json', JSON.stringify(payload, null, 2));format_version: 6 describes the content you send. The Accept header in the request below chooses the format you receive back. They serve different purposes.
Newsroom references accept a UUID string or numeric ID. Use a UUID for new integrations. culture is a configured locale code, such as en or nl_NL; it is not arbitrary text for the language's display name.
2. Create the draft and keep its UUID
curl --fail-with-body --silent --show-error --request POST 'https://api.prezly.com/v2/stories' \
--header "Authorization: Bearer $PREZLY_API_TOKEN" \
--header 'Content-Type: application/json' \
--header 'Accept: application/json; content-format=v6' \
--data-binary @create-story.jsonA successful create returns 201 Created and the resource under story. Copy story.uuid for the next steps. Content created this way starts as a draft; the example also sets private visibility. Creating a draft does not publish it.
Record that UUID against the record in your source system. Repeating a create request can create another story. If the response is lost, inspect the account before repeating the write; a timeout does not establish that nothing was saved.
3. Read before replacing content
A content update replaces the whole document; it is not a patch to one paragraph. Read the current document, change the intended node, and send the complete result. Do not replace an existing article with only the new headline.
For the draft you just created, replace YOUR_STORY_UUID, save this as prepare-update.mjs, and run node prepare-update.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 response = await fetch('https://api.prezly.com/v2/stories/YOUR_STORY_UUID?include=content_version', {
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 (!Number.isInteger(story.content_version)) throw new Error('Expected content_version.');
const document = JSON.parse(story.content);
const title = document.children.find((node) => node.role === 'title');
if (!title) throw new Error('Expected a title node.');
title.children = [{ text: 'Acme studio opens on 1 November' }];
await writeFile('update-story.json', JSON.stringify({
format_version: 6,
content: JSON.stringify(document),
content_version: story.content_version + 1,
}, null, 2));Request include=content_version explicitly: it is not part of the default full-story response. The update sends the next content version, the value you read plus one. This is separate from both the story's format version and the document's 2.6 schema version.
4. Apply the update
Replace YOUR_STORY_UUID with the same UUID:
curl --fail-with-body --silent --show-error --request PATCH 'https://api.prezly.com/v2/stories/YOUR_STORY_UUID' \
--header "Authorization: Bearer $PREZLY_API_TOKEN" \
--header 'Content-Type: application/json' \
--header 'Accept: application/json; content-format=v6' \
--data-binary @update-story.jsonA successful update returns 200 OK and story. Unsent fields remain unchanged. If another edit moved the content version forward, the API returns 409 with content_version_conflict. Read the latest document and reconcile the changes; do not simply increment the number and resend your stale copy. Omitting content_version omits this protection.
This example updates the draft created above. Updating a published story changes that existing story; it does not create a separate review draft. For a legacy story, explicitly changing format_version can also change its stored format. Choose that conversion deliberately.
Keep metadata and publication separate
Send only fields you intend to change. For arrays such as categories or tags, send the intended resulting list rather than assuming the API appends your values. Categories must belong to the selected newsroom. Do not use an update to move a story to another newsroom.
For v4+, standalone title, subtitle, intro, and header_image inputs are rejected: title, subtitle, and header media belong in the document. Preview and social images are separate fields; consult their request schemas and Images and files.
A publication date is not a substitute for a publishing action. Publishing and scheduling use their own endpoints and, for a personal token, stories:publish. Follow Publishing stories when you are ready to publish, schedule, or unpublish.
If a request is rejected
401/403: check the credential, write scope, endpoint support, and newsroom permissions.422: check the serialized content, configured culture, field types, and node structure. Every element needs children; media nodes reference uploaded file objects rather than remote URLs.409: inspect the code. A content version conflict needs reconciliation; a custom-slug conflict needs a different slug or an explicit decision about the existing story.
The Create/Update reference covers several stored formats. Choose format_version: 6 explicitly and use serialized document version 2.6 content as shown here. Keep the content-format guide alongside the request schema when preparing documents.
For existing article HTML, use Importing HTML. For language versions, see Story translations.