PrismDraft SDK

articles

The methods of pd.articles. Inputs and outputs are described in Conventions; every method can also fail as described in Errors.

Methods

pd.articles.create(input, init?)

Create an article

POST /api/articles

A DRAFT in the project, empty unless document or contentMarkdown is sent (one of the two, never both: 400). Needs articles.create, and articles.publish too when publishedAt is sent.

Importing Markdown. contentMarkdown is converted on the server. The images it refers to are downloaded (http and https only; relative URLs need imageBaseUrl; at most 50 distinct images, 25 MB each, 15 seconds each; every address and redirect is checked so private networks are refused) and stored as the article's assets, and the blocks point at them. The leading image (first in the document, or right after the level-1 heading) becomes the article's hero asset, stored whole, and is not a block of the body; it stays an inline image when the body shows the same picture again. Everything is one unit: if any image or construct is refused, no article and no asset is left behind, and the INVALID_REQUEST names the URL (details.src) or lists details.issues ([{ construct, line }]). QUOTA_EXCEEDED (429) when the images do not fit the workspace's storage quota. The first version is recorded as an import.

Language versions. translationGroupId makes the new article another language version of an existing one. language (or primaryLanguage) must be one of the project's languages; a group that already has that language answers 409 CONFLICT.

Keeping a legacy date. publishedAt is stored on the DRAFT. Publishing goes POST /submit, POST /approve, POST /publish, and the last call stamps its own time unless it repeats the date in its body. With no publishing integration nothing else is needed: cmsIntegration is null.

Possible duplicates. The response carries possibleDuplicates: articles of the same project and language, in another group, whose title or slug is close. The same matches are in warnings as { code: "POSSIBLE_DUPLICATE", articleId, translationGroupId, similarity, title, slug }. It is advice and never blocks the creation; when the check itself fails, both are empty.

Side effects. Costs no credits. Sends the article.created webhook and writes an articles.created audit entry. Records version 1 of the document. A given ideaId is marked written by the article.

Input

