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);