Name In Type Required Description
body body object yes
body.projectId body string (uuid) yes Id of the project to create the article in. The workspace is taken from it; a project you cannot see answers 404.
body.title body string yes The article's title, 1 to 300 characters, printed above its body. The slug is made from it when slug is not given.
body.slug body string no Normalised with slugify. When another article of the project in the same language (or a redirect in that language) already holds it, a short random suffix is added instead of an error: compare the returned article.slug with the one you asked for. Another language's articles do not count.
body.document body object no The article as blocks. Send this or contentMarkdown, never both.
body.document.version body 1 yes Version of the document format. Always 1.
body.document.blocks body (object<heading> | object<paragraph> | object<list> | object<quote> | object<image> | object<code> | object<table> | object<divider>)[] yes The article body, top to bottom. Each block's type says which of heading, paragraph, list, quote, image, code, table and divider it is and which other fields it carries. An empty array is an empty article.
body.document.blocks[]<heading>.id body string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
body.document.blocks[]<heading>.type body "heading" yes Block kind: always heading.
body.document.blocks[]<heading>.level body 1 | 2 | 3 | 4 | 5 | 6 yes Heading rank, 1 (<h1>) to 6 (<h6>). The article's title is its own field and is printed above the body, so generated bodies carry no level-1 heading; one at the very start is part of the lead that stripLead removes.
body.document.blocks[]<heading>.text body string yes The heading's words.
body.document.blocks[]<heading>.format body "markdown" no Absent: the heading is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
body.document.blocks[]<paragraph>.id body string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
body.document.blocks[]<paragraph>.type body "paragraph" yes Block kind: always paragraph.
body.document.blocks[]<paragraph>.text body string yes The paragraph's words.
body.document.blocks[]<paragraph>.links body object[] no Links from words of text to other articles, at most 20. Only the first link to each article in the whole document is kept (the rest are dropped on save). Absent: none. Ignored when format is "markdown": write the links in the text instead.
body.document.blocks[]<paragraph>.links[].text body string yes The words of the block's text that become the link, 1 to 200 characters. Matched literally, first occurrence only; a link whose words no longer occur in the block is dropped on the next save.
body.document.blocks[]<paragraph>.links[].articleId body string (uuid) yes Id of the article the words link to. It is resolved when the article is rendered: the words stay plain text until that article is PUBLISHED in the same project and has an address (its blog page, or the URL its publishing integration reported).
body.document.blocks[]<paragraph>.format body "markdown" no Absent: the paragraph is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
body.document.blocks[]<list>.id body string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
body.document.blocks[]<list>.type body "list" yes Block kind: always list.
body.document.blocks[]<list>.ordered body boolean yes true: a numbered list. false: a bulleted list.
body.document.blocks[]<list>.items body string[] yes The top-level items, as text. A reader that ignores children still sees every top-level item.
body.document.blocks[]<list>.children body (object | null)[] no Optional, one entry per item of items (an entry count that differs from items is refused): the level 2 sub-list under that item, or null for an item with none. Lists nest to four levels.
body.document.blocks[]<list>.children[].ordered body boolean yes true: a numbered sub-list. false: a bulleted one. Independent of the parent list.
body.document.blocks[]<list>.children[].items body string[] yes The sub-list's items, at least one, as text.
body.document.blocks[]<list>.children[].children body (object | null)[] no Optional, one entry per item of this sub-list (an entry count that differs from items is refused): the level 3 sub-list under that item, or null for an item with none. Same shape as body.document.blocks[]<list>.children[]: see above.
body.document.blocks[]<list>.format body "markdown" no Absent: every item of the list, at every level, is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
body.document.blocks[]<quote>.id body string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
body.document.blocks[]<quote>.type body "quote" yes Block kind: always quote.
body.document.blocks[]<quote>.text body string yes The quoted words.
body.document.blocks[]<quote>.attribution body string no Who said it, always plain text. Absent: the quote is unattributed.
body.document.blocks[]<quote>.links body object[] no Links from words of text to other articles, at most 20. Only the first link to each article in the whole document is kept (the rest are dropped on save). Absent: none. Ignored when format is "markdown": write the links in the text instead.
body.document.blocks[]<quote>.links[].text body string yes The words of the block's text that become the link, 1 to 200 characters. Matched literally, first occurrence only; a link whose words no longer occur in the block is dropped on the next save.
body.document.blocks[]<quote>.links[].articleId body string (uuid) yes Id of the article the words link to. It is resolved when the article is rendered: the words stay plain text until that article is PUBLISHED in the same project and has an address (its blog page, or the URL its publishing integration reported).
body.document.blocks[]<quote>.format body "markdown" no Absent: the quoted text is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
body.document.blocks[]<image>.id body string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
body.document.blocks[]<image>.type body "image" yes Block kind: always image.
body.document.blocks[]<image>.assetId body string (uuid) yes Id of the article's asset the block shows (assets[].id of GET /api/articles/{articleId}). The asset's address is resolved when the article is rendered; a block whose asset is missing renders as nothing. The article's hero asset is its cover and cannot be added to the body.
body.document.blocks[]<image>.altText body string yes Alternative text for the image. When empty, the asset's own alternative text is used when the article is rendered.
body.document.blocks[]<image>.caption body string no Plain-text caption printed under the image. Absent: no caption.
body.document.blocks[]<code>.id body string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
body.document.blocks[]<code>.type body "code" yes Block kind: always code.
body.document.blocks[]<code>.language body string no The language of the code (the fence's info string), at most 40 characters, e.g. ts. Renderers keep only the characters A-Z a-z 0-9 _ + # . -. Absent: unspecified.
body.document.blocks[]<code>.text body string yes The code, verbatim: never interpreted as Markdown and never translated.
body.document.blocks[]<table>.id body string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
body.document.blocks[]<table>.type body "table" yes Block kind: always table.
body.document.blocks[]<table>.header body string[] yes The header row: one cell of text per column, at least one.
body.document.blocks[]<table>.rows body string[][] yes The body rows, each an array of cell text with exactly as many cells as header (otherwise refused). May be empty.
body.document.blocks[]<table>.align body ("left" | "center" | "right" | null)[] no One alignment per column, so as many entries as header has cells (otherwise refused); null leaves a column to the browser's default. Absent: all default.
body.document.blocks[]<table>.format body "markdown" no Absent: every cell, header included, is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
body.document.blocks[]<divider>.id body string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
body.document.blocks[]<divider>.type body "divider" yes Block kind: always divider.
body.contentMarkdown body string no Markdown to convert into the article's blocks (up to 200000 characters). Send this or document, never both. Images are downloaded by the server (http and https only, at most 50 distinct ones) and stored as the article's assets. A horizontal rule becomes a divider block and a nested list keeps its sub-lists as the list's children (four levels; deeper ones are merged into the fourth); the 201 lists both in conversions. What the document cannot hold is refused as a whole with details.issues: [{ construct, line }]: HTML, task lists, two sub-lists under one item, footnotes, strikethrough, reference-style links and others. A source the converter cannot read within 5 seconds (or that nests deeper than the stack allows) is refused with INVALID_REQUEST "The Markdown is too complex to import".
body.imageBaseUrl body string (uri) no Resolves relative image URLs in contentMarkdown (/img/a.png, a.png). Without it a relative image URL is refused.
body.metaDescription body string | null no The article's meta description (at most 320 characters, as PATCH allows), stored on the article. Omitted or null: none.
body.generationConfig body object no The settings of an AI generation, stored on the article as given. Creating the article does not start a generation and costs no credits; absent: the article has none (null).
body.generationConfig.topic body string yes What the article is about (1 to 500 characters). The outline step plans the article from it, and with primaryKeyword it is the query for web research and for the project's knowledge base. The article's title is not copied from it: the outline's title, written by the model, becomes the title.
body.generationConfig.prompt body string no Free-form direction for the writer, up to 5000 characters (angle, points to cover, things to avoid). Sent to the outline step as 'Additional direction'; the section writers follow the outline it produces.
body.generationConfig.primaryKeyword body string no The keyword the article targets, up to 200 characters. The outline step is told to use it where it reads naturally, and it joins topic in the research and knowledge-base queries. It is not copied into the article's targetKeywords.
body.generationConfig.secondaryKeywords body string[] no Supporting keywords, up to 25 of 1 to 200 characters. Sent to the outline step, which spreads them over the sections' keywords. Empty by default. Default: [].
body.generationConfig.audience body string no Who the article is for, up to 500 characters. Sent to the outline step, to every section writer, to the image-prompt writers and to the knowledge-base query.
body.generationConfig.tone body string no The tone to write in, as free text up to 200 characters; the web app offers professional, conversational, authoritative, practical and persuasive. Sent to the outline step as 'Tone'. The section writers read no separate tone: they follow the outline and the brand voice.
body.generationConfig.language body string no Language the article is written in, as a language tag such as en or pt-BR (default en). The outline, section, research and image-prompt steps use it, so the text and the images' alt text come out in it. A run does not change the article's own primaryLanguage. Default: "en".
body.generationConfig.wordCount body integer no Target length of the whole article in words, 200 to 10000 (default 1200). The outline's sections are planned to add up to roughly this. It also sets the text price per started 1,000 words: 2 credits with writingQuality premium, 1 with standard (1200 costs 4 or 2), and the image count when imageCount is null. Default: 1200.
body.generationConfig.imageStyle body "photorealistic" | "editorial" | "minimalist-vector" | "3d-render" | "watercolor" | "corporate-tech" | "cinematic" | "illustration" no Style of the article's images (default editorial). It applies to the cover and the inline images unless the project's imageSettings set a hero.style or inline.style, which take precedence. Values: photorealistic (natural-light documentary photograph, realistic textures, no stylisation), editorial (modern editorial illustration or photograph, clean composition, restrained palette), minimalist-vector (flat vector illustration, few shapes, solid colours, plenty of empty space), 3d-render (soft studio 3D render, smooth matte materials, simple forms), watercolor (loose watercolour painting, soft edges, muted washes), corporate-tech (clean technology visual in cool blues and greys, geometric shapes), cinematic (cinematic still, dramatic lighting, strong atmosphere, wide framing), illustration (hand-drawn illustration, confident linework, warm friendly palette). Default: "editorial".
body.generationConfig.imageCount body integer | null no How many images to make, cover included, 0 to 20. 0 makes none, 1 only the cover, more add one inline image per section up to the number of outline sections. null (the default) derives it from wordCount: 2 under 1,000 words, 3 under 2,000, 4 under 3,000, otherwise 5. Each image delivered costs 1 credit, or 3 when imageQuality is hd; the quote and the reservation count the full number. Default: null.
body.generationConfig.writingQuality body "standard" | "premium" no Which model writes the article (default premium). premium uses the deployment's article model and costs 2 credits per started 1,000 words; standard uses a lighter, cheaper model and costs 1 credit per started 1,000 words. Default: "premium".
body.generationConfig.imageQuality body "standard" | "hd" no Quality of the article's images (default standard). standard costs 1 credit per image; hd asks the image model for its highest quality and costs 3 credits per image, for the run's images and for each regeneration of an image of the article. Default: "standard".
body.generationConfig.brandVoiceId body string (uuid) no Id of the brand voice to write in. Absent, or an id that is not one of the workspace's voices, the project's default voice applies, then the workspace's default voice; with neither, no voice is applied.
body.generationConfig.contentTemplateId body string (uuid) no Id of the content template to follow: the outline must contain its sections, and the last section ends on its call to action. Only built-in templates and the workspace's own count; any other id, or a deleted template, is ignored without an error.
body.generationConfig.sourceUrl body string (uri) no A reference URL for the article. It is stored with the configuration; no generation step reads or fetches it.
body.generationConfig.researchEnabled body boolean no Whether to research and fact-check (default false). Research looks up sources before the outline (needs the deployment's web_research feature); fact checking verifies the finished article's claims (needs fact_checking). Research adds 1 credit and fact checking 3 to the quote, each only when its feature is on, and is charged only if it delivered: research found a source, fact checking checked a claim. Default: false.
body.generationConfig.reviewOutline body boolean no When true the run stops after the outline (default false): the job ends COMPLETED, the article stays DRAFT with the outline stored, and only research, if any, is charged. Read it with GET /api/articles/{articleId}/outline, edit it, then POST /api/articles/{articleId}/outline/approve to write the article. Default: false.
body.generationConfig.faq body boolean no When true (default false) the article ends with a "Frequently asked questions" section of 3 to 5 questions a reader still has after the body, each answered in two or three sentences, in the article's language. It is written after the sections as one more model call, and its words come out of wordCount: the outline is planned for 250 words fewer, so the price does not change. The section is ordinary content (an H2, then an H3 and a paragraph per question) that can be edited like any other. Default: false.
body.primaryLanguage body string no The language the article is written in. Alias: language. Must be one of the project's languages; the project's default when neither is given.
body.language body string no Alias of primaryLanguage. Giving both is accepted only when they are the same tag. Must be one of the project's languages; the project's default when neither is given.
body.translationGroupId body string (uuid) no Joins an existing set of language versions: the new article becomes the version in its language. The group must have an article in this project (404 otherwise, whether it does not exist or lives elsewhere), and none of its articles may already be in that language (409).
body.publishedAt body string (date-time) no ISO 8601 date-time: an imported article's original publication date, stored on the DRAFT. Needs articles.publish besides articles.create. Not in the future (60 s of skew allowed) and not before 1990. Publishing later stamps the time of the publish, not this date, unless the publish call repeats it.
body.targetKeywords body string[] no The keywords the article is written to rank for: at most 25, each 1 to 200 characters. Absent: none.
body.secondaryKeywords body string[] no Supporting keywords to work into the article: at most 25, each 1 to 200 characters. Absent: none.
body.ideaId body string (uuid) no Id of a planned idea of the workspace that this article writes; the idea is marked as written by it. 404 when the idea is not in the workspace, checked before anything is created.
body.briefId body string (uuid) no Id of the stored content brief the article is written from, kept as briefId on the article. It must belong to the same project (400 otherwise); 404 when it is not in the workspace.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 201 Created

Field Type Required Description
article object yes The new article as stored, a DRAFT. slug may differ from the one asked for.
article.id string (uuid) yes Id of the article.
article.workspaceId string (uuid) yes Id of the workspace that owns the article.
article.projectId string (uuid) yes Id of the project the article belongs to.
article.title string yes The article's title, printed above its body.
article.slug string yes The article's address segment, unique among the project's articles of the same language. Renaming an article that has been published keeps its old slug as a redirect.
article.status "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes Where the article is in its life. DRAFT: being written or edited, and what an unpublished article goes back to. QUEUED and PROCESSING: owned by an AI job (waiting, then running); nothing but the job moves it out. NEEDS_REVIEW: submitted, waiting for a decision. CHANGES_REQUESTED: sent back by a reviewer (reject or request changes). APPROVED: approved and ready to publish. PUBLISHED: published. Only the workflow routes change it, and a move the state machine does not allow answers 409.
article.documentJson object yes The canonical article body as typed blocks. contentHtml and contentMarkdown are rendered from it on every save, so edit this and never them.
article.documentJson.version 1 yes Version of the document format. Always 1.
article.documentJson.blocks (object<heading> | object<paragraph> | object<list> | object<quote> | object<image> | object<code> | object<table> | object<divider>)[] yes The article body, top to bottom. Each block's type says which of heading, paragraph, list, quote, image, code, table and divider it is and which other fields it carries. An empty array is an empty article.
article.documentJson.blocks[]<heading>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<heading>.type "heading" yes Block kind: always heading.
article.documentJson.blocks[]<heading>.level 1 | 2 | 3 | 4 | 5 | 6 yes Heading rank, 1 (<h1>) to 6 (<h6>). The article's title is its own field and is printed above the body, so generated bodies carry no level-1 heading; one at the very start is part of the lead that stripLead removes.
article.documentJson.blocks[]<heading>.text string yes The heading's words.
article.documentJson.blocks[]<heading>.format "markdown" no Absent: the heading is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<paragraph>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<paragraph>.type "paragraph" yes Block kind: always paragraph.
article.documentJson.blocks[]<paragraph>.text string yes The paragraph's words.
article.documentJson.blocks[]<paragraph>.links object[] no Links from words of text to other articles, at most 20. Only the first link to each article in the whole document is kept (the rest are dropped on save). Absent: none. Ignored when format is "markdown": write the links in the text instead.
article.documentJson.blocks[]<paragraph>.links[].text string yes The words of the block's text that become the link, 1 to 200 characters. Matched literally, first occurrence only; a link whose words no longer occur in the block is dropped on the next save.
article.documentJson.blocks[]<paragraph>.links[].articleId string (uuid) yes Id of the article the words link to. It is resolved when the article is rendered: the words stay plain text until that article is PUBLISHED in the same project and has an address (its blog page, or the URL its publishing integration reported).
article.documentJson.blocks[]<paragraph>.format "markdown" no Absent: the paragraph is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<list>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<list>.type "list" yes Block kind: always list.
article.documentJson.blocks[]<list>.ordered boolean yes true: a numbered list. false: a bulleted list.
article.documentJson.blocks[]<list>.items string[] yes The top-level items, as text. A reader that ignores children still sees every top-level item.
article.documentJson.blocks[]<list>.children (object | null)[] no Optional, one entry per item of items (an entry count that differs from items is refused): the level 2 sub-list under that item, or null for an item with none. Lists nest to four levels.
article.documentJson.blocks[]<list>.children[].ordered boolean yes true: a numbered sub-list. false: a bulleted one. Independent of the parent list.
article.documentJson.blocks[]<list>.children[].items string[] yes The sub-list's items, at least one, as text.
article.documentJson.blocks[]<list>.children[].children (object | null)[] no Optional, one entry per item of this sub-list (an entry count that differs from items is refused): the level 3 sub-list under that item, or null for an item with none. Same shape as article.documentJson.blocks[]<list>.children[]: see above.
article.documentJson.blocks[]<list>.format "markdown" no Absent: every item of the list, at every level, is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<quote>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<quote>.type "quote" yes Block kind: always quote.
article.documentJson.blocks[]<quote>.text string yes The quoted words.
article.documentJson.blocks[]<quote>.attribution string no Who said it, always plain text. Absent: the quote is unattributed.
article.documentJson.blocks[]<quote>.links object[] no Links from words of text to other articles, at most 20. Only the first link to each article in the whole document is kept (the rest are dropped on save). Absent: none. Ignored when format is "markdown": write the links in the text instead.
article.documentJson.blocks[]<quote>.links[].text string yes The words of the block's text that become the link, 1 to 200 characters. Matched literally, first occurrence only; a link whose words no longer occur in the block is dropped on the next save.
article.documentJson.blocks[]<quote>.links[].articleId string (uuid) yes Id of the article the words link to. It is resolved when the article is rendered: the words stay plain text until that article is PUBLISHED in the same project and has an address (its blog page, or the URL its publishing integration reported).
article.documentJson.blocks[]<quote>.format "markdown" no Absent: the quoted text is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<image>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<image>.type "image" yes Block kind: always image.
article.documentJson.blocks[]<image>.assetId string (uuid) yes Id of the article's asset the block shows (assets[].id of GET /api/articles/{articleId}). The asset's address is resolved when the article is rendered; a block whose asset is missing renders as nothing. The article's hero asset is its cover and cannot be added to the body.
article.documentJson.blocks[]<image>.altText string yes Alternative text for the image. When empty, the asset's own alternative text is used when the article is rendered.
article.documentJson.blocks[]<image>.caption string no Plain-text caption printed under the image. Absent: no caption.
article.documentJson.blocks[]<code>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<code>.type "code" yes Block kind: always code.
article.documentJson.blocks[]<code>.language string no The language of the code (the fence's info string), at most 40 characters, e.g. ts. Renderers keep only the characters A-Z a-z 0-9 _ + # . -. Absent: unspecified.
article.documentJson.blocks[]<code>.text string yes The code, verbatim: never interpreted as Markdown and never translated.
article.documentJson.blocks[]<table>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<table>.type "table" yes Block kind: always table.
article.documentJson.blocks[]<table>.header string[] yes The header row: one cell of text per column, at least one.
article.documentJson.blocks[]<table>.rows string[][] yes The body rows, each an array of cell text with exactly as many cells as header (otherwise refused). May be empty.
article.documentJson.blocks[]<table>.align ("left" | "center" | "right" | null)[] no One alignment per column, so as many entries as header has cells (otherwise refused); null leaves a column to the browser's default. Absent: all default.
article.documentJson.blocks[]<table>.format "markdown" no Absent: every cell, header included, is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<divider>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<divider>.type "divider" yes Block kind: always divider.
article.contentHtml string | null yes The body rendered as HTML from documentJson, with image blocks resolved to their public URLs. Read-only: re-rendered on every document save and again on publish. null before the first render.
article.contentMarkdown string | null yes The body rendered as Markdown from documentJson, with image blocks resolved to their public URLs. Read-only: re-rendered like contentHtml. null before the first render. On GET /api/articles/{articleId}, stripLead=true returns it without the lead.
article.metaTitle string | null yes The SEO title for search results and link previews; null when none is set.
article.metaDescription string | null yes The SEO description for search results and link previews; null when none is set.
article.targetKeywords string[] yes The keywords the article is written to rank for, in the order given.
article.secondaryKeywords string[] yes Supporting keywords to work into the article; empty when none.
article.primaryLanguage string yes Language tag the article is written in, e.g. en or pt-BR; one of the project's languages.
article.translationGroupId string (uuid) yes Shared by every language version of one article, so the versions can be found together. An article with no translations is alone in its group.
article.generationConfig object | null yes The settings an AI generation was started with (topic, tone, length, image style, ...), kept so the article can be rerun. null for an article created without them.
article.generationConfig.topic string yes What the article is about (1 to 500 characters). The outline step plans the article from it, and with primaryKeyword it is the query for web research and for the project's knowledge base. The article's title is not copied from it: the outline's title, written by the model, becomes the title.
article.generationConfig.prompt string no Free-form direction for the writer, up to 5000 characters (angle, points to cover, things to avoid). Sent to the outline step as 'Additional direction'; the section writers follow the outline it produces.
article.generationConfig.primaryKeyword string no The keyword the article targets, up to 200 characters. The outline step is told to use it where it reads naturally, and it joins topic in the research and knowledge-base queries. It is not copied into the article's targetKeywords.
article.generationConfig.secondaryKeywords string[] yes Supporting keywords, up to 25 of 1 to 200 characters. Sent to the outline step, which spreads them over the sections' keywords. Empty by default. Default: [].
article.generationConfig.audience string no Who the article is for, up to 500 characters. Sent to the outline step, to every section writer, to the image-prompt writers and to the knowledge-base query.
article.generationConfig.tone string no The tone to write in, as free text up to 200 characters; the web app offers professional, conversational, authoritative, practical and persuasive. Sent to the outline step as 'Tone'. The section writers read no separate tone: they follow the outline and the brand voice.
article.generationConfig.language string yes Language the article is written in, as a language tag such as en or pt-BR (default en). The outline, section, research and image-prompt steps use it, so the text and the images' alt text come out in it. A run does not change the article's own primaryLanguage. Default: "en".
article.generationConfig.wordCount integer yes Target length of the whole article in words, 200 to 10000 (default 1200). The outline's sections are planned to add up to roughly this. It also sets the text price per started 1,000 words: 2 credits with writingQuality premium, 1 with standard (1200 costs 4 or 2), and the image count when imageCount is null. Default: 1200.
article.generationConfig.imageStyle "photorealistic" | "editorial" | "minimalist-vector" | "3d-render" | "watercolor" | "corporate-tech" | "cinematic" | "illustration" yes Style of the article's images (default editorial). It applies to the cover and the inline images unless the project's imageSettings set a hero.style or inline.style, which take precedence. Values: photorealistic (natural-light documentary photograph, realistic textures, no stylisation), editorial (modern editorial illustration or photograph, clean composition, restrained palette), minimalist-vector (flat vector illustration, few shapes, solid colours, plenty of empty space), 3d-render (soft studio 3D render, smooth matte materials, simple forms), watercolor (loose watercolour painting, soft edges, muted washes), corporate-tech (clean technology visual in cool blues and greys, geometric shapes), cinematic (cinematic still, dramatic lighting, strong atmosphere, wide framing), illustration (hand-drawn illustration, confident linework, warm friendly palette). Default: "editorial".
article.generationConfig.imageCount integer | null yes How many images to make, cover included, 0 to 20. 0 makes none, 1 only the cover, more add one inline image per section up to the number of outline sections. null (the default) derives it from wordCount: 2 under 1,000 words, 3 under 2,000, 4 under 3,000, otherwise 5. Each image delivered costs 1 credit, or 3 when imageQuality is hd; the quote and the reservation count the full number. Default: null.
article.generationConfig.writingQuality "standard" | "premium" yes Which model writes the article (default premium). premium uses the deployment's article model and costs 2 credits per started 1,000 words; standard uses a lighter, cheaper model and costs 1 credit per started 1,000 words. Default: "premium".
article.generationConfig.imageQuality "standard" | "hd" yes Quality of the article's images (default standard). standard costs 1 credit per image; hd asks the image model for its highest quality and costs 3 credits per image, for the run's images and for each regeneration of an image of the article. Default: "standard".
article.generationConfig.brandVoiceId string (uuid) no Id of the brand voice to write in. Absent, or an id that is not one of the workspace's voices, the project's default voice applies, then the workspace's default voice; with neither, no voice is applied.
article.generationConfig.contentTemplateId string (uuid) no Id of the content template to follow: the outline must contain its sections, and the last section ends on its call to action. Only built-in templates and the workspace's own count; any other id, or a deleted template, is ignored without an error.
article.generationConfig.sourceUrl string (uri) no A reference URL for the article. It is stored with the configuration; no generation step reads or fetches it.
article.generationConfig.researchEnabled boolean yes Whether to research and fact-check (default false). Research looks up sources before the outline (needs the deployment's web_research feature); fact checking verifies the finished article's claims (needs fact_checking). Research adds 1 credit and fact checking 3 to the quote, each only when its feature is on, and is charged only if it delivered: research found a source, fact checking checked a claim. Default: false.
article.generationConfig.reviewOutline boolean yes When true the run stops after the outline (default false): the job ends COMPLETED, the article stays DRAFT with the outline stored, and only research, if any, is charged. Read it with GET /api/articles/{articleId}/outline, edit it, then POST /api/articles/{articleId}/outline/approve to write the article. Default: false.
article.generationConfig.faq boolean yes When true (default false) the article ends with a "Frequently asked questions" section of 3 to 5 questions a reader still has after the body, each answered in two or three sentences, in the article's language. It is written after the sections as one more model call, and its words come out of wordCount: the outline is planned for 250 words fewer, so the price does not change. The section is ordinary content (an H2, then an H3 and a paragraph per question) that can be edited like any other. Default: false.
article.outline object | null yes The outline the AI pipeline planned the article from, kept so a writing step can be retried (and, with reviewOutline, the one a person approved). null for an article that was not generated.
article.outline.title string yes The article title, 1 to 300 characters.
article.outline.slug string yes The URL slug: lowercase letters and digits separated by hyphens, up to 120 characters.
article.outline.metaTitle string yes The SEO title, 1 to 70 characters.
article.outline.metaDescription string yes The SEO description, 1 to 200 characters.
article.outline.sections object[] yes The sections in reading order, 1 to 40.
article.outline.sections[].id string yes Identifier of the section inside the outline, 1 to 64 characters.
article.outline.sections[].heading string yes The section's heading, 1 to 300 characters.
article.outline.sections[].level 2 | 3 yes Heading rank: 2 for a main section, 3 for a sub-section.
article.outline.sections[].purpose string yes What the section covers, 1 to 1000 characters.
article.outline.sections[].keywords string[] yes The keywords attached to the section, at most 25 of 1 to 120 characters. Empty: none. Default: [].
article.outline.sections[].estimatedWords integer yes The section's length in words, between 20 and 5000.
article.createdBy string | null yes Id of the user who created the article (for an article made with an API key, the key's creator). null when no user is attached or that user has been deleted.
article.publishedAt string (date-time) | null yes The article's publication date: when it was published, or the date given at creation or publish for an imported article. null until then; unpublishing keeps it.
article.briefId string (uuid) | null yes Id of the stored content brief the article was written from; null when none.
article.publishedUrl string | null yes The address the publishing integration reported for the live article, which links from other articles point to. null until a publish reports one, and for a webhook target, which never does.
article.wordCount integer | null yes Words in documentJson, stored with every document write; null for an article not counted since the count was added, until the backfill reaches it.
article.createdAt string (date-time) yes When the article was created.
article.updatedAt string (date-time) yes When the article last changed (a save, a status move, a re-render). It is what expectedUpdatedAt is compared with on PATCH /api/articles/{articleId}.
possibleDuplicates object[] yes Articles of the same project and language, in another translation group, whose title or slug is close to the new one's. Advice, never a refusal; empty when none, and when the check itself failed.
possibleDuplicates[].id string (uuid) yes Id of the existing article that looks like the new one.
possibleDuplicates[].translationGroupId string (uuid) yes Translation group of that article; it is never the new article's own group.
possibleDuplicates[].title string yes The existing article's title.
possibleDuplicates[].slug string yes The existing article's slug.
possibleDuplicates[].score number yes How close the title or slug is to the new one, from 0 to 1; 1 is the same words or the same address.
conversions object[] yes What a contentMarkdown import converted so the document could hold it, in source order: { construct: "horizontal rule", action: "divider" }, { construct: "nested list", action: "nested" } (kept as children), or action: "flattened" (a sub-list deeper than four levels, merged into the fourth). Empty when nothing was converted, and always empty for a document body.
conversions[].construct "horizontal rule" | "nested list" yes The Markdown construct that needed converting.
conversions[].line integer yes 1-based line of the Markdown source where the construct starts.
conversions[].action "divider" | "nested" | "flattened" yes What the import did with it. divider: a horizontal rule became a divider block. nested: a sub-list was kept as children of its list. flattened: a sub-list deeper than four levels was merged into the fourth.
warnings object[] yes Advice about the new article, never a refusal; empty when there is nothing to say. POSSIBLE_DUPLICATE: an article of the same project and language, in another translation group, whose title (or slug) is close: the possibleDuplicates entries, as articleId and similarity (0..1, flagged from 0.6).
warnings[].code "POSSIBLE_DUPLICATE" yes Kind of advice: POSSIBLE_DUPLICATE, the only one so far.
warnings[].articleId string (uuid) yes Id of the existing article that looks like the new one.
warnings[].translationGroupId string (uuid) yes Translation group of that article.
warnings[].similarity number yes How close the title or slug is to the new one, from 0 to 1; 1 is the same words or the same address.
warnings[].title string yes The existing article's title.
warnings[].slug string yes The existing article's slug.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.create({
  body: {
    projectId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
    title: "example",
  },
});
console.log(result);

pd.articles.list(input, init?)

List articles

GET /api/articles

One page of the workspace's articles, newest change first, and how many match in all. Every filter narrows the result: an article is listed only when it passes all the filters given, and total counts the same set. Each language version of an article is its own article; translationGroupId links them. The list carries the card fields, never the document: read one article for that. Needs articles.read on the workspace, or on the project when projectId is given.

Input

Name In Type Required Description
workspaceId query string (uuid) yes Id of the workspace whose articles are listed.
projectId query string (uuid) no Only articles of this project, and the permission is then checked on that project. Absent: every project of the workspace.
categoryId query string (uuid) no Only articles carrying this category.
category query string (uuid) no Alias of categoryId. Giving both is accepted only when they are the same id; otherwise the request is refused with INVALID_REQUEST.
status query "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" no Only articles in this status.
language query string no Only articles whose primary language is exactly this tag, e.g. fr or pt-BR (en does not match en-GB). Each language version of an article is its own article.
slug query string no Only articles whose current slug is exactly this. A slug is unique per language of a project, so with projectId this finds one article per language that uses it; add language for at most one. Without projectId, one per project and language. An old slug that now only redirects to a renamed article is not matched.
q query string no Case-insensitive match on the title: articles whose title contains this text. % and _ match themselves. An empty value is no search.
createdBy query "me" no me: only the articles the caller created. For an API key, those of the user who created the key (400 for a key with no creator). Absent: everyone's.
limit query integer no Page size, 1 to 100. Default 25. Default: 25.
offset query integer no How many matching articles to skip, for paging. Default 0. Default: 0.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
articles object[] yes The page of articles, newest change first (updatedAt).
articles[].id string (uuid) yes Id of the article.
articles[].projectId string (uuid) yes Id of the project the article belongs to.
articles[].title string yes The article's title, printed above its body.
articles[].slug string yes The article's address segment, unique among the project's articles of the same language.
articles[].status "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes Where the article is in its life. DRAFT: being written or edited, and what an unpublished article goes back to. QUEUED and PROCESSING: owned by an AI job (waiting, then running); nothing but the job moves it out. NEEDS_REVIEW: submitted, waiting for a decision. CHANGES_REQUESTED: sent back by a reviewer (reject or request changes). APPROVED: approved and ready to publish. PUBLISHED: published. Only the workflow routes change it, and a move the state machine does not allow answers 409.
articles[].metaTitle string | null yes The SEO title for search results and link previews; null when none is set.
articles[].metaDescription string | null yes The SEO description for search results and link previews; null when none is set.
articles[].primaryLanguage string yes Language tag the article is written in, e.g. en or pt-BR; one of the project's languages.
articles[].translationGroupId string (uuid) yes Shared by every language version of one article, so the versions can be found together. An article with no translations is alone in its group.
articles[].createdBy string | null yes Id of the user who created the article (for an article made with an API key, the key's creator). null when no user is attached or that user has been deleted.
articles[].publishedAt string (date-time) | null yes The article's publication date: when it was published, or the date given at creation or publish for an imported article. null until then; unpublishing keeps it.
articles[].createdAt string (date-time) yes When the article was created.
articles[].updatedAt string (date-time) yes When the article last changed (a save, a status move, a re-render). It is what expectedUpdatedAt is compared with on PATCH /api/articles/{articleId}.
articles[].categories object[] yes The categories assigned to the article, in the order they were assigned.
articles[].categories[].id string (uuid) yes Id of the category.
articles[].categories[].primaryLanguage string yes Language tag of the category's main name.
articles[].categories[].translations object[] yes The category's name in each language it has.
articles[].categories[].translations[].language string yes Language tag of this name.
articles[].categories[].translations[].name string yes The category's name in language.
articles[].categories[].translations[].slug string yes URL-safe form of name, unique among the workspace's categories in language.
articles[].targetKeywords string[] yes The article's target keywords, in order.
articles[].heroImageUrl string | null yes The public URL of the article's hero (cover) image; null when it has none.
articles[].wordCount integer yes Words in the article's document, as the editor counts them (countWords).
total integer yes How many articles match the filters in all, not only on this page.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.list({
  workspaceId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.duplicate(input, init?)

Duplicate an article

POST /api/articles/{articleId}/duplicate

A DRAFT copy in the same project, with its own copy of every image. Answers like POST /api/articles. Needs articles.read and articles.create. The copy has the original's blocks, keywords, language, generation settings and categories, a new translation group of its own, and a slug derived from its title; meta title and meta description, comments, reviews and version history are not copied, and the copy starts at version 1. QUOTA_EXCEEDED (429) when the copied images do not fit the workspace's storage quota. Sends the article.created webhook and writes an articles.created audit entry.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
body body object | null no
body.title body string no The copy's title. Absent: Copy of <the original's title>. Either way it is cut to 300 characters, ending in an ellipsis when it was cut.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 201 Created

Field Type Required Description
article object yes The copy, a new DRAFT with its own id, its own translation group and its own copies of the images.
article.id string (uuid) yes Id of the article.
article.workspaceId string (uuid) yes Id of the workspace that owns the article.
article.projectId string (uuid) yes Id of the project the article belongs to.
article.title string yes The article's title, printed above its body.
article.slug string yes The article's address segment, unique among the project's articles of the same language. Renaming an article that has been published keeps its old slug as a redirect.
article.status "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes Where the article is in its life. DRAFT: being written or edited, and what an unpublished article goes back to. QUEUED and PROCESSING: owned by an AI job (waiting, then running); nothing but the job moves it out. NEEDS_REVIEW: submitted, waiting for a decision. CHANGES_REQUESTED: sent back by a reviewer (reject or request changes). APPROVED: approved and ready to publish. PUBLISHED: published. Only the workflow routes change it, and a move the state machine does not allow answers 409.
article.documentJson object yes The canonical article body as typed blocks. contentHtml and contentMarkdown are rendered from it on every save, so edit this and never them.
article.documentJson.version 1 yes Version of the document format. Always 1.
article.documentJson.blocks (object<heading> | object<paragraph> | object<list> | object<quote> | object<image> | object<code> | object<table> | object<divider>)[] yes The article body, top to bottom. Each block's type says which of heading, paragraph, list, quote, image, code, table and divider it is and which other fields it carries. An empty array is an empty article.
article.documentJson.blocks[]<heading>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<heading>.type "heading" yes Block kind: always heading.
article.documentJson.blocks[]<heading>.level 1 | 2 | 3 | 4 | 5 | 6 yes Heading rank, 1 (<h1>) to 6 (<h6>). The article's title is its own field and is printed above the body, so generated bodies carry no level-1 heading; one at the very start is part of the lead that stripLead removes.
article.documentJson.blocks[]<heading>.text string yes The heading's words.
article.documentJson.blocks[]<heading>.format "markdown" no Absent: the heading is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<paragraph>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<paragraph>.type "paragraph" yes Block kind: always paragraph.
article.documentJson.blocks[]<paragraph>.text string yes The paragraph's words.
article.documentJson.blocks[]<paragraph>.links object[] no Links from words of text to other articles, at most 20. Only the first link to each article in the whole document is kept (the rest are dropped on save). Absent: none. Ignored when format is "markdown": write the links in the text instead.
article.documentJson.blocks[]<paragraph>.links[].text string yes The words of the block's text that become the link, 1 to 200 characters. Matched literally, first occurrence only; a link whose words no longer occur in the block is dropped on the next save.
article.documentJson.blocks[]<paragraph>.links[].articleId string (uuid) yes Id of the article the words link to. It is resolved when the article is rendered: the words stay plain text until that article is PUBLISHED in the same project and has an address (its blog page, or the URL its publishing integration reported).
article.documentJson.blocks[]<paragraph>.format "markdown" no Absent: the paragraph is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<list>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<list>.type "list" yes Block kind: always list.
article.documentJson.blocks[]<list>.ordered boolean yes true: a numbered list. false: a bulleted list.
article.documentJson.blocks[]<list>.items string[] yes The top-level items, as text. A reader that ignores children still sees every top-level item.
article.documentJson.blocks[]<list>.children (object | null)[] no Optional, one entry per item of items (an entry count that differs from items is refused): the level 2 sub-list under that item, or null for an item with none. Lists nest to four levels.
article.documentJson.blocks[]<list>.children[].ordered boolean yes true: a numbered sub-list. false: a bulleted one. Independent of the parent list.
article.documentJson.blocks[]<list>.children[].items string[] yes The sub-list's items, at least one, as text.
article.documentJson.blocks[]<list>.children[].children (object | null)[] no Optional, one entry per item of this sub-list (an entry count that differs from items is refused): the level 3 sub-list under that item, or null for an item with none. Same shape as article.documentJson.blocks[]<list>.children[]: see above.
article.documentJson.blocks[]<list>.format "markdown" no Absent: every item of the list, at every level, is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<quote>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<quote>.type "quote" yes Block kind: always quote.
article.documentJson.blocks[]<quote>.text string yes The quoted words.
article.documentJson.blocks[]<quote>.attribution string no Who said it, always plain text. Absent: the quote is unattributed.
article.documentJson.blocks[]<quote>.links object[] no Links from words of text to other articles, at most 20. Only the first link to each article in the whole document is kept (the rest are dropped on save). Absent: none. Ignored when format is "markdown": write the links in the text instead.
article.documentJson.blocks[]<quote>.links[].text string yes The words of the block's text that become the link, 1 to 200 characters. Matched literally, first occurrence only; a link whose words no longer occur in the block is dropped on the next save.
article.documentJson.blocks[]<quote>.links[].articleId string (uuid) yes Id of the article the words link to. It is resolved when the article is rendered: the words stay plain text until that article is PUBLISHED in the same project and has an address (its blog page, or the URL its publishing integration reported).
article.documentJson.blocks[]<quote>.format "markdown" no Absent: the quoted text is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<image>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<image>.type "image" yes Block kind: always image.
article.documentJson.blocks[]<image>.assetId string (uuid) yes Id of the article's asset the block shows (assets[].id of GET /api/articles/{articleId}). The asset's address is resolved when the article is rendered; a block whose asset is missing renders as nothing. The article's hero asset is its cover and cannot be added to the body.
article.documentJson.blocks[]<image>.altText string yes Alternative text for the image. When empty, the asset's own alternative text is used when the article is rendered.
article.documentJson.blocks[]<image>.caption string no Plain-text caption printed under the image. Absent: no caption.
article.documentJson.blocks[]<code>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<code>.type "code" yes Block kind: always code.
article.documentJson.blocks[]<code>.language string no The language of the code (the fence's info string), at most 40 characters, e.g. ts. Renderers keep only the characters A-Z a-z 0-9 _ + # . -. Absent: unspecified.
article.documentJson.blocks[]<code>.text string yes The code, verbatim: never interpreted as Markdown and never translated.
article.documentJson.blocks[]<table>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<table>.type "table" yes Block kind: always table.
article.documentJson.blocks[]<table>.header string[] yes The header row: one cell of text per column, at least one.
article.documentJson.blocks[]<table>.rows string[][] yes The body rows, each an array of cell text with exactly as many cells as header (otherwise refused). May be empty.
article.documentJson.blocks[]<table>.align ("left" | "center" | "right" | null)[] no One alignment per column, so as many entries as header has cells (otherwise refused); null leaves a column to the browser's default. Absent: all default.
article.documentJson.blocks[]<table>.format "markdown" no Absent: every cell, header included, is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<divider>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<divider>.type "divider" yes Block kind: always divider.
article.contentHtml string | null yes The body rendered as HTML from documentJson, with image blocks resolved to their public URLs. Read-only: re-rendered on every document save and again on publish. null before the first render.
article.contentMarkdown string | null yes The body rendered as Markdown from documentJson, with image blocks resolved to their public URLs. Read-only: re-rendered like contentHtml. null before the first render. On GET /api/articles/{articleId}, stripLead=true returns it without the lead.
article.metaTitle string | null yes The SEO title for search results and link previews; null when none is set.
article.metaDescription string | null yes The SEO description for search results and link previews; null when none is set.
article.targetKeywords string[] yes The keywords the article is written to rank for, in the order given.
article.secondaryKeywords string[] yes Supporting keywords to work into the article; empty when none.
article.primaryLanguage string yes Language tag the article is written in, e.g. en or pt-BR; one of the project's languages.
article.translationGroupId string (uuid) yes Shared by every language version of one article, so the versions can be found together. An article with no translations is alone in its group.
article.generationConfig object | null yes The settings an AI generation was started with (topic, tone, length, image style, ...), kept so the article can be rerun. null for an article created without them.
article.generationConfig.topic string yes What the article is about (1 to 500 characters). The outline step plans the article from it, and with primaryKeyword it is the query for web research and for the project's knowledge base. The article's title is not copied from it: the outline's title, written by the model, becomes the title.
article.generationConfig.prompt string no Free-form direction for the writer, up to 5000 characters (angle, points to cover, things to avoid). Sent to the outline step as 'Additional direction'; the section writers follow the outline it produces.
article.generationConfig.primaryKeyword string no The keyword the article targets, up to 200 characters. The outline step is told to use it where it reads naturally, and it joins topic in the research and knowledge-base queries. It is not copied into the article's targetKeywords.
article.generationConfig.secondaryKeywords string[] yes Supporting keywords, up to 25 of 1 to 200 characters. Sent to the outline step, which spreads them over the sections' keywords. Empty by default. Default: [].
article.generationConfig.audience string no Who the article is for, up to 500 characters. Sent to the outline step, to every section writer, to the image-prompt writers and to the knowledge-base query.
article.generationConfig.tone string no The tone to write in, as free text up to 200 characters; the web app offers professional, conversational, authoritative, practical and persuasive. Sent to the outline step as 'Tone'. The section writers read no separate tone: they follow the outline and the brand voice.
article.generationConfig.language string yes Language the article is written in, as a language tag such as en or pt-BR (default en). The outline, section, research and image-prompt steps use it, so the text and the images' alt text come out in it. A run does not change the article's own primaryLanguage. Default: "en".
article.generationConfig.wordCount integer yes Target length of the whole article in words, 200 to 10000 (default 1200). The outline's sections are planned to add up to roughly this. It also sets the text price per started 1,000 words: 2 credits with writingQuality premium, 1 with standard (1200 costs 4 or 2), and the image count when imageCount is null. Default: 1200.
article.generationConfig.imageStyle "photorealistic" | "editorial" | "minimalist-vector" | "3d-render" | "watercolor" | "corporate-tech" | "cinematic" | "illustration" yes Style of the article's images (default editorial). It applies to the cover and the inline images unless the project's imageSettings set a hero.style or inline.style, which take precedence. Values: photorealistic (natural-light documentary photograph, realistic textures, no stylisation), editorial (modern editorial illustration or photograph, clean composition, restrained palette), minimalist-vector (flat vector illustration, few shapes, solid colours, plenty of empty space), 3d-render (soft studio 3D render, smooth matte materials, simple forms), watercolor (loose watercolour painting, soft edges, muted washes), corporate-tech (clean technology visual in cool blues and greys, geometric shapes), cinematic (cinematic still, dramatic lighting, strong atmosphere, wide framing), illustration (hand-drawn illustration, confident linework, warm friendly palette). Default: "editorial".
article.generationConfig.imageCount integer | null yes How many images to make, cover included, 0 to 20. 0 makes none, 1 only the cover, more add one inline image per section up to the number of outline sections. null (the default) derives it from wordCount: 2 under 1,000 words, 3 under 2,000, 4 under 3,000, otherwise 5. Each image delivered costs 1 credit, or 3 when imageQuality is hd; the quote and the reservation count the full number. Default: null.
article.generationConfig.writingQuality "standard" | "premium" yes Which model writes the article (default premium). premium uses the deployment's article model and costs 2 credits per started 1,000 words; standard uses a lighter, cheaper model and costs 1 credit per started 1,000 words. Default: "premium".
article.generationConfig.imageQuality "standard" | "hd" yes Quality of the article's images (default standard). standard costs 1 credit per image; hd asks the image model for its highest quality and costs 3 credits per image, for the run's images and for each regeneration of an image of the article. Default: "standard".
article.generationConfig.brandVoiceId string (uuid) no Id of the brand voice to write in. Absent, or an id that is not one of the workspace's voices, the project's default voice applies, then the workspace's default voice; with neither, no voice is applied.
article.generationConfig.contentTemplateId string (uuid) no Id of the content template to follow: the outline must contain its sections, and the last section ends on its call to action. Only built-in templates and the workspace's own count; any other id, or a deleted template, is ignored without an error.
article.generationConfig.sourceUrl string (uri) no A reference URL for the article. It is stored with the configuration; no generation step reads or fetches it.
article.generationConfig.researchEnabled boolean yes Whether to research and fact-check (default false). Research looks up sources before the outline (needs the deployment's web_research feature); fact checking verifies the finished article's claims (needs fact_checking). Research adds 1 credit and fact checking 3 to the quote, each only when its feature is on, and is charged only if it delivered: research found a source, fact checking checked a claim. Default: false.
article.generationConfig.reviewOutline boolean yes When true the run stops after the outline (default false): the job ends COMPLETED, the article stays DRAFT with the outline stored, and only research, if any, is charged. Read it with GET /api/articles/{articleId}/outline, edit it, then POST /api/articles/{articleId}/outline/approve to write the article. Default: false.
article.generationConfig.faq boolean yes When true (default false) the article ends with a "Frequently asked questions" section of 3 to 5 questions a reader still has after the body, each answered in two or three sentences, in the article's language. It is written after the sections as one more model call, and its words come out of wordCount: the outline is planned for 250 words fewer, so the price does not change. The section is ordinary content (an H2, then an H3 and a paragraph per question) that can be edited like any other. Default: false.
article.outline object | null yes The outline the AI pipeline planned the article from, kept so a writing step can be retried (and, with reviewOutline, the one a person approved). null for an article that was not generated.
article.outline.title string yes The article title, 1 to 300 characters.
article.outline.slug string yes The URL slug: lowercase letters and digits separated by hyphens, up to 120 characters.
article.outline.metaTitle string yes The SEO title, 1 to 70 characters.
article.outline.metaDescription string yes The SEO description, 1 to 200 characters.
article.outline.sections object[] yes The sections in reading order, 1 to 40.
article.outline.sections[].id string yes Identifier of the section inside the outline, 1 to 64 characters.
article.outline.sections[].heading string yes The section's heading, 1 to 300 characters.
article.outline.sections[].level 2 | 3 yes Heading rank: 2 for a main section, 3 for a sub-section.
article.outline.sections[].purpose string yes What the section covers, 1 to 1000 characters.
article.outline.sections[].keywords string[] yes The keywords attached to the section, at most 25 of 1 to 120 characters. Empty: none. Default: [].
article.outline.sections[].estimatedWords integer yes The section's length in words, between 20 and 5000.
article.createdBy string | null yes Id of the user who created the article (for an article made with an API key, the key's creator). null when no user is attached or that user has been deleted.
article.publishedAt string (date-time) | null yes The article's publication date: when it was published, or the date given at creation or publish for an imported article. null until then; unpublishing keeps it.
article.briefId string (uuid) | null yes Id of the stored content brief the article was written from; null when none.
article.publishedUrl string | null yes The address the publishing integration reported for the live article, which links from other articles point to. null until a publish reports one, and for a webhook target, which never does.
article.wordCount integer | null yes Words in documentJson, stored with every document write; null for an article not counted since the count was added, until the backfill reaches it.
article.createdAt string (date-time) yes When the article was created.
article.updatedAt string (date-time) yes When the article last changed (a save, a status move, a re-render). It is what expectedUpdatedAt is compared with on PATCH /api/articles/{articleId}.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.duplicate({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.get(input, init?)

Read an article

GET /api/articles/{articleId}

documentJson is the canonical article; contentHtml and contentMarkdown are derived from it on every save. Also returns the article's images (assets), the blog address of a published post of the PrismDraft site (blogPath), its categories, and its language versions with the project's languages (translations). Needs articles.read. Send stripLead=true to get contentMarkdown without the title heading and hero image.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
stripLead query "0" | "1" | "true" | "false" no true (or 1): article.contentMarkdown omits the lead: a level-1 heading and the hero image at the very start of the document, in either order, each at most once (a later H1 or image stays). For a consumer that prints the title and the cover itself. Only article.contentMarkdown changes: contentHtml, documentJson, assets, categories and everything else are returned as stored, and nothing is saved. The remaining blocks render exactly as in the default contentMarkdown. false, 0 or absent: unchanged. Any other value is refused with INVALID_REQUEST.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
article object yes The stored article, with its categories.
article.id string (uuid) yes Id of the article.
article.workspaceId string (uuid) yes Id of the workspace that owns the article.
article.projectId string (uuid) yes Id of the project the article belongs to.
article.title string yes The article's title, printed above its body.
article.slug string yes The article's address segment, unique among the project's articles of the same language. Renaming an article that has been published keeps its old slug as a redirect.
article.status "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes Where the article is in its life. DRAFT: being written or edited, and what an unpublished article goes back to. QUEUED and PROCESSING: owned by an AI job (waiting, then running); nothing but the job moves it out. NEEDS_REVIEW: submitted, waiting for a decision. CHANGES_REQUESTED: sent back by a reviewer (reject or request changes). APPROVED: approved and ready to publish. PUBLISHED: published. Only the workflow routes change it, and a move the state machine does not allow answers 409.
article.documentJson object yes The canonical article body as typed blocks. contentHtml and contentMarkdown are rendered from it on every save, so edit this and never them.
article.documentJson.version 1 yes Version of the document format. Always 1.
article.documentJson.blocks (object<heading> | object<paragraph> | object<list> | object<quote> | object<image> | object<code> | object<table> | object<divider>)[] yes The article body, top to bottom. Each block's type says which of heading, paragraph, list, quote, image, code, table and divider it is and which other fields it carries. An empty array is an empty article.
article.documentJson.blocks[]<heading>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<heading>.type "heading" yes Block kind: always heading.
article.documentJson.blocks[]<heading>.level 1 | 2 | 3 | 4 | 5 | 6 yes Heading rank, 1 (<h1>) to 6 (<h6>). The article's title is its own field and is printed above the body, so generated bodies carry no level-1 heading; one at the very start is part of the lead that stripLead removes.
article.documentJson.blocks[]<heading>.text string yes The heading's words.
article.documentJson.blocks[]<heading>.format "markdown" no Absent: the heading is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<paragraph>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<paragraph>.type "paragraph" yes Block kind: always paragraph.
article.documentJson.blocks[]<paragraph>.text string yes The paragraph's words.
article.documentJson.blocks[]<paragraph>.links object[] no Links from words of text to other articles, at most 20. Only the first link to each article in the whole document is kept (the rest are dropped on save). Absent: none. Ignored when format is "markdown": write the links in the text instead.
article.documentJson.blocks[]<paragraph>.links[].text string yes The words of the block's text that become the link, 1 to 200 characters. Matched literally, first occurrence only; a link whose words no longer occur in the block is dropped on the next save.
article.documentJson.blocks[]<paragraph>.links[].articleId string (uuid) yes Id of the article the words link to. It is resolved when the article is rendered: the words stay plain text until that article is PUBLISHED in the same project and has an address (its blog page, or the URL its publishing integration reported).
article.documentJson.blocks[]<paragraph>.format "markdown" no Absent: the paragraph is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<list>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<list>.type "list" yes Block kind: always list.
article.documentJson.blocks[]<list>.ordered boolean yes true: a numbered list. false: a bulleted list.
article.documentJson.blocks[]<list>.items string[] yes The top-level items, as text. A reader that ignores children still sees every top-level item.
article.documentJson.blocks[]<list>.children (object | null)[] no Optional, one entry per item of items (an entry count that differs from items is refused): the level 2 sub-list under that item, or null for an item with none. Lists nest to four levels.
article.documentJson.blocks[]<list>.children[].ordered boolean yes true: a numbered sub-list. false: a bulleted one. Independent of the parent list.
article.documentJson.blocks[]<list>.children[].items string[] yes The sub-list's items, at least one, as text.
article.documentJson.blocks[]<list>.children[].children (object | null)[] no Optional, one entry per item of this sub-list (an entry count that differs from items is refused): the level 3 sub-list under that item, or null for an item with none. Same shape as article.documentJson.blocks[]<list>.children[]: see above.
article.documentJson.blocks[]<list>.format "markdown" no Absent: every item of the list, at every level, is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<quote>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<quote>.type "quote" yes Block kind: always quote.
article.documentJson.blocks[]<quote>.text string yes The quoted words.
article.documentJson.blocks[]<quote>.attribution string no Who said it, always plain text. Absent: the quote is unattributed.
article.documentJson.blocks[]<quote>.links object[] no Links from words of text to other articles, at most 20. Only the first link to each article in the whole document is kept (the rest are dropped on save). Absent: none. Ignored when format is "markdown": write the links in the text instead.
article.documentJson.blocks[]<quote>.links[].text string yes The words of the block's text that become the link, 1 to 200 characters. Matched literally, first occurrence only; a link whose words no longer occur in the block is dropped on the next save.
article.documentJson.blocks[]<quote>.links[].articleId string (uuid) yes Id of the article the words link to. It is resolved when the article is rendered: the words stay plain text until that article is PUBLISHED in the same project and has an address (its blog page, or the URL its publishing integration reported).
article.documentJson.blocks[]<quote>.format "markdown" no Absent: the quoted text is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<image>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<image>.type "image" yes Block kind: always image.
article.documentJson.blocks[]<image>.assetId string (uuid) yes Id of the article's asset the block shows (assets[].id of GET /api/articles/{articleId}). The asset's address is resolved when the article is rendered; a block whose asset is missing renders as nothing. The article's hero asset is its cover and cannot be added to the body.
article.documentJson.blocks[]<image>.altText string yes Alternative text for the image. When empty, the asset's own alternative text is used when the article is rendered.
article.documentJson.blocks[]<image>.caption string no Plain-text caption printed under the image. Absent: no caption.
article.documentJson.blocks[]<code>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<code>.type "code" yes Block kind: always code.
article.documentJson.blocks[]<code>.language string no The language of the code (the fence's info string), at most 40 characters, e.g. ts. Renderers keep only the characters A-Z a-z 0-9 _ + # . -. Absent: unspecified.
article.documentJson.blocks[]<code>.text string yes The code, verbatim: never interpreted as Markdown and never translated.
article.documentJson.blocks[]<table>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<table>.type "table" yes Block kind: always table.
article.documentJson.blocks[]<table>.header string[] yes The header row: one cell of text per column, at least one.
article.documentJson.blocks[]<table>.rows string[][] yes The body rows, each an array of cell text with exactly as many cells as header (otherwise refused). May be empty.
article.documentJson.blocks[]<table>.align ("left" | "center" | "right" | null)[] no One alignment per column, so as many entries as header has cells (otherwise refused); null leaves a column to the browser's default. Absent: all default.
article.documentJson.blocks[]<table>.format "markdown" no Absent: every cell, header included, is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<divider>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<divider>.type "divider" yes Block kind: always divider.
article.contentHtml string | null yes The body rendered as HTML from documentJson, with image blocks resolved to their public URLs. Read-only: re-rendered on every document save and again on publish. null before the first render.
article.contentMarkdown string | null yes The body rendered as Markdown from documentJson, with image blocks resolved to their public URLs. Read-only: re-rendered like contentHtml. null before the first render. On GET /api/articles/{articleId}, stripLead=true returns it without the lead.
article.metaTitle string | null yes The SEO title for search results and link previews; null when none is set.
article.metaDescription string | null yes The SEO description for search results and link previews; null when none is set.
article.targetKeywords string[] yes The keywords the article is written to rank for, in the order given.
article.secondaryKeywords string[] yes Supporting keywords to work into the article; empty when none.
article.primaryLanguage string yes Language tag the article is written in, e.g. en or pt-BR; one of the project's languages.
article.translationGroupId string (uuid) yes Shared by every language version of one article, so the versions can be found together. An article with no translations is alone in its group.
article.generationConfig object | null yes The settings an AI generation was started with (topic, tone, length, image style, ...), kept so the article can be rerun. null for an article created without them.
article.generationConfig.topic string yes What the article is about (1 to 500 characters). The outline step plans the article from it, and with primaryKeyword it is the query for web research and for the project's knowledge base. The article's title is not copied from it: the outline's title, written by the model, becomes the title.
article.generationConfig.prompt string no Free-form direction for the writer, up to 5000 characters (angle, points to cover, things to avoid). Sent to the outline step as 'Additional direction'; the section writers follow the outline it produces.
article.generationConfig.primaryKeyword string no The keyword the article targets, up to 200 characters. The outline step is told to use it where it reads naturally, and it joins topic in the research and knowledge-base queries. It is not copied into the article's targetKeywords.
article.generationConfig.secondaryKeywords string[] yes Supporting keywords, up to 25 of 1 to 200 characters. Sent to the outline step, which spreads them over the sections' keywords. Empty by default. Default: [].
article.generationConfig.audience string no Who the article is for, up to 500 characters. Sent to the outline step, to every section writer, to the image-prompt writers and to the knowledge-base query.
article.generationConfig.tone string no The tone to write in, as free text up to 200 characters; the web app offers professional, conversational, authoritative, practical and persuasive. Sent to the outline step as 'Tone'. The section writers read no separate tone: they follow the outline and the brand voice.
article.generationConfig.language string yes Language the article is written in, as a language tag such as en or pt-BR (default en). The outline, section, research and image-prompt steps use it, so the text and the images' alt text come out in it. A run does not change the article's own primaryLanguage. Default: "en".
article.generationConfig.wordCount integer yes Target length of the whole article in words, 200 to 10000 (default 1200). The outline's sections are planned to add up to roughly this. It also sets the text price per started 1,000 words: 2 credits with writingQuality premium, 1 with standard (1200 costs 4 or 2), and the image count when imageCount is null. Default: 1200.
article.generationConfig.imageStyle "photorealistic" | "editorial" | "minimalist-vector" | "3d-render" | "watercolor" | "corporate-tech" | "cinematic" | "illustration" yes Style of the article's images (default editorial). It applies to the cover and the inline images unless the project's imageSettings set a hero.style or inline.style, which take precedence. Values: photorealistic (natural-light documentary photograph, realistic textures, no stylisation), editorial (modern editorial illustration or photograph, clean composition, restrained palette), minimalist-vector (flat vector illustration, few shapes, solid colours, plenty of empty space), 3d-render (soft studio 3D render, smooth matte materials, simple forms), watercolor (loose watercolour painting, soft edges, muted washes), corporate-tech (clean technology visual in cool blues and greys, geometric shapes), cinematic (cinematic still, dramatic lighting, strong atmosphere, wide framing), illustration (hand-drawn illustration, confident linework, warm friendly palette). Default: "editorial".
article.generationConfig.imageCount integer | null yes How many images to make, cover included, 0 to 20. 0 makes none, 1 only the cover, more add one inline image per section up to the number of outline sections. null (the default) derives it from wordCount: 2 under 1,000 words, 3 under 2,000, 4 under 3,000, otherwise 5. Each image delivered costs 1 credit, or 3 when imageQuality is hd; the quote and the reservation count the full number. Default: null.
article.generationConfig.writingQuality "standard" | "premium" yes Which model writes the article (default premium). premium uses the deployment's article model and costs 2 credits per started 1,000 words; standard uses a lighter, cheaper model and costs 1 credit per started 1,000 words. Default: "premium".
article.generationConfig.imageQuality "standard" | "hd" yes Quality of the article's images (default standard). standard costs 1 credit per image; hd asks the image model for its highest quality and costs 3 credits per image, for the run's images and for each regeneration of an image of the article. Default: "standard".
article.generationConfig.brandVoiceId string (uuid) no Id of the brand voice to write in. Absent, or an id that is not one of the workspace's voices, the project's default voice applies, then the workspace's default voice; with neither, no voice is applied.
article.generationConfig.contentTemplateId string (uuid) no Id of the content template to follow: the outline must contain its sections, and the last section ends on its call to action. Only built-in templates and the workspace's own count; any other id, or a deleted template, is ignored without an error.
article.generationConfig.sourceUrl string (uri) no A reference URL for the article. It is stored with the configuration; no generation step reads or fetches it.
article.generationConfig.researchEnabled boolean yes Whether to research and fact-check (default false). Research looks up sources before the outline (needs the deployment's web_research feature); fact checking verifies the finished article's claims (needs fact_checking). Research adds 1 credit and fact checking 3 to the quote, each only when its feature is on, and is charged only if it delivered: research found a source, fact checking checked a claim. Default: false.
article.generationConfig.reviewOutline boolean yes When true the run stops after the outline (default false): the job ends COMPLETED, the article stays DRAFT with the outline stored, and only research, if any, is charged. Read it with GET /api/articles/{articleId}/outline, edit it, then POST /api/articles/{articleId}/outline/approve to write the article. Default: false.
article.generationConfig.faq boolean yes When true (default false) the article ends with a "Frequently asked questions" section of 3 to 5 questions a reader still has after the body, each answered in two or three sentences, in the article's language. It is written after the sections as one more model call, and its words come out of wordCount: the outline is planned for 250 words fewer, so the price does not change. The section is ordinary content (an H2, then an H3 and a paragraph per question) that can be edited like any other. Default: false.
article.outline object | null yes The outline the AI pipeline planned the article from, kept so a writing step can be retried (and, with reviewOutline, the one a person approved). null for an article that was not generated.
article.outline.title string yes The article title, 1 to 300 characters.
article.outline.slug string yes The URL slug: lowercase letters and digits separated by hyphens, up to 120 characters.
article.outline.metaTitle string yes The SEO title, 1 to 70 characters.
article.outline.metaDescription string yes The SEO description, 1 to 200 characters.
article.outline.sections object[] yes The sections in reading order, 1 to 40.
article.outline.sections[].id string yes Identifier of the section inside the outline, 1 to 64 characters.
article.outline.sections[].heading string yes The section's heading, 1 to 300 characters.
article.outline.sections[].level 2 | 3 yes Heading rank: 2 for a main section, 3 for a sub-section.
article.outline.sections[].purpose string yes What the section covers, 1 to 1000 characters.
article.outline.sections[].keywords string[] yes The keywords attached to the section, at most 25 of 1 to 120 characters. Empty: none. Default: [].
article.outline.sections[].estimatedWords integer yes The section's length in words, between 20 and 5000.
article.createdBy string | null yes Id of the user who created the article (for an article made with an API key, the key's creator). null when no user is attached or that user has been deleted.
article.publishedAt string (date-time) | null yes The article's publication date: when it was published, or the date given at creation or publish for an imported article. null until then; unpublishing keeps it.
article.briefId string (uuid) | null yes Id of the stored content brief the article was written from; null when none.
article.publishedUrl string | null yes The address the publishing integration reported for the live article, which links from other articles point to. null until a publish reports one, and for a webhook target, which never does.
article.wordCount integer | null yes Words in documentJson, stored with every document write; null for an article not counted since the count was added, until the backfill reaches it.
article.createdAt string (date-time) yes When the article was created.
article.updatedAt string (date-time) yes When the article last changed (a save, a status move, a re-render). It is what expectedUpdatedAt is compared with on PATCH /api/articles/{articleId}.
article.categories object[] yes The categories assigned to the article, in the order they were assigned.
article.categories[].id string (uuid) yes Id of the category.
article.categories[].primaryLanguage string yes Language tag of the category's main name.
article.categories[].translations object[] yes The category's name in each language it has.
article.categories[].translations[].language string yes Language tag of this name.
article.categories[].translations[].name string yes The category's name in language.
article.categories[].translations[].slug string yes URL-safe form of name, unique among the workspace's categories in language.
assets object[] yes Every image of the article, the hero included. Storage paths are never exposed: publicUrl is what an image block resolves to.
assets[].id string (uuid) yes Id of the asset; an image block's assetId refers to it.
assets[].type "hero" | "inline" yes hero: the article's cover image (one per article, kept out of the body). inline: an image shown in the body by an image block.
assets[].publicUrl string yes The public URL the image is served from.
assets[].altText string yes Alternative text of the image.
assets[].prompt string yes The text the image was generated from. Empty for a picture that was uploaded, fetched from a URL or imported.
assets[].style string | null yes The image style used to generate it; null for a picture that was not generated.
assets[].width integer yes Width of the stored image in pixels.
assets[].height integer yes Height of the stored image in pixels.
blogPath string | null yes Where the article can be read on the PrismDraft site blog, e.g. /en/blog/my-post. null for an article of any other project, or while it is not PUBLISHED.
translations object yes The article's language versions and the languages its project supports.
translations.languages string[] yes The project's languages as tags, the project's default language first.
translations.versions object[] yes Every language version of the article, the article itself included, oldest first.
translations.versions[].id string (uuid) yes Id of this language version, an article of its own.
translations.versions[].title string yes This version's title.
translations.versions[].slug string yes This version's slug, unique within its language.
translations.versions[].status "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes Where the article is in its life. DRAFT: being written or edited, and what an unpublished article goes back to. QUEUED and PROCESSING: owned by an AI job (waiting, then running); nothing but the job moves it out. NEEDS_REVIEW: submitted, waiting for a decision. CHANGES_REQUESTED: sent back by a reviewer (reject or request changes). APPROVED: approved and ready to publish. PUBLISHED: published. Only the workflow routes change it, and a move the state machine does not allow answers 409.
translations.versions[].primaryLanguage string yes Language tag this version is written in.
translations.versions[].updatedAt string (date-time) yes When this version last changed.
translations.versions[].publishedAt string (date-time) | null yes This version's publication date; null until it is published.
translations.versions[].retryable boolean yes true for the empty draft a failed or cancelled translation leaves behind: a new translation request reuses it instead of refusing. Such a version blocks a group transition until it is translated or removed.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.get({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.update(input, init?)

Update an article

PATCH /api/articles/{articleId}

Changes the fields sent and leaves the others as they are; the status is never changed here (see POST /submit, /approve, /publish). Sending document replaces the whole body, writes a new version (source human) and re-renders contentHtml and contentMarkdown; a save without it writes no version. Needs articles.update. 409 CONFLICT when the slug or the language is taken, or when expectedUpdatedAt no longer matches (details.reason article_changed, with the current details.updatedAt, so a client can offer to reload); nothing is saved then. 400 when an image block points at the article's hero. For a PUBLISHED article that changed its title, content, slug, meta fields or keywords, the article.updated webhook is sent with the names of the changed fields; a draft sends none. Writes an articles.updated audit entry.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
body body object no
body.title body string no The article's new title, 1 to 300 characters. Absent: unchanged.
body.slug body string no The new slug, normalised like POST /api/articles does (400 when nothing usable remains). 409 when another article of the project in the same language already uses it. When the article has been published, its old slug keeps redirecting to it. Absent: unchanged.
body.metaTitle body string | null no The SEO title, at most 120 characters. null clears it; absent: unchanged.
body.metaDescription body string | null no The SEO description, at most 320 characters. null clears it; absent: unchanged.
body.targetKeywords body string[] no Replaces the target keywords: at most 25, each 1 to 200 characters. An empty array clears them; absent: unchanged.
body.secondaryKeywords body string[] no Replaces the secondary keywords: at most 25, each 1 to 200 characters. An empty array clears them; absent: unchanged.
body.primaryLanguage body string no Moves the article to another language of its project (400 when the project does not have it; 409 when another version of the same translation group already is in it). The slug is then checked in that language. Absent: unchanged.
body.document body object no Replaces the whole document (send every block, not a patch). It writes a new version, re-renders contentHtml and contentMarkdown and recounts the words. An image block may not point at the article's hero (400). Absent: the document is unchanged and no version is written.
body.document.version body 1 yes Version of the document format. Always 1.
body.document.blocks body (object<heading> | object<paragraph> | object<list> | object<quote> | object<image> | object<code> | object<table> | object<divider>)[] yes The article body, top to bottom. Each block's type says which of heading, paragraph, list, quote, image, code, table and divider it is and which other fields it carries. An empty array is an empty article.
body.document.blocks[]<heading>.id body string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
body.document.blocks[]<heading>.type body "heading" yes Block kind: always heading.
body.document.blocks[]<heading>.level body 1 | 2 | 3 | 4 | 5 | 6 yes Heading rank, 1 (<h1>) to 6 (<h6>). The article's title is its own field and is printed above the body, so generated bodies carry no level-1 heading; one at the very start is part of the lead that stripLead removes.
body.document.blocks[]<heading>.text body string yes The heading's words.
body.document.blocks[]<heading>.format body "markdown" no Absent: the heading is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
body.document.blocks[]<paragraph>.id body string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
body.document.blocks[]<paragraph>.type body "paragraph" yes Block kind: always paragraph.
body.document.blocks[]<paragraph>.text body string yes The paragraph's words.
body.document.blocks[]<paragraph>.links body object[] no Links from words of text to other articles, at most 20. Only the first link to each article in the whole document is kept (the rest are dropped on save). Absent: none. Ignored when format is "markdown": write the links in the text instead.
body.document.blocks[]<paragraph>.links[].text body string yes The words of the block's text that become the link, 1 to 200 characters. Matched literally, first occurrence only; a link whose words no longer occur in the block is dropped on the next save.
body.document.blocks[]<paragraph>.links[].articleId body string (uuid) yes Id of the article the words link to. It is resolved when the article is rendered: the words stay plain text until that article is PUBLISHED in the same project and has an address (its blog page, or the URL its publishing integration reported).
body.document.blocks[]<paragraph>.format body "markdown" no Absent: the paragraph is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
body.document.blocks[]<list>.id body string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
body.document.blocks[]<list>.type body "list" yes Block kind: always list.
body.document.blocks[]<list>.ordered body boolean yes true: a numbered list. false: a bulleted list.
body.document.blocks[]<list>.items body string[] yes The top-level items, as text. A reader that ignores children still sees every top-level item.
body.document.blocks[]<list>.children body (object | null)[] no Optional, one entry per item of items (an entry count that differs from items is refused): the level 2 sub-list under that item, or null for an item with none. Lists nest to four levels.
body.document.blocks[]<list>.children[].ordered body boolean yes true: a numbered sub-list. false: a bulleted one. Independent of the parent list.
body.document.blocks[]<list>.children[].items body string[] yes The sub-list's items, at least one, as text.
body.document.blocks[]<list>.children[].children body (object | null)[] no Optional, one entry per item of this sub-list (an entry count that differs from items is refused): the level 3 sub-list under that item, or null for an item with none. Same shape as body.document.blocks[]<list>.children[]: see above.
body.document.blocks[]<list>.format body "markdown" no Absent: every item of the list, at every level, is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
body.document.blocks[]<quote>.id body string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
body.document.blocks[]<quote>.type body "quote" yes Block kind: always quote.
body.document.blocks[]<quote>.text body string yes The quoted words.
body.document.blocks[]<quote>.attribution body string no Who said it, always plain text. Absent: the quote is unattributed.
body.document.blocks[]<quote>.links body object[] no Links from words of text to other articles, at most 20. Only the first link to each article in the whole document is kept (the rest are dropped on save). Absent: none. Ignored when format is "markdown": write the links in the text instead.
body.document.blocks[]<quote>.links[].text body string yes The words of the block's text that become the link, 1 to 200 characters. Matched literally, first occurrence only; a link whose words no longer occur in the block is dropped on the next save.
body.document.blocks[]<quote>.links[].articleId body string (uuid) yes Id of the article the words link to. It is resolved when the article is rendered: the words stay plain text until that article is PUBLISHED in the same project and has an address (its blog page, or the URL its publishing integration reported).
body.document.blocks[]<quote>.format body "markdown" no Absent: the quoted text is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
body.document.blocks[]<image>.id body string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
body.document.blocks[]<image>.type body "image" yes Block kind: always image.
body.document.blocks[]<image>.assetId body string (uuid) yes Id of the article's asset the block shows (assets[].id of GET /api/articles/{articleId}). The asset's address is resolved when the article is rendered; a block whose asset is missing renders as nothing. The article's hero asset is its cover and cannot be added to the body.
body.document.blocks[]<image>.altText body string yes Alternative text for the image. When empty, the asset's own alternative text is used when the article is rendered.
body.document.blocks[]<image>.caption body string no Plain-text caption printed under the image. Absent: no caption.
body.document.blocks[]<code>.id body string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
body.document.blocks[]<code>.type body "code" yes Block kind: always code.
body.document.blocks[]<code>.language body string no The language of the code (the fence's info string), at most 40 characters, e.g. ts. Renderers keep only the characters A-Z a-z 0-9 _ + # . -. Absent: unspecified.
body.document.blocks[]<code>.text body string yes The code, verbatim: never interpreted as Markdown and never translated.
body.document.blocks[]<table>.id body string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
body.document.blocks[]<table>.type body "table" yes Block kind: always table.
body.document.blocks[]<table>.header body string[] yes The header row: one cell of text per column, at least one.
body.document.blocks[]<table>.rows body string[][] yes The body rows, each an array of cell text with exactly as many cells as header (otherwise refused). May be empty.
body.document.blocks[]<table>.align body ("left" | "center" | "right" | null)[] no One alignment per column, so as many entries as header has cells (otherwise refused); null leaves a column to the browser's default. Absent: all default.
body.document.blocks[]<table>.format body "markdown" no Absent: every cell, header included, is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
body.document.blocks[]<divider>.id body string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
body.document.blocks[]<divider>.type body "divider" yes Block kind: always divider.
body.expectedUpdatedAt body string (date-time) no The updatedAt the edit was based on, as an ISO 8601 UTC date-time, compared at millisecond precision. When it no longer matches the article's, nothing is saved and the answer is 409 CONFLICT with details.reason article_changed and the current details.updatedAt. Absent: the save overwrites whatever is there.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
article object yes The article as stored after the save.
article.id string (uuid) yes Id of the article.
article.workspaceId string (uuid) yes Id of the workspace that owns the article.
article.projectId string (uuid) yes Id of the project the article belongs to.
article.title string yes The article's title, printed above its body.
article.slug string yes The article's address segment, unique among the project's articles of the same language. Renaming an article that has been published keeps its old slug as a redirect.
article.status "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes Where the article is in its life. DRAFT: being written or edited, and what an unpublished article goes back to. QUEUED and PROCESSING: owned by an AI job (waiting, then running); nothing but the job moves it out. NEEDS_REVIEW: submitted, waiting for a decision. CHANGES_REQUESTED: sent back by a reviewer (reject or request changes). APPROVED: approved and ready to publish. PUBLISHED: published. Only the workflow routes change it, and a move the state machine does not allow answers 409.
article.documentJson object yes The canonical article body as typed blocks. contentHtml and contentMarkdown are rendered from it on every save, so edit this and never them.
article.documentJson.version 1 yes Version of the document format. Always 1.
article.documentJson.blocks (object<heading> | object<paragraph> | object<list> | object<quote> | object<image> | object<code> | object<table> | object<divider>)[] yes The article body, top to bottom. Each block's type says which of heading, paragraph, list, quote, image, code, table and divider it is and which other fields it carries. An empty array is an empty article.
article.documentJson.blocks[]<heading>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<heading>.type "heading" yes Block kind: always heading.
article.documentJson.blocks[]<heading>.level 1 | 2 | 3 | 4 | 5 | 6 yes Heading rank, 1 (<h1>) to 6 (<h6>). The article's title is its own field and is printed above the body, so generated bodies carry no level-1 heading; one at the very start is part of the lead that stripLead removes.
article.documentJson.blocks[]<heading>.text string yes The heading's words.
article.documentJson.blocks[]<heading>.format "markdown" no Absent: the heading is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<paragraph>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<paragraph>.type "paragraph" yes Block kind: always paragraph.
article.documentJson.blocks[]<paragraph>.text string yes The paragraph's words.
article.documentJson.blocks[]<paragraph>.links object[] no Links from words of text to other articles, at most 20. Only the first link to each article in the whole document is kept (the rest are dropped on save). Absent: none. Ignored when format is "markdown": write the links in the text instead.
article.documentJson.blocks[]<paragraph>.links[].text string yes The words of the block's text that become the link, 1 to 200 characters. Matched literally, first occurrence only; a link whose words no longer occur in the block is dropped on the next save.
article.documentJson.blocks[]<paragraph>.links[].articleId string (uuid) yes Id of the article the words link to. It is resolved when the article is rendered: the words stay plain text until that article is PUBLISHED in the same project and has an address (its blog page, or the URL its publishing integration reported).
article.documentJson.blocks[]<paragraph>.format "markdown" no Absent: the paragraph is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<list>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<list>.type "list" yes Block kind: always list.
article.documentJson.blocks[]<list>.ordered boolean yes true: a numbered list. false: a bulleted list.
article.documentJson.blocks[]<list>.items string[] yes The top-level items, as text. A reader that ignores children still sees every top-level item.
article.documentJson.blocks[]<list>.children (object | null)[] no Optional, one entry per item of items (an entry count that differs from items is refused): the level 2 sub-list under that item, or null for an item with none. Lists nest to four levels.
article.documentJson.blocks[]<list>.children[].ordered boolean yes true: a numbered sub-list. false: a bulleted one. Independent of the parent list.
article.documentJson.blocks[]<list>.children[].items string[] yes The sub-list's items, at least one, as text.
article.documentJson.blocks[]<list>.children[].children (object | null)[] no Optional, one entry per item of this sub-list (an entry count that differs from items is refused): the level 3 sub-list under that item, or null for an item with none. Same shape as article.documentJson.blocks[]<list>.children[]: see above.
article.documentJson.blocks[]<list>.format "markdown" no Absent: every item of the list, at every level, is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<quote>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<quote>.type "quote" yes Block kind: always quote.
article.documentJson.blocks[]<quote>.text string yes The quoted words.
article.documentJson.blocks[]<quote>.attribution string no Who said it, always plain text. Absent: the quote is unattributed.
article.documentJson.blocks[]<quote>.links object[] no Links from words of text to other articles, at most 20. Only the first link to each article in the whole document is kept (the rest are dropped on save). Absent: none. Ignored when format is "markdown": write the links in the text instead.
article.documentJson.blocks[]<quote>.links[].text string yes The words of the block's text that become the link, 1 to 200 characters. Matched literally, first occurrence only; a link whose words no longer occur in the block is dropped on the next save.
article.documentJson.blocks[]<quote>.links[].articleId string (uuid) yes Id of the article the words link to. It is resolved when the article is rendered: the words stay plain text until that article is PUBLISHED in the same project and has an address (its blog page, or the URL its publishing integration reported).
article.documentJson.blocks[]<quote>.format "markdown" no Absent: the quoted text is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<image>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<image>.type "image" yes Block kind: always image.
article.documentJson.blocks[]<image>.assetId string (uuid) yes Id of the article's asset the block shows (assets[].id of GET /api/articles/{articleId}). The asset's address is resolved when the article is rendered; a block whose asset is missing renders as nothing. The article's hero asset is its cover and cannot be added to the body.
article.documentJson.blocks[]<image>.altText string yes Alternative text for the image. When empty, the asset's own alternative text is used when the article is rendered.
article.documentJson.blocks[]<image>.caption string no Plain-text caption printed under the image. Absent: no caption.
article.documentJson.blocks[]<code>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<code>.type "code" yes Block kind: always code.
article.documentJson.blocks[]<code>.language string no The language of the code (the fence's info string), at most 40 characters, e.g. ts. Renderers keep only the characters A-Z a-z 0-9 _ + # . -. Absent: unspecified.
article.documentJson.blocks[]<code>.text string yes The code, verbatim: never interpreted as Markdown and never translated.
article.documentJson.blocks[]<table>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<table>.type "table" yes Block kind: always table.
article.documentJson.blocks[]<table>.header string[] yes The header row: one cell of text per column, at least one.
article.documentJson.blocks[]<table>.rows string[][] yes The body rows, each an array of cell text with exactly as many cells as header (otherwise refused). May be empty.
article.documentJson.blocks[]<table>.align ("left" | "center" | "right" | null)[] no One alignment per column, so as many entries as header has cells (otherwise refused); null leaves a column to the browser's default. Absent: all default.
article.documentJson.blocks[]<table>.format "markdown" no Absent: every cell, header included, is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<divider>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<divider>.type "divider" yes Block kind: always divider.
article.contentHtml string | null yes The body rendered as HTML from documentJson, with image blocks resolved to their public URLs. Read-only: re-rendered on every document save and again on publish. null before the first render.
article.contentMarkdown string | null yes The body rendered as Markdown from documentJson, with image blocks resolved to their public URLs. Read-only: re-rendered like contentHtml. null before the first render. On GET /api/articles/{articleId}, stripLead=true returns it without the lead.
article.metaTitle string | null yes The SEO title for search results and link previews; null when none is set.
article.metaDescription string | null yes The SEO description for search results and link previews; null when none is set.
article.targetKeywords string[] yes The keywords the article is written to rank for, in the order given.
article.secondaryKeywords string[] yes Supporting keywords to work into the article; empty when none.
article.primaryLanguage string yes Language tag the article is written in, e.g. en or pt-BR; one of the project's languages.
article.translationGroupId string (uuid) yes Shared by every language version of one article, so the versions can be found together. An article with no translations is alone in its group.
article.generationConfig object | null yes The settings an AI generation was started with (topic, tone, length, image style, ...), kept so the article can be rerun. null for an article created without them.
article.generationConfig.topic string yes What the article is about (1 to 500 characters). The outline step plans the article from it, and with primaryKeyword it is the query for web research and for the project's knowledge base. The article's title is not copied from it: the outline's title, written by the model, becomes the title.
article.generationConfig.prompt string no Free-form direction for the writer, up to 5000 characters (angle, points to cover, things to avoid). Sent to the outline step as 'Additional direction'; the section writers follow the outline it produces.
article.generationConfig.primaryKeyword string no The keyword the article targets, up to 200 characters. The outline step is told to use it where it reads naturally, and it joins topic in the research and knowledge-base queries. It is not copied into the article's targetKeywords.
article.generationConfig.secondaryKeywords string[] yes Supporting keywords, up to 25 of 1 to 200 characters. Sent to the outline step, which spreads them over the sections' keywords. Empty by default. Default: [].
article.generationConfig.audience string no Who the article is for, up to 500 characters. Sent to the outline step, to every section writer, to the image-prompt writers and to the knowledge-base query.
article.generationConfig.tone string no The tone to write in, as free text up to 200 characters; the web app offers professional, conversational, authoritative, practical and persuasive. Sent to the outline step as 'Tone'. The section writers read no separate tone: they follow the outline and the brand voice.
article.generationConfig.language string yes Language the article is written in, as a language tag such as en or pt-BR (default en). The outline, section, research and image-prompt steps use it, so the text and the images' alt text come out in it. A run does not change the article's own primaryLanguage. Default: "en".
article.generationConfig.wordCount integer yes Target length of the whole article in words, 200 to 10000 (default 1200). The outline's sections are planned to add up to roughly this. It also sets the text price per started 1,000 words: 2 credits with writingQuality premium, 1 with standard (1200 costs 4 or 2), and the image count when imageCount is null. Default: 1200.
article.generationConfig.imageStyle "photorealistic" | "editorial" | "minimalist-vector" | "3d-render" | "watercolor" | "corporate-tech" | "cinematic" | "illustration" yes Style of the article's images (default editorial). It applies to the cover and the inline images unless the project's imageSettings set a hero.style or inline.style, which take precedence. Values: photorealistic (natural-light documentary photograph, realistic textures, no stylisation), editorial (modern editorial illustration or photograph, clean composition, restrained palette), minimalist-vector (flat vector illustration, few shapes, solid colours, plenty of empty space), 3d-render (soft studio 3D render, smooth matte materials, simple forms), watercolor (loose watercolour painting, soft edges, muted washes), corporate-tech (clean technology visual in cool blues and greys, geometric shapes), cinematic (cinematic still, dramatic lighting, strong atmosphere, wide framing), illustration (hand-drawn illustration, confident linework, warm friendly palette). Default: "editorial".
article.generationConfig.imageCount integer | null yes How many images to make, cover included, 0 to 20. 0 makes none, 1 only the cover, more add one inline image per section up to the number of outline sections. null (the default) derives it from wordCount: 2 under 1,000 words, 3 under 2,000, 4 under 3,000, otherwise 5. Each image delivered costs 1 credit, or 3 when imageQuality is hd; the quote and the reservation count the full number. Default: null.
article.generationConfig.writingQuality "standard" | "premium" yes Which model writes the article (default premium). premium uses the deployment's article model and costs 2 credits per started 1,000 words; standard uses a lighter, cheaper model and costs 1 credit per started 1,000 words. Default: "premium".
article.generationConfig.imageQuality "standard" | "hd" yes Quality of the article's images (default standard). standard costs 1 credit per image; hd asks the image model for its highest quality and costs 3 credits per image, for the run's images and for each regeneration of an image of the article. Default: "standard".
article.generationConfig.brandVoiceId string (uuid) no Id of the brand voice to write in. Absent, or an id that is not one of the workspace's voices, the project's default voice applies, then the workspace's default voice; with neither, no voice is applied.
article.generationConfig.contentTemplateId string (uuid) no Id of the content template to follow: the outline must contain its sections, and the last section ends on its call to action. Only built-in templates and the workspace's own count; any other id, or a deleted template, is ignored without an error.
article.generationConfig.sourceUrl string (uri) no A reference URL for the article. It is stored with the configuration; no generation step reads or fetches it.
article.generationConfig.researchEnabled boolean yes Whether to research and fact-check (default false). Research looks up sources before the outline (needs the deployment's web_research feature); fact checking verifies the finished article's claims (needs fact_checking). Research adds 1 credit and fact checking 3 to the quote, each only when its feature is on, and is charged only if it delivered: research found a source, fact checking checked a claim. Default: false.
article.generationConfig.reviewOutline boolean yes When true the run stops after the outline (default false): the job ends COMPLETED, the article stays DRAFT with the outline stored, and only research, if any, is charged. Read it with GET /api/articles/{articleId}/outline, edit it, then POST /api/articles/{articleId}/outline/approve to write the article. Default: false.
article.generationConfig.faq boolean yes When true (default false) the article ends with a "Frequently asked questions" section of 3 to 5 questions a reader still has after the body, each answered in two or three sentences, in the article's language. It is written after the sections as one more model call, and its words come out of wordCount: the outline is planned for 250 words fewer, so the price does not change. The section is ordinary content (an H2, then an H3 and a paragraph per question) that can be edited like any other. Default: false.
article.outline object | null yes The outline the AI pipeline planned the article from, kept so a writing step can be retried (and, with reviewOutline, the one a person approved). null for an article that was not generated.
article.outline.title string yes The article title, 1 to 300 characters.
article.outline.slug string yes The URL slug: lowercase letters and digits separated by hyphens, up to 120 characters.
article.outline.metaTitle string yes The SEO title, 1 to 70 characters.
article.outline.metaDescription string yes The SEO description, 1 to 200 characters.
article.outline.sections object[] yes The sections in reading order, 1 to 40.
article.outline.sections[].id string yes Identifier of the section inside the outline, 1 to 64 characters.
article.outline.sections[].heading string yes The section's heading, 1 to 300 characters.
article.outline.sections[].level 2 | 3 yes Heading rank: 2 for a main section, 3 for a sub-section.
article.outline.sections[].purpose string yes What the section covers, 1 to 1000 characters.
article.outline.sections[].keywords string[] yes The keywords attached to the section, at most 25 of 1 to 120 characters. Empty: none. Default: [].
article.outline.sections[].estimatedWords integer yes The section's length in words, between 20 and 5000.
article.createdBy string | null yes Id of the user who created the article (for an article made with an API key, the key's creator). null when no user is attached or that user has been deleted.
article.publishedAt string (date-time) | null yes The article's publication date: when it was published, or the date given at creation or publish for an imported article. null until then; unpublishing keeps it.
article.briefId string (uuid) | null yes Id of the stored content brief the article was written from; null when none.
article.publishedUrl string | null yes The address the publishing integration reported for the live article, which links from other articles point to. null until a publish reports one, and for a webhook target, which never does.
article.wordCount integer | null yes Words in documentJson, stored with every document write; null for an article not counted since the count was added, until the backfill reaches it.
article.createdAt string (date-time) yes When the article was created.
article.updatedAt string (date-time) yes When the article last changed (a save, a status move, a re-render). It is what expectedUpdatedAt is compared with on PATCH /api/articles/{articleId}.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.update({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.delete(input, init?)

Delete an article

DELETE /api/articles/{articleId}

Permanently deletes the article with its versions, comments, reviews and image records, and removes the stored image files nothing else uses. Other language versions of the article are not touched. Needs articles.delete. Deleting a PUBLISHED article sends the article.unpublished webhook with reason: "deleted"; an articles.deleted audit entry is written. There is no undo.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
ok true yes Always true: the article is deleted.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.delete({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.submit(input, init?)

Submit an article for review

POST /api/articles/{articleId}/submit

Moves the article to NEEDS_REVIEW so a reviewer can decide on it. Needs articles.update. 409 CONFLICT when the article's status cannot move there, for example when it is already NEEDS_REVIEW, is QUEUED, or is PUBLISHED. Queues an email to every member who holds articles.approve and can open the project (not the caller, and not anyone who opted out), and updates open dashboards live.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
status "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes The status the article moved to: NEEDS_REVIEW.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.submit({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.approve(input, init?)

Approve an article

POST /api/articles/{articleId}/approve

Records an approved review and moves the article from NEEDS_REVIEW to APPROVED, ready for POST /publish; 409 CONFLICT from any other status. Needs articles.approve. The body is optional and may carry a comment. Sends the article.approved webhook, writes an articles.approved audit entry and queues an email to the article's author (unless that is the caller).

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
body body object no
body.comment body string no The reviewer's note, at most 4000 characters, recorded on the review (trimmed). Optional for an approval; required, and not blank, to reject or request changes, where it is also put in the email to the author.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
review object yes The decision just recorded.
review.id string (uuid) yes Id of the review record.
review.articleId string (uuid) yes Id of the article that was reviewed.
review.reviewerId string | null yes Id of the user who decided. null for a decision made with an API key, or once that user has been deleted.
review.decision "approved" | "rejected" | "changes_requested" yes The decision. approved: the article moves to APPROVED. rejected and changes_requested: both move it to CHANGES_REQUESTED and need a comment; the webhook article.rejected carries which of the two it was.
review.comment string | null yes The reviewer's note, trimmed. Always present for rejected and changes_requested; null for an approval that came without one.
review.previousStatus "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes The article's status just before the decision.
review.newStatus "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes The article's status after the decision: APPROVED or CHANGES_REQUESTED.
review.automated boolean yes true when the decision was made by a plan's autopilot rather than by a person; no one reviewed it and reviewerId is null.
review.createdAt string (date-time) yes When the decision was recorded.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.approve({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.reject(input, init?)

Record a review decision: rejected

POST /api/articles/{articleId}/reject

Records a rejected review and moves the article to CHANGES_REQUESTED, from NEEDS_REVIEW, APPROVED or PUBLISHED (409 CONFLICT from any other status). A non-blank comment is required (400 otherwise), since a bare decision gives the writer nothing to act on. Needs articles.reject. Sends the article.rejected webhook, which names the decision in decision, and writes an articles.rejected audit entry. A PUBLISHED article that is sent back also sends article.unpublished (reason: "status_changed"). Queues an email with the comment to the article's author (unless that is the caller).

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
body body object no
body.comment body string no The reviewer's note, at most 4000 characters, recorded on the review (trimmed). Optional for an approval; required, and not blank, to reject or request changes, where it is also put in the email to the author.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
review object yes The decision just recorded.
review.id string (uuid) yes Id of the review record.
review.articleId string (uuid) yes Id of the article that was reviewed.
review.reviewerId string | null yes Id of the user who decided. null for a decision made with an API key, or once that user has been deleted.
review.decision "approved" | "rejected" | "changes_requested" yes The decision. approved: the article moves to APPROVED. rejected and changes_requested: both move it to CHANGES_REQUESTED and need a comment; the webhook article.rejected carries which of the two it was.
review.comment string | null yes The reviewer's note, trimmed. Always present for rejected and changes_requested; null for an approval that came without one.
review.previousStatus "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes The article's status just before the decision.
review.newStatus "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes The article's status after the decision: APPROVED or CHANGES_REQUESTED.
review.automated boolean yes true when the decision was made by a plan's autopilot rather than by a person; no one reviewed it and reviewerId is null.
review.createdAt string (date-time) yes When the decision was recorded.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.reject({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.requestChanges(input, init?)

Record a review decision: changes_requested

POST /api/articles/{articleId}/request-changes

Records a changes_requested review and moves the article to CHANGES_REQUESTED, from NEEDS_REVIEW, APPROVED or PUBLISHED (409 CONFLICT from any other status). A non-blank comment is required (400 otherwise), since a bare decision gives the writer nothing to act on. Needs articles.reject. Sends the article.rejected webhook, which names the decision in decision, and writes an articles.changes_requested audit entry. A PUBLISHED article that is sent back also sends article.unpublished (reason: "status_changed"). Queues an email with the comment to the article's author (unless that is the caller).

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
body body object no
body.comment body string no The reviewer's note, at most 4000 characters, recorded on the review (trimmed). Optional for an approval; required, and not blank, to reject or request changes, where it is also put in the email to the author.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
review object yes The decision just recorded.
review.id string (uuid) yes Id of the review record.
review.articleId string (uuid) yes Id of the article that was reviewed.
review.reviewerId string | null yes Id of the user who decided. null for a decision made with an API key, or once that user has been deleted.
review.decision "approved" | "rejected" | "changes_requested" yes The decision. approved: the article moves to APPROVED. rejected and changes_requested: both move it to CHANGES_REQUESTED and need a comment; the webhook article.rejected carries which of the two it was.
review.comment string | null yes The reviewer's note, trimmed. Always present for rejected and changes_requested; null for an approval that came without one.
review.previousStatus "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes The article's status just before the decision.
review.newStatus "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes The article's status after the decision: APPROVED or CHANGES_REQUESTED.
review.automated boolean yes true when the decision was made by a plan's autopilot rather than by a person; no one reviewed it and reviewerId is null.
review.createdAt string (date-time) yes When the decision was recorded.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.requestChanges({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.publish(input, init?)

Publish an approved article to its integration

POST /api/articles/{articleId}/publish

The article must be APPROVED (POST /submit, then POST /approve): 409 otherwise. Without a publishing integration it is simply marked PUBLISHED and cmsIntegration is null; with one, the hand-off happens in the background.

publishedAt (optional) stores that date instead of now, for an imported article whose original date must survive. Publishing without it stamps the current time, also over a publishedAt given at creation. Needs articles.publish.

warnings lists what the publish went through despite, and is empty when there is nothing: hero_in_body means the cover image is also a block of the body, so a site that prints the cover above the title shows it twice (POST /hero/detach-from-body removes the block).

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
body body object | null no
body.publishedAt body string (date-time) no ISO 8601 date-time stored as the article's publication date instead of now. Not in the future (60 s of skew allowed) and not before 1990. The article's publishedAt from creation is not carried over: send it again here.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
status "PUBLISHED" yes The article's new status: always PUBLISHED.
cmsIntegration string | null yes The type of the publishing integration the article was handed to, which happens in the background. null when the project has none: the article is only marked published.
warnings object[] yes What the publish went through despite. hero_in_body: the cover image is also a block of the body (an article from before the hero was kept out of it), so it shows twice where a site prints the cover above the title. Empty when there is nothing to say.
warnings[].code "hero_in_body" yes Kind of warning: hero_in_body, the only one so far.
warnings[].assetId string (uuid) yes Id of the hero asset that the body also shows.
warnings[].blockId string yes Id of the image block of the body that shows the hero; one entry per such block.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.publish({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.unpublish(input, init?)

Take a published article back to DRAFT

POST /api/articles/{articleId}/unpublish

The article must be PUBLISHED: 409 otherwise. It becomes a DRAFT and keeps its publishedAt; an article.unpublished webhook is sent (reason: "status_changed"). Nothing already handed to a publishing integration is retracted. Needs articles.publish.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
status "DRAFT" yes The article's new status: always DRAFT.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.unpublish({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.transitionTranslations(input, init?)

Submit, approve or publish every language version together

POST /api/articles/{articleId}/translations/{action}

Applies submit (to NEEDS_REVIEW), approve (to APPROVED) or publish (to PUBLISHED) to every language version of the article's translation group, each exactly as its single route would. Needs articles.update, articles.approve or articles.publish respectively. All-or-nothing: versions already at the target are skipped; if any other cannot make the transition (409 CONFLICT, details.blocked lists { id, language, status }), none moves. A version still QUEUED or PROCESSING, or an empty draft left by a failed translation, blocks the group. The versions move in one transaction, so a failure while applying leaves none of them changed. Answers which versions moved (applied) and which were skipped.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
action path "submit" | "approve" | "publish" yes The transition to apply to every language version: submit (to NEEDS_REVIEW), approve (to APPROVED) or publish (to PUBLISHED).
body body object no
body.comment body string no The reviewer's note, at most 4000 characters, recorded on the review (trimmed). Optional for an approval; required, and not blank, to reject or request changes, where it is also put in the email to the author.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
applied string (uuid)[] yes Ids of the versions that changed status.
skipped string (uuid)[] yes Ids of the versions left as they were: already at the target status, or, for unpublish, not published.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.transitionTranslations({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
  action: "submit",
});
console.log(result);

pd.articles.listReviews(input, init?)

The article's review history

GET /api/articles/{articleId}/reviews

Every review decision recorded on the article, newest first, each with the status it moved from and to and the reviewer's name. A submission is not a review and is not listed. Needs articles.read.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
reviews object[] yes Every recorded review decision, newest first.
reviews[].id string (uuid) yes Id of the review record.
reviews[].articleId string (uuid) yes Id of the article that was reviewed.
reviews[].reviewerId string | null yes Id of the user who decided. null for a decision made with an API key, or once that user has been deleted.
reviews[].decision "approved" | "rejected" | "changes_requested" yes The decision. approved: the article moves to APPROVED. rejected and changes_requested: both move it to CHANGES_REQUESTED and need a comment; the webhook article.rejected carries which of the two it was.
reviews[].comment string | null yes The reviewer's note, trimmed. Always present for rejected and changes_requested; null for an approval that came without one.
reviews[].previousStatus "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes The article's status just before the decision.
reviews[].newStatus "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes The article's status after the decision: APPROVED or CHANGES_REQUESTED.
reviews[].automated boolean yes true when the decision was made by a plan's autopilot rather than by a person; no one reviewed it and reviewerId is null.
reviews[].createdAt string (date-time) yes When the decision was recorded.
reviews[].reviewer object | null yes The reviewer's id and name; null when reviewerId is null.
reviews[].reviewer.id string yes Id of the user.
reviews[].reviewer.name string yes The user's display name.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.listReviews({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.listVersions(input, init?)

The article's version history

GET /api/articles/{articleId}/versions

The saved versions of the article's document, oldest first, without the documents (read one with GET /versions/{version}). A version is written when the article is created and by every save that changes document, including a restore; a save of the title, keywords or meta fields writes none. Needs articles.read.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
versions object[] yes Every saved version of the article's document, oldest first, without the documents. A version is written when the article is created and by every save that changes document, a restore included; a save of other fields writes none.
versions[].id string (uuid) yes Id of the version record.
versions[].version integer yes The version number, counted per article from 1. Pass it to GET /api/articles/{articleId}/versions/{version} or its restore.
versions[].changeSource "human" | "ai" | "system" | "import" | "restore" yes What produced the version. human: a save through the editor or the API (an API key included). ai: written by the generation or translation pipeline. system: an automatic correction, such as taking the hero out of the body. import: the article was created from Markdown. restore: an earlier version was restored.
versions[].createdBy string | null yes Id of the user who saved it. null for an AI or system change, a later edit made with an API key, or once that user has been deleted.
versions[].createdAt string (date-time) yes When the version was saved.
versions[].author object | null yes The author's id and name; null when createdBy is null.
versions[].author.id string yes Id of the user.
versions[].author.name string yes The user's display name.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.listVersions({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.getVersion(input, init?)

Read one version of an article

GET /api/articles/{articleId}/versions/{version}

The version's full documentJson, for preview and comparison before a restore.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
version path integer yes The version number to read or restore, counted per article from 1 (GET /api/articles/{articleId}/versions lists them). 404 when the article has no such version.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
version object yes One version of the article, whole.
version.versionNumber integer yes The version number, counted per article from 1.
version.documentJson object yes The article's document as of this version.
version.documentJson.version 1 yes Version of the document format. Always 1.
version.documentJson.blocks (object<heading> | object<paragraph> | object<list> | object<quote> | object<image> | object<code> | object<table> | object<divider>)[] yes The article body, top to bottom. Each block's type says which of heading, paragraph, list, quote, image, code, table and divider it is and which other fields it carries. An empty array is an empty article.
version.documentJson.blocks[]<heading>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
version.documentJson.blocks[]<heading>.type "heading" yes Block kind: always heading.
version.documentJson.blocks[]<heading>.level 1 | 2 | 3 | 4 | 5 | 6 yes Heading rank, 1 (<h1>) to 6 (<h6>). The article's title is its own field and is printed above the body, so generated bodies carry no level-1 heading; one at the very start is part of the lead that stripLead removes.
version.documentJson.blocks[]<heading>.text string yes The heading's words.
version.documentJson.blocks[]<heading>.format "markdown" no Absent: the heading is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
version.documentJson.blocks[]<paragraph>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
version.documentJson.blocks[]<paragraph>.type "paragraph" yes Block kind: always paragraph.
version.documentJson.blocks[]<paragraph>.text string yes The paragraph's words.
version.documentJson.blocks[]<paragraph>.links object[] no Links from words of text to other articles, at most 20. Only the first link to each article in the whole document is kept (the rest are dropped on save). Absent: none. Ignored when format is "markdown": write the links in the text instead.
version.documentJson.blocks[]<paragraph>.links[].text string yes The words of the block's text that become the link, 1 to 200 characters. Matched literally, first occurrence only; a link whose words no longer occur in the block is dropped on the next save.
version.documentJson.blocks[]<paragraph>.links[].articleId string (uuid) yes Id of the article the words link to. It is resolved when the article is rendered: the words stay plain text until that article is PUBLISHED in the same project and has an address (its blog page, or the URL its publishing integration reported).
version.documentJson.blocks[]<paragraph>.format "markdown" no Absent: the paragraph is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
version.documentJson.blocks[]<list>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
version.documentJson.blocks[]<list>.type "list" yes Block kind: always list.
version.documentJson.blocks[]<list>.ordered boolean yes true: a numbered list. false: a bulleted list.
version.documentJson.blocks[]<list>.items string[] yes The top-level items, as text. A reader that ignores children still sees every top-level item.
version.documentJson.blocks[]<list>.children (object | null)[] no Optional, one entry per item of items (an entry count that differs from items is refused): the level 2 sub-list under that item, or null for an item with none. Lists nest to four levels.
version.documentJson.blocks[]<list>.children[].ordered boolean yes true: a numbered sub-list. false: a bulleted one. Independent of the parent list.
version.documentJson.blocks[]<list>.children[].items string[] yes The sub-list's items, at least one, as text.
version.documentJson.blocks[]<list>.children[].children (object | null)[] no Optional, one entry per item of this sub-list (an entry count that differs from items is refused): the level 3 sub-list under that item, or null for an item with none. Same shape as version.documentJson.blocks[]<list>.children[]: see above.
version.documentJson.blocks[]<list>.format "markdown" no Absent: every item of the list, at every level, is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
version.documentJson.blocks[]<quote>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
version.documentJson.blocks[]<quote>.type "quote" yes Block kind: always quote.
version.documentJson.blocks[]<quote>.text string yes The quoted words.
version.documentJson.blocks[]<quote>.attribution string no Who said it, always plain text. Absent: the quote is unattributed.
version.documentJson.blocks[]<quote>.links object[] no Links from words of text to other articles, at most 20. Only the first link to each article in the whole document is kept (the rest are dropped on save). Absent: none. Ignored when format is "markdown": write the links in the text instead.
version.documentJson.blocks[]<quote>.links[].text string yes The words of the block's text that become the link, 1 to 200 characters. Matched literally, first occurrence only; a link whose words no longer occur in the block is dropped on the next save.
version.documentJson.blocks[]<quote>.links[].articleId string (uuid) yes Id of the article the words link to. It is resolved when the article is rendered: the words stay plain text until that article is PUBLISHED in the same project and has an address (its blog page, or the URL its publishing integration reported).
version.documentJson.blocks[]<quote>.format "markdown" no Absent: the quoted text is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
version.documentJson.blocks[]<image>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
version.documentJson.blocks[]<image>.type "image" yes Block kind: always image.
version.documentJson.blocks[]<image>.assetId string (uuid) yes Id of the article's asset the block shows (assets[].id of GET /api/articles/{articleId}). The asset's address is resolved when the article is rendered; a block whose asset is missing renders as nothing. The article's hero asset is its cover and cannot be added to the body.
version.documentJson.blocks[]<image>.altText string yes Alternative text for the image. When empty, the asset's own alternative text is used when the article is rendered.
version.documentJson.blocks[]<image>.caption string no Plain-text caption printed under the image. Absent: no caption.
version.documentJson.blocks[]<code>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
version.documentJson.blocks[]<code>.type "code" yes Block kind: always code.
version.documentJson.blocks[]<code>.language string no The language of the code (the fence's info string), at most 40 characters, e.g. ts. Renderers keep only the characters A-Z a-z 0-9 _ + # . -. Absent: unspecified.
version.documentJson.blocks[]<code>.text string yes The code, verbatim: never interpreted as Markdown and never translated.
version.documentJson.blocks[]<table>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
version.documentJson.blocks[]<table>.type "table" yes Block kind: always table.
version.documentJson.blocks[]<table>.header string[] yes The header row: one cell of text per column, at least one.
version.documentJson.blocks[]<table>.rows string[][] yes The body rows, each an array of cell text with exactly as many cells as header (otherwise refused). May be empty.
version.documentJson.blocks[]<table>.align ("left" | "center" | "right" | null)[] no One alignment per column, so as many entries as header has cells (otherwise refused); null leaves a column to the browser's default. Absent: all default.
version.documentJson.blocks[]<table>.format "markdown" no Absent: every cell, header included, is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
version.documentJson.blocks[]<divider>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
version.documentJson.blocks[]<divider>.type "divider" yes Block kind: always divider.
version.changeSource "human" | "ai" | "system" | "import" | "restore" yes What produced the version. human: a save through the editor or the API (an API key included). ai: written by the generation or translation pipeline. system: an automatic correction, such as taking the hero out of the body. import: the article was created from Markdown. restore: an earlier version was restored.
version.createdAt string (date-time) yes When the version was saved.
version.author object | null yes The user who saved it; null for an AI or system change, an edit made with an API key, or once that user has been deleted.
version.author.id string yes Id of the user.
version.author.name string yes The user's display name.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.getVersion({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
  version: 1,
});
console.log(result);

pd.articles.getSources(input, init?)

Research sources and fact checks behind an article

GET /api/articles/{articleId}/sources

The pages research retrieved for the article and the claims fact checking assessed against them, each oldest first. Both lists are empty for an article that was written without research. Needs articles.read.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
sources object[] yes The web pages research retrieved for the article, in the order they were retrieved. Empty when the article was generated without research.
sources[].id string (uuid) yes Id of the source.
sources[].url string yes Address of the page research retrieved.
sources[].title string yes The page's title.
sources[].excerpt string yes The stored summary of the page.
sources[].createdAt string (date-time) yes When the page was retrieved.
factChecks object[] yes One entry per claim that fact checking examined, in the order it examined them. Empty when fact checking did not run.
factChecks[].id string (uuid) yes Id of the fact check.
factChecks[].claim string yes The statement of the article that was checked.
factChecks[].verdict "verified" | "partially_verified" | "unverified" | "contradicted" yes How the retrieved sources bear on the claim. verified: they plainly support it. partially_verified: they support part of it, or with caveats. unverified: they do not address it. contradicted: they say otherwise.
factChecks[].explanation string yes The reasoning behind the verdict, up to 1000 characters; when no source could be retrieved for the claim it says so.
factChecks[].sourceUrls string[] yes The addresses of the pages the check actually read.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.getSources({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.restoreVersion(input, init?)

Restore an earlier version

POST /api/articles/{articleId}/versions/{version}/restore

Puts the document of an earlier version back as the article's body. History is not rewritten: the restore is saved as a new version (source restore) and contentHtml and contentMarkdown are re-rendered. The title, keywords and status are untouched. Needs articles.update; 404 when the version does not exist. For a PUBLISHED article whose content changed, sends the article.updated webhook. Writes an articles.version_restored audit entry.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
version path integer yes The version number to read or restore, counted per article from 1 (GET /api/articles/{articleId}/versions lists them). 404 when the article has no such version.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
article object yes The article after the restore: documentJson is the old version's, saved as a new version.
article.id string (uuid) yes Id of the article.
article.workspaceId string (uuid) yes Id of the workspace that owns the article.
article.projectId string (uuid) yes Id of the project the article belongs to.
article.title string yes The article's title, printed above its body.
article.slug string yes The article's address segment, unique among the project's articles of the same language. Renaming an article that has been published keeps its old slug as a redirect.
article.status "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes Where the article is in its life. DRAFT: being written or edited, and what an unpublished article goes back to. QUEUED and PROCESSING: owned by an AI job (waiting, then running); nothing but the job moves it out. NEEDS_REVIEW: submitted, waiting for a decision. CHANGES_REQUESTED: sent back by a reviewer (reject or request changes). APPROVED: approved and ready to publish. PUBLISHED: published. Only the workflow routes change it, and a move the state machine does not allow answers 409.
article.documentJson object yes The canonical article body as typed blocks. contentHtml and contentMarkdown are rendered from it on every save, so edit this and never them.
article.documentJson.version 1 yes Version of the document format. Always 1.
article.documentJson.blocks (object<heading> | object<paragraph> | object<list> | object<quote> | object<image> | object<code> | object<table> | object<divider>)[] yes The article body, top to bottom. Each block's type says which of heading, paragraph, list, quote, image, code, table and divider it is and which other fields it carries. An empty array is an empty article.
article.documentJson.blocks[]<heading>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<heading>.type "heading" yes Block kind: always heading.
article.documentJson.blocks[]<heading>.level 1 | 2 | 3 | 4 | 5 | 6 yes Heading rank, 1 (<h1>) to 6 (<h6>). The article's title is its own field and is printed above the body, so generated bodies carry no level-1 heading; one at the very start is part of the lead that stripLead removes.
article.documentJson.blocks[]<heading>.text string yes The heading's words.
article.documentJson.blocks[]<heading>.format "markdown" no Absent: the heading is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<paragraph>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<paragraph>.type "paragraph" yes Block kind: always paragraph.
article.documentJson.blocks[]<paragraph>.text string yes The paragraph's words.
article.documentJson.blocks[]<paragraph>.links object[] no Links from words of text to other articles, at most 20. Only the first link to each article in the whole document is kept (the rest are dropped on save). Absent: none. Ignored when format is "markdown": write the links in the text instead.
article.documentJson.blocks[]<paragraph>.links[].text string yes The words of the block's text that become the link, 1 to 200 characters. Matched literally, first occurrence only; a link whose words no longer occur in the block is dropped on the next save.
article.documentJson.blocks[]<paragraph>.links[].articleId string (uuid) yes Id of the article the words link to. It is resolved when the article is rendered: the words stay plain text until that article is PUBLISHED in the same project and has an address (its blog page, or the URL its publishing integration reported).
article.documentJson.blocks[]<paragraph>.format "markdown" no Absent: the paragraph is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<list>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<list>.type "list" yes Block kind: always list.
article.documentJson.blocks[]<list>.ordered boolean yes true: a numbered list. false: a bulleted list.
article.documentJson.blocks[]<list>.items string[] yes The top-level items, as text. A reader that ignores children still sees every top-level item.
article.documentJson.blocks[]<list>.children (object | null)[] no Optional, one entry per item of items (an entry count that differs from items is refused): the level 2 sub-list under that item, or null for an item with none. Lists nest to four levels.
article.documentJson.blocks[]<list>.children[].ordered boolean yes true: a numbered sub-list. false: a bulleted one. Independent of the parent list.
article.documentJson.blocks[]<list>.children[].items string[] yes The sub-list's items, at least one, as text.
article.documentJson.blocks[]<list>.children[].children (object | null)[] no Optional, one entry per item of this sub-list (an entry count that differs from items is refused): the level 3 sub-list under that item, or null for an item with none. Same shape as article.documentJson.blocks[]<list>.children[]: see above.
article.documentJson.blocks[]<list>.format "markdown" no Absent: every item of the list, at every level, is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<quote>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<quote>.type "quote" yes Block kind: always quote.
article.documentJson.blocks[]<quote>.text string yes The quoted words.
article.documentJson.blocks[]<quote>.attribution string no Who said it, always plain text. Absent: the quote is unattributed.
article.documentJson.blocks[]<quote>.links object[] no Links from words of text to other articles, at most 20. Only the first link to each article in the whole document is kept (the rest are dropped on save). Absent: none. Ignored when format is "markdown": write the links in the text instead.
article.documentJson.blocks[]<quote>.links[].text string yes The words of the block's text that become the link, 1 to 200 characters. Matched literally, first occurrence only; a link whose words no longer occur in the block is dropped on the next save.
article.documentJson.blocks[]<quote>.links[].articleId string (uuid) yes Id of the article the words link to. It is resolved when the article is rendered: the words stay plain text until that article is PUBLISHED in the same project and has an address (its blog page, or the URL its publishing integration reported).
article.documentJson.blocks[]<quote>.format "markdown" no Absent: the quoted text is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<image>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<image>.type "image" yes Block kind: always image.
article.documentJson.blocks[]<image>.assetId string (uuid) yes Id of the article's asset the block shows (assets[].id of GET /api/articles/{articleId}). The asset's address is resolved when the article is rendered; a block whose asset is missing renders as nothing. The article's hero asset is its cover and cannot be added to the body.
article.documentJson.blocks[]<image>.altText string yes Alternative text for the image. When empty, the asset's own alternative text is used when the article is rendered.
article.documentJson.blocks[]<image>.caption string no Plain-text caption printed under the image. Absent: no caption.
article.documentJson.blocks[]<code>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<code>.type "code" yes Block kind: always code.
article.documentJson.blocks[]<code>.language string no The language of the code (the fence's info string), at most 40 characters, e.g. ts. Renderers keep only the characters A-Z a-z 0-9 _ + # . -. Absent: unspecified.
article.documentJson.blocks[]<code>.text string yes The code, verbatim: never interpreted as Markdown and never translated.
article.documentJson.blocks[]<table>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<table>.type "table" yes Block kind: always table.
article.documentJson.blocks[]<table>.header string[] yes The header row: one cell of text per column, at least one.
article.documentJson.blocks[]<table>.rows string[][] yes The body rows, each an array of cell text with exactly as many cells as header (otherwise refused). May be empty.
article.documentJson.blocks[]<table>.align ("left" | "center" | "right" | null)[] no One alignment per column, so as many entries as header has cells (otherwise refused); null leaves a column to the browser's default. Absent: all default.
article.documentJson.blocks[]<table>.format "markdown" no Absent: every cell, header included, is plain text, shown exactly as written. "markdown": inline Markdown limited to bold, italic, inline code, links (http, https, mailto and relative only), autolinks, escaped characters and character references.
article.documentJson.blocks[]<divider>.id string yes Identifier of the block inside its document, 1 to 64 characters. A client picks it; keep it stable across saves, since a comment's targetId and a publish warning's blockId name a block by it.
article.documentJson.blocks[]<divider>.type "divider" yes Block kind: always divider.
article.contentHtml string | null yes The body rendered as HTML from documentJson, with image blocks resolved to their public URLs. Read-only: re-rendered on every document save and again on publish. null before the first render.
article.contentMarkdown string | null yes The body rendered as Markdown from documentJson, with image blocks resolved to their public URLs. Read-only: re-rendered like contentHtml. null before the first render. On GET /api/articles/{articleId}, stripLead=true returns it without the lead.
article.metaTitle string | null yes The SEO title for search results and link previews; null when none is set.
article.metaDescription string | null yes The SEO description for search results and link previews; null when none is set.
article.targetKeywords string[] yes The keywords the article is written to rank for, in the order given.
article.secondaryKeywords string[] yes Supporting keywords to work into the article; empty when none.
article.primaryLanguage string yes Language tag the article is written in, e.g. en or pt-BR; one of the project's languages.
article.translationGroupId string (uuid) yes Shared by every language version of one article, so the versions can be found together. An article with no translations is alone in its group.
article.generationConfig object | null yes The settings an AI generation was started with (topic, tone, length, image style, ...), kept so the article can be rerun. null for an article created without them.
article.generationConfig.topic string yes What the article is about (1 to 500 characters). The outline step plans the article from it, and with primaryKeyword it is the query for web research and for the project's knowledge base. The article's title is not copied from it: the outline's title, written by the model, becomes the title.
article.generationConfig.prompt string no Free-form direction for the writer, up to 5000 characters (angle, points to cover, things to avoid). Sent to the outline step as 'Additional direction'; the section writers follow the outline it produces.
article.generationConfig.primaryKeyword string no The keyword the article targets, up to 200 characters. The outline step is told to use it where it reads naturally, and it joins topic in the research and knowledge-base queries. It is not copied into the article's targetKeywords.
article.generationConfig.secondaryKeywords string[] yes Supporting keywords, up to 25 of 1 to 200 characters. Sent to the outline step, which spreads them over the sections' keywords. Empty by default. Default: [].
article.generationConfig.audience string no Who the article is for, up to 500 characters. Sent to the outline step, to every section writer, to the image-prompt writers and to the knowledge-base query.
article.generationConfig.tone string no The tone to write in, as free text up to 200 characters; the web app offers professional, conversational, authoritative, practical and persuasive. Sent to the outline step as 'Tone'. The section writers read no separate tone: they follow the outline and the brand voice.
article.generationConfig.language string yes Language the article is written in, as a language tag such as en or pt-BR (default en). The outline, section, research and image-prompt steps use it, so the text and the images' alt text come out in it. A run does not change the article's own primaryLanguage. Default: "en".
article.generationConfig.wordCount integer yes Target length of the whole article in words, 200 to 10000 (default 1200). The outline's sections are planned to add up to roughly this. It also sets the text price per started 1,000 words: 2 credits with writingQuality premium, 1 with standard (1200 costs 4 or 2), and the image count when imageCount is null. Default: 1200.
article.generationConfig.imageStyle "photorealistic" | "editorial" | "minimalist-vector" | "3d-render" | "watercolor" | "corporate-tech" | "cinematic" | "illustration" yes Style of the article's images (default editorial). It applies to the cover and the inline images unless the project's imageSettings set a hero.style or inline.style, which take precedence. Values: photorealistic (natural-light documentary photograph, realistic textures, no stylisation), editorial (modern editorial illustration or photograph, clean composition, restrained palette), minimalist-vector (flat vector illustration, few shapes, solid colours, plenty of empty space), 3d-render (soft studio 3D render, smooth matte materials, simple forms), watercolor (loose watercolour painting, soft edges, muted washes), corporate-tech (clean technology visual in cool blues and greys, geometric shapes), cinematic (cinematic still, dramatic lighting, strong atmosphere, wide framing), illustration (hand-drawn illustration, confident linework, warm friendly palette). Default: "editorial".
article.generationConfig.imageCount integer | null yes How many images to make, cover included, 0 to 20. 0 makes none, 1 only the cover, more add one inline image per section up to the number of outline sections. null (the default) derives it from wordCount: 2 under 1,000 words, 3 under 2,000, 4 under 3,000, otherwise 5. Each image delivered costs 1 credit, or 3 when imageQuality is hd; the quote and the reservation count the full number. Default: null.
article.generationConfig.writingQuality "standard" | "premium" yes Which model writes the article (default premium). premium uses the deployment's article model and costs 2 credits per started 1,000 words; standard uses a lighter, cheaper model and costs 1 credit per started 1,000 words. Default: "premium".
article.generationConfig.imageQuality "standard" | "hd" yes Quality of the article's images (default standard). standard costs 1 credit per image; hd asks the image model for its highest quality and costs 3 credits per image, for the run's images and for each regeneration of an image of the article. Default: "standard".
article.generationConfig.brandVoiceId string (uuid) no Id of the brand voice to write in. Absent, or an id that is not one of the workspace's voices, the project's default voice applies, then the workspace's default voice; with neither, no voice is applied.
article.generationConfig.contentTemplateId string (uuid) no Id of the content template to follow: the outline must contain its sections, and the last section ends on its call to action. Only built-in templates and the workspace's own count; any other id, or a deleted template, is ignored without an error.
article.generationConfig.sourceUrl string (uri) no A reference URL for the article. It is stored with the configuration; no generation step reads or fetches it.
article.generationConfig.researchEnabled boolean yes Whether to research and fact-check (default false). Research looks up sources before the outline (needs the deployment's web_research feature); fact checking verifies the finished article's claims (needs fact_checking). Research adds 1 credit and fact checking 3 to the quote, each only when its feature is on, and is charged only if it delivered: research found a source, fact checking checked a claim. Default: false.
article.generationConfig.reviewOutline boolean yes When true the run stops after the outline (default false): the job ends COMPLETED, the article stays DRAFT with the outline stored, and only research, if any, is charged. Read it with GET /api/articles/{articleId}/outline, edit it, then POST /api/articles/{articleId}/outline/approve to write the article. Default: false.
article.generationConfig.faq boolean yes When true (default false) the article ends with a "Frequently asked questions" section of 3 to 5 questions a reader still has after the body, each answered in two or three sentences, in the article's language. It is written after the sections as one more model call, and its words come out of wordCount: the outline is planned for 250 words fewer, so the price does not change. The section is ordinary content (an H2, then an H3 and a paragraph per question) that can be edited like any other. Default: false.
article.outline object | null yes The outline the AI pipeline planned the article from, kept so a writing step can be retried (and, with reviewOutline, the one a person approved). null for an article that was not generated.
article.outline.title string yes The article title, 1 to 300 characters.
article.outline.slug string yes The URL slug: lowercase letters and digits separated by hyphens, up to 120 characters.
article.outline.metaTitle string yes The SEO title, 1 to 70 characters.
article.outline.metaDescription string yes The SEO description, 1 to 200 characters.
article.outline.sections object[] yes The sections in reading order, 1 to 40.
article.outline.sections[].id string yes Identifier of the section inside the outline, 1 to 64 characters.
article.outline.sections[].heading string yes The section's heading, 1 to 300 characters.
article.outline.sections[].level 2 | 3 yes Heading rank: 2 for a main section, 3 for a sub-section.
article.outline.sections[].purpose string yes What the section covers, 1 to 1000 characters.
article.outline.sections[].keywords string[] yes The keywords attached to the section, at most 25 of 1 to 120 characters. Empty: none. Default: [].
article.outline.sections[].estimatedWords integer yes The section's length in words, between 20 and 5000.
article.createdBy string | null yes Id of the user who created the article (for an article made with an API key, the key's creator). null when no user is attached or that user has been deleted.
article.publishedAt string (date-time) | null yes The article's publication date: when it was published, or the date given at creation or publish for an imported article. null until then; unpublishing keeps it.
article.briefId string (uuid) | null yes Id of the stored content brief the article was written from; null when none.
article.publishedUrl string | null yes The address the publishing integration reported for the live article, which links from other articles point to. null until a publish reports one, and for a webhook target, which never does.
article.wordCount integer | null yes Words in documentJson, stored with every document write; null for an article not counted since the count was added, until the backfill reaches it.
article.createdAt string (date-time) yes When the article was created.
article.updatedAt string (date-time) yes When the article last changed (a save, a status move, a re-render). It is what expectedUpdatedAt is compared with on PATCH /api/articles/{articleId}.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.restoreVersion({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
  version: 1,
});
console.log(result);

pd.articles.listComments(input, init?)

List comments on an article

GET /api/articles/{articleId}/comments

The article's comments, oldest first, each with its author. A reply names the comment it answers in parentCommentId, so the threads are rebuilt from that. Needs articles.read.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
status query "open" | "resolved" no Only comments in this status. Absent: both.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
comments object[] yes The article's comments, oldest first; replies are listed among them by parentCommentId.
comments[].id string (uuid) yes Id of the comment.
comments[].articleId string (uuid) yes Id of the article the comment is on.
comments[].authorId string | null yes Id of the user who wrote it. null for a comment made with an API key, or once that user has been deleted.
comments[].parentCommentId string (uuid) | null yes Id of the comment this one replies to, on the same article, which makes a thread. null for a comment that starts a thread.
comments[].targetType "article" | "section" | "paragraph" | "image" | "seo" yes What part of the article the comment is about: the whole article, a section, a paragraph, an image, or the seo fields.
comments[].targetId string | null yes Id of the block, section or asset the comment points at, depending on targetType. null for a comment on the whole article.
comments[].content string yes The comment's text, trimmed.
comments[].status "open" | "resolved" yes open until someone resolves it with POST /api/articles/{articleId}/comments/{commentId}/resolve; reopen puts it back.
comments[].resolvedAt string (date-time) | null yes When the comment was resolved; null while it is open.
comments[].resolvedBy string | null yes Id of the user who resolved it. null while it is open, when it was resolved with an API key, or once that user has been deleted.
comments[].createdAt string (date-time) yes When the comment was written.
comments[].author object | null yes The author's id and name; null when authorId is null.
comments[].author.id string yes Id of the user.
comments[].author.name string yes The user's display name.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.listComments({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.addComment(input, init?)

Comment on an article

POST /api/articles/{articleId}/comments

Adds an open comment to the article, on the whole article or on a block, section, image or the SEO fields, or a reply to another comment of the article (404 when parentCommentId is not one). Needs articles.comment, which the six system roles hold; a custom role needs it granted. Sends no webhook or email.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
body body object yes
body.content body string yes The comment's text, 1 to 4000 characters; it is trimmed, and a comment that is empty after that is refused.
body.targetType body "article" | "section" | "paragraph" | "image" | "seo" no What part of the article the comment is about: the whole article, a section, a paragraph, an image, or the seo fields. Absent: article.
body.targetId body string | null no Id of the block, section or asset the comment points at, up to 64 characters, depending on targetType. It is stored as given, not checked. Absent or null: a comment on the whole article.
body.parentCommentId body string (uuid) | null no Id of the comment this one replies to, which makes a thread. It must be a comment of the same article (404 otherwise). Absent or null: the comment starts a thread.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 201 Created

Field Type Required Description
comment object yes The comment after the change.
comment.id string (uuid) yes Id of the comment.
comment.articleId string (uuid) yes Id of the article the comment is on.
comment.authorId string | null yes Id of the user who wrote it. null for a comment made with an API key, or once that user has been deleted.
comment.parentCommentId string (uuid) | null yes Id of the comment this one replies to, on the same article, which makes a thread. null for a comment that starts a thread.
comment.targetType "article" | "section" | "paragraph" | "image" | "seo" yes What part of the article the comment is about: the whole article, a section, a paragraph, an image, or the seo fields.
comment.targetId string | null yes Id of the block, section or asset the comment points at, depending on targetType. null for a comment on the whole article.
comment.content string yes The comment's text, trimmed.
comment.status "open" | "resolved" yes open until someone resolves it with POST /api/articles/{articleId}/comments/{commentId}/resolve; reopen puts it back.
comment.resolvedAt string (date-time) | null yes When the comment was resolved; null while it is open.
comment.resolvedBy string | null yes Id of the user who resolved it. null while it is open, when it was resolved with an API key, or once that user has been deleted.
comment.createdAt string (date-time) yes When the comment was written.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.addComment({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
  body: {
    content: "example",
  },
});
console.log(result);

pd.articles.resolveComment(input, init?)

Resolve a comment

POST /api/articles/{articleId}/comments/{commentId}/resolve

Marks the comment resolved, recording who and when. Resolving a comment that is already resolved records the new resolver and time. It does not touch the replies of the thread. Needs articles.comment; 404 when the comment is not on the article.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
commentId path string (uuid) yes Id of the comment, which must belong to the article in the path (404 otherwise).
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
comment object yes The comment after the change.
comment.id string (uuid) yes Id of the comment.
comment.articleId string (uuid) yes Id of the article the comment is on.
comment.authorId string | null yes Id of the user who wrote it. null for a comment made with an API key, or once that user has been deleted.
comment.parentCommentId string (uuid) | null yes Id of the comment this one replies to, on the same article, which makes a thread. null for a comment that starts a thread.
comment.targetType "article" | "section" | "paragraph" | "image" | "seo" yes What part of the article the comment is about: the whole article, a section, a paragraph, an image, or the seo fields.
comment.targetId string | null yes Id of the block, section or asset the comment points at, depending on targetType. null for a comment on the whole article.
comment.content string yes The comment's text, trimmed.
comment.status "open" | "resolved" yes open until someone resolves it with POST /api/articles/{articleId}/comments/{commentId}/resolve; reopen puts it back.
comment.resolvedAt string (date-time) | null yes When the comment was resolved; null while it is open.
comment.resolvedBy string | null yes Id of the user who resolved it. null while it is open, when it was resolved with an API key, or once that user has been deleted.
comment.createdAt string (date-time) yes When the comment was written.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.resolveComment({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
  commentId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.reopenComment(input, init?)

Reopen a resolved comment

POST /api/articles/{articleId}/comments/{commentId}/reopen

Marks the comment open again and clears resolvedAt and resolvedBy. Needs articles.comment; 404 when the comment is not on the article.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
commentId path string (uuid) yes Id of the comment, which must belong to the article in the path (404 otherwise).
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
comment object yes The comment after the change.
comment.id string (uuid) yes Id of the comment.
comment.articleId string (uuid) yes Id of the article the comment is on.
comment.authorId string | null yes Id of the user who wrote it. null for a comment made with an API key, or once that user has been deleted.
comment.parentCommentId string (uuid) | null yes Id of the comment this one replies to, on the same article, which makes a thread. null for a comment that starts a thread.
comment.targetType "article" | "section" | "paragraph" | "image" | "seo" yes What part of the article the comment is about: the whole article, a section, a paragraph, an image, or the seo fields.
comment.targetId string | null yes Id of the block, section or asset the comment points at, depending on targetType. null for a comment on the whole article.
comment.content string yes The comment's text, trimmed.
comment.status "open" | "resolved" yes open until someone resolves it with POST /api/articles/{articleId}/comments/{commentId}/resolve; reopen puts it back.
comment.resolvedAt string (date-time) | null yes When the comment was resolved; null while it is open.
comment.resolvedBy string | null yes Id of the user who resolved it. null while it is open, when it was resolved with an API key, or once that user has been deleted.
comment.createdAt string (date-time) yes When the comment was written.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.reopenComment({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
  commentId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);

pd.articles.checkDuplicates(input, init?)

Check a title for a possible duplicate before creating an article

GET /api/projects/{projectId}/duplicate-check

Articles of the project in language (default: the project's), in any group, whose title or slug is close to the one given: at most 3, best match first, one per group, each with a similarity score of at least 0.6. Needs articles.read. Advice for a form: it never blocks anything, and nothing is written.

Input

Name In Type Required Description
projectId path string (uuid) yes Project whose articles the title is compared with.
title query string yes The title the new article would get, 1 to 300 characters.
slug query string no The address the new article would get, 1 to 120 characters. Omit to compare with the address derived from title.
language query string no Only articles in this language tag are compared. Defaults to the project's default language.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
possibleDuplicates object[] yes Existing articles that look like the one being created; empty when none.
possibleDuplicates[].id string (uuid) yes Id of the existing article that looks like the new one.
possibleDuplicates[].translationGroupId string (uuid) yes Translation group of that article; it is never the new article's own group.
possibleDuplicates[].title string yes The existing article's title.
possibleDuplicates[].slug string yes The existing article's slug.
possibleDuplicates[].score number yes How close the title or slug is to the new one, from 0 to 1; 1 is the same words or the same address.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.checkDuplicates({
  projectId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
  title: "example",
});
console.log(result);

pd.articles.move(input, init?)

Move articles to another project

POST /api/workspaces/{workspaceId}/articles/move

Moves language versions of articles to another project of the workspace, in one transaction. Needs articles.delete on the source project and articles.create on the destination, and projects.update on the destination when addMissingLanguages adds a language. A translation group that moves whole keeps its translationGroupId; when only some versions move they take one new id, because the versions of a group must share a project. A slug the destination already holds in that language becomes slug-2, slug-3…, listed in renamed. PUBLISHED articles go back to DRAFT (their publishedAt and publishedUrl are cleared) and the source project's webhooks receive article.unpublished (reason: "status_changed"). Slug redirects of the moved articles are dropped, calendar items and ideas linked to them follow, and link suggestions that would cross projects are removed. 409 CONFLICT with details.reason: missing_languages, articles_busy (QUEUED or PROCESSING versions, in details.blocked) or destination_archived. One articles.moved audit entry per article.

Input

Name In Type Required Description
workspaceId path string (uuid) yes Id of the workspace the request works in. The caller must be a member of it, or hold an API key issued by it. For anyone else, an unknown id and an id they do not belong to both answer 404 RESOURCE_NOT_FOUND, so membership cannot be probed.
body body object yes
body.destinationProjectId body string (uuid) yes Id of the project the articles move to: an active project of the same workspace. Another workspace's project answers 404, an archived one 409 destination_archived.
body.addMissingLanguages body boolean no Add the languages the destination does not offer yet to its list. Without it such a move answers 409 with details.reason: missing_languages and the languages in details.missingLanguages. Adding needs projects.update on the destination. Default false.
body.articleIds body string (uuid)[] no Ids of the articles (language versions) to move, 1 to 200, all in one project. Give this, or sourceProjectId with languages, not both.
body.sourceProjectId body string (uuid) no With languages: move every article of this project written in those languages.
body.languages body string[] no With sourceProjectId: the language tags whose articles move.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
moved object[] yes Every article that moved.
moved[].id string yes Id of the article.
moved[].slug string yes Its slug in the destination project (renamed on a clash).
moved[].projectId string yes The destination project.
moved[].translationGroupId string yes Its translation group now: unchanged when the whole group moved.
moved[].status "DRAFT" | "QUEUED" | "PROCESSING" | "NEEDS_REVIEW" | "CHANGES_REQUESTED" | "APPROVED" | "PUBLISHED" yes Its status now: DRAFT for one that was PUBLISHED.
renamed object[] yes The articles whose slug changed because the destination already held it.
renamed[].id string yes Id of the article.
renamed[].from string yes The slug it had.
renamed[].to string yes The slug it has in the destination.
addedLanguages string[] yes Languages added to the destination project's list; empty when none.
unpublished string[] yes Ids of the articles that were PUBLISHED and are DRAFT now.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.move({
  workspaceId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
  body: {
    destinationProjectId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
  },
});
console.log(result);

pd.articles.setHeroFromUrl(input, init?)

Replace the hero image with a picture from a URL

POST /api/articles/{articleId}/hero

Fetches imageUrl (http and https only; every address and redirect is checked so private networks are refused; 25 MB and 15 seconds at most), stores it whole in the hero box (never cropped) under a new object key, and makes it the article's one hero. The previous hero row goes, and its object once nothing else points at it, unless the article is or was published: a copy on a CMS still names that URL, so the object stays served. 409 CONFLICT when another replacement landed at the same moment. Needs images.update. QUOTA_EXCEEDED (429) when the picture does not fit the workspace's storage quota. Webhook image.regenerated with type: hero. Costs no credits.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
body body object yes
body.imageUrl body string (uri) yes Where to fetch the picture (http or https; private networks refused).
body.altText body string no Alternative text for the new hero, 1 to 300 characters. Omitted: the previous hero's, or the article's title when it had none.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 201 Created

Field Type Required Description
asset object yes The new hero asset, of type hero.
asset.id string (uuid) yes Id of the asset; an image block's assetId refers to it.
asset.type "hero" | "inline" yes hero: the article's cover image (one per article, kept out of the body). inline: an image shown in the body by an image block.
asset.publicUrl string yes The public URL the image is served from.
asset.altText string yes Alternative text of the image.
asset.prompt string yes The text the image was generated from. Empty for a picture that was uploaded, fetched from a URL or imported.
asset.style string | null yes The image style used to generate it; null for a picture that was not generated.
asset.width integer yes Width of the stored image in pixels.
asset.height integer yes Height of the stored image in pixels.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.setHeroFromUrl({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
  body: {
    imageUrl: "https://example.com",
  },
});
console.log(result);

pd.articles.setHeroFromFile(input, init?)

Replace the hero image with an uploaded file

PUT /api/articles/{articleId}/hero/file

The body is the raw image, sent with its own content type (image/png, image/jpeg, image/webp, image/gif, image/avif), 25 MB at most (413 above). The bytes must decode as an image (400 otherwise). Everything else is as for POST /hero: stored whole under a new key, one hero per article, images.update, the storage quota, the image.regenerated webhook.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
altText query string no Alternative text for the new hero, 1 to 300 characters. Omitted: the previous hero's, or the article's title when it had none.
body body Blob | ArrayBuffer | Uint8Array yes The raw file.
contentType body "image/png" | "image/jpeg" | "image/webp" | "image/gif" | "image/avif" yes Sent as the Content-Type header; picks how the server reads the file.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 201 Created

Field Type Required Description
asset object yes The new hero asset, of type hero.
asset.id string (uuid) yes Id of the asset; an image block's assetId refers to it.
asset.type "hero" | "inline" yes hero: the article's cover image (one per article, kept out of the body). inline: an image shown in the body by an image block.
asset.publicUrl string yes The public URL the image is served from.
asset.altText string yes Alternative text of the image.
asset.prompt string yes The text the image was generated from. Empty for a picture that was uploaded, fetched from a URL or imported.
asset.style string | null yes The image style used to generate it; null for a picture that was not generated.
asset.width integer yes Width of the stored image in pixels.
asset.height integer yes Height of the stored image in pixels.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.setHeroFromFile({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
  body: new Blob([/* the file's bytes */]),
  contentType: "image/png",
});
console.log(result);

pd.articles.detachHeroFromBody(input, init?)

Take the hero image out of the article body

POST /api/articles/{articleId}/hero/detach-from-body

For an article from before the hero was kept out of the body: removes the image blocks that show the hero (a new version; the asset itself stays as the cover) and answers how many it removed, 0 when there were none. article.updated fires for a PUBLISHED article. Needs articles.update.

Input

Name In Type Required Description
articleId path string (uuid) yes Id of an article. The workspace and project it belongs to are read from the article, never from the request; the caller must be a member of that workspace (and of the project, when it has members). For anyone else the answer is 404 RESOURCE_NOT_FOUND, the same as for an id that does not exist.
init init { signal?: AbortSignal; timeoutMs?: number } no The second argument: signal aborts the call, timeoutMs overrides the client's per-attempt timeout.

Returns 200 OK

Field Type Required Description
removed integer yes How many image blocks of the body showed the hero and were removed; 0 when none.

Errors 400 invalid input, 401 missing or invalid key, 403 the key's roles lack the permission, 404 not found, 429 rate limited or out of quota. See Errors.

Example

import { PrismDraft } from "@prism-draft/sdk";

const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY! });

const result = await pd.articles.detachHeroFromBody({
  articleId: "3f0c5a1e-8b2d-4c6e-9a7f-1d2e3f4a5b6c",
});
console.log(result);