News Factory API
The News Factory public REST API: find news in your niche, turn it into stories with AI, check them for quality and SEO, and publish them to your site. Everything the app does with stories, content sources, RSS feeds and workflows, scoped to one organization and one Topic Workspace.
Authentication. Send Authorization: Bearer <API key> with every request. An organization admin creates keys in the app under Organization settings, API keys (Pro, Business and Enterprise plans), choosing the scopes each integration needs and, optionally, one Topic Workspace and an expiry. Keys start with nfk_ and are shown once.
Start with GET /me. It returns the organization, the key's scopes, the Topic Workspaces the key can act on and anything that would block a request.
Errors are always { "error": { "code", "message", "details", "requestId", "docs" } }; docs links to what the code means and what to do.
Limits. 100 requests per minute per organization (429 RATE_LIMIT_EXCEEDED with Retry-After). AI endpoints spend the organization's AI credits (429 LLM_QUOTA_EXCEEDED when they run out).
Locale codes are always full codes such as en-US, pt-BR or es-CO, never bare en.
Quickstart
The News Factory API lets software, and AI agents, do what the News Factory app does: find news in a niche, turn articles into stories with AI, check them for quality and SEO, translate them, and publish them.
- Get a key. An organization admin opens the app, goes to Organization settings, API keys, and creates a key. Pick the access it needs (read only, read and write, or full access including AI), optionally one Topic Workspace and an expiry. The key starts with
nfk_and is shown once: store it as a secret. - Check it. Call
GET /v1/me. It answers who the key is, its scopes, the Topic Workspaces it can act on and anything that would block a request.
curl https://client-api.news-factory.app/v1/me \
-H "Authorization: Bearer $NEWS_FACTORY_API_KEY"
- List stories.
curl "https://client-api.news-factory.app/v1/stories?status=published&language=en-US&order=publishedAt%20DESC&limit=10" \
-H "Authorization: Bearer $NEWS_FACTORY_API_KEY"
Every response is JSON. Lists answer { "data": [...], "meta": { "page", "limit", "hasMore" } }; single records answer { "data": {...} }; writes answer { "success": true, "data": {...}, "message" }; errors answer { "error": { "code", "message", "details", "requestId", "docs" } }.
Machine-readable versions of this reference: OpenAPI 3.1, llms.txt and the whole reference as Markdown. The OpenAPI operationIds (listStories, createContentSource, ...) make good tool names for an agent.
Authentication and scopes
Send the key on every request:
Authorization: Bearer nfk_...
Keys belong to the organization, not to a person: they keep working when their creator leaves, until an admin revokes them or they expire. Work done with a key is attributed to the admin who created it. An organization can hold up to 25 active keys, so give every integration its own key with only the scopes it needs, and revoke it on its own when it is no longer used.
Scopes
A key carries one or more scopes. An endpoint lists the scopes it needs; a key must have all of them, otherwise the answer is 403 INSUFFICIENT_SCOPE with details.missing.
| Scope | Grants |
|---|---|
stories:read |
Read stories, their media, quality-check and SEO audit history |
stories:write |
Create, update and delete stories; upload, attach and detach media; set thumbnails |
content-sources:read |
Read content sources |
content-sources:write |
Create, update, delete and scrape content sources |
feeds:read |
Read followed feeds, the feed catalogue, feed articles and the scheduler status |
feeds:write |
Follow and unfollow feeds, fetch a feed now |
workflows:read |
Read workflows, workflow runs and node runs |
workflows:run |
Start, pause, resume and stop workflow runs |
ai:generate |
Use AI tools that spend AI credits |
AI endpoints need ai:generate together with the scope of what they read or change: suggesting titles needs ai:generate and stories:read; translating a story, which creates one, needs ai:generate and stories:write.
The app offers three presets: Read only (every :read scope), Read and write (adds every :write scope and workflows:run, no AI) and Full access (everything).
Keeping keys safe
- Keys are shown once. News Factory stores only a hash, so a lost key cannot be recovered: create a new one and revoke the old one.
- Never put a key in a browser, a mobile app or a public repository. Call the API from a server.
- Revoking or expiring a key takes effect everywhere within one minute.
- Keys start with
nfk_so secret scanners can recognise them.
Legacy keys
Keys created before scoped keys (they start with ak_) keep working with full access, but they are deprecated. GET /v1/me reports them with deprecated: true and the problem LEGACY_API_KEY. Replace them with scoped keys in the app.
Plans
The API is included in the Pro, Business and Enterprise plans (and their trials). On other plans every endpoint except GET /v1/me answers 403 PLAN_REQUIRED.
Topic Workspaces
Everything in News Factory belongs to a Topic Workspace: a niche with its own feeds, stories, settings and keywords. Every request acts on exactly one.
- A key bound to a Topic Workspace always acts on it. Leave out
X-Topic-Key; sending a different one is refused with403 INVALID_TOPIC. - A key for all Topic Workspaces picks one with the
X-Topic-Keyheader. Without the header, the organization's first Topic Workspace is used. An unknown key answers400 INVALID_TOPICwithdetails.validTopics.
GET /v1/me lists the Topic Workspaces a key can use (data.topic.available, each { key, name }) and which one the request resolved to.
curl https://client-api.news-factory.app/v1/stories \
-H "Authorization: Bearer $NEWS_FACTORY_API_KEY" \
-H "X-Topic-Key: health"
Records never move between Topic Workspaces through the API: the request's Topic Workspace decides where a story or content source is created.
Errors
Every error has the same shape and a matching HTTP status:
{
"error": {
"code": "INSUFFICIENT_SCOPE",
"message": "This API key lacks the scope stories:write needed for this endpoint. Edit the key's access in the app or use another key.",
"details": { "required": ["stories:write"], "missing": ["stories:write"], "granted": ["stories:read"] },
"requestId": "6f1c2a9e-3b7d-4c41-9f0e-2d8a5b7c9e10",
"docs": "https://client-api.news-factory.app/docs#error-insufficient-scope"
}
}
message is written for people and may change; branch on code. requestId is also in the X-Request-Id response header: quote it to support. You may send your own X-Request-Id (8 to 64 letters, digits, ., _ or -) to correlate logs.
UNAUTHORIZED (401). No key, a malformed key, or a revoked or expired key. Do not retry with the same key. Check the Authorization: Bearer nfk_... header; if the key was revoked, ask an admin for a new one. Many failed attempts from one address pause that address for a few minutes (429).
FORBIDDEN (403). The request is not allowed for this organization, for example a record of another organization. Do not retry.
INSUFFICIENT_SCOPE (403). The key lacks a scope the endpoint needs. details.missing names them. An admin can add scopes to the key in the app, or use a key that has them.
PLAN_REQUIRED (403). The organization's plan does not include the API. It needs Pro, Business or Enterprise.
INVALID_TOPIC (400 or 403). X-Topic-Key names a Topic Workspace that does not exist (400, details.validTopics lists the valid keys), or the key is bound to another Topic Workspace (403), or the key's Topic Workspace was deleted (403, create a new key).
NO_TOPIC_WORKSPACE (409). The organization has no Topic Workspace yet. Create one in the app.
NOT_FOUND (404). The record does not exist in this Topic Workspace, or the path is not an endpoint (see this reference).
METHOD_NOT_ALLOWED (405). The path exists but not with this HTTP method. The Allow header lists the methods it supports.
VALIDATION_ERROR (400, 413 or 422). A parameter or body field is missing or wrong; message says which. 413 means the body is too large (10 MB for image uploads, 1 MB otherwise). 422 from an apply-fix endpoint means the text changed since the check; retry with mode: "rewrite". Fix the request before retrying.
CONFLICT (409). The request clashes with the current state: a content source with this link already exists (details.existingContentSourceId), a workflow run is already active (details.runId), a run already finished. Use the existing record or wait.
PLAN_LIMIT_REACHED (403). A plan limit stops the action (feeds, languages, storage, ...). details.limitKey, details.limit and details.current say which. An admin can upgrade or buy an add-on.
RATE_LIMIT_EXCEEDED (429). More than 100 requests in a minute for the organization, too many failed authentications from one address, or a job queue that is busy. Wait the number of seconds in Retry-After, then retry.
DAILY_QUOTA_EXCEEDED (429). The plan's daily request quota is used up. details.resetsAt is the next midnight UTC.
LLM_QUOTA_EXCEEDED (429). The organization has no AI credits left for today. details.resetsAt says when the daily allowance comes back. Non-AI endpoints keep working.
UPSTREAM_ERROR (502). A News Factory service failed. Retry with exponential backoff (for example 2, 4, 8 seconds); if it keeps failing, contact support with the requestId.
GATEWAY_MISCONFIGURED (503). API keys cannot be verified right now. Retry in a minute.
INTERNAL_ERROR (500). Something unexpected failed. Retry once; then contact support with the requestId.
Retrying safely
Retry 429, 502, 503 and 500 with backoff. Never blindly retry a POST that creates something (a story, a content source, a translation, a run) after a timeout: list the records first, or you may create a duplicate. Do not retry other 4xx answers without changing the request.
Pagination and filtering
List endpoints take limit (default 20, at most 100) and page (zero based) and answer meta: { page, limit, hasMore }. hasMore is true when the page was full, so request the next page until it is false. page * limit can be at most 10000; narrow the query with filters to reach older records.
Most lists sort with order=<field> ASC|DESC on the fields each endpoint documents.
Stories
GET /v1/stories and GET /v1/stories/by-language/{language} take flat filters (status, language, tag, search, dateFrom, dateTo, include) or one advanced filter JSON in LoopBack's shape:
{
"where": { "status": "published", "publishedAt": { "gte": "2026-08-01T00:00:00Z" } },
"include": ["images"],
"order": ["publishedAt DESC"],
"limit": 50
}
Rules for filter (a filter that breaks one answers 400 VALIDATION_ERROR naming it):
wherefields: id, title, slug, summary, content, language, status, tags, thumbnail, thumbnailImageId, mainImageId, mainImageUrl, translationStatus, publishedAt, firstReported, createdAt, updatedAt. Other fields are ignored.- Operators: eq, neq, gt, gte, lt, lte, between, inq, nin, like, nlike, ilike, nilike, exists.
regexpandnearare not supported. likepatterns are regular expressions of at most 100 characters, without groups, repetition counts or back references.and/or/nornest at most 3 levels with at most 20 conditions each;inq/nintake at most 100 values; strings at most 500 characters;offset/skipat most 10000.
Story dates: publishedAt is when the story was first published (null until then; kept if it is unpublished later), firstReported when its earliest source was published, createdAt when News Factory wrote it, updatedAt its last change. dateFrom / dateTo filter on createdAt; filter on publication with where.publishedAt. A page of the latest published stories is status=published&order=publishedAt DESC.
include adds relations to each story: images, authors, categories, qualityChecks, seoAudits, translations, contentSources. Relations you do not request are never returned.
Locale codes
Languages are always full locale codes: en-US, en-GB, pt-BR, es-CO, de-DE. Bare codes such as en are not used anywhere in the API.
Limits
| Limit | Value | When exceeded |
|---|---|---|
| Requests per organization | 100 per minute | 429 RATE_LIMIT_EXCEEDED, Retry-After header |
| Failed authentications per address | 60 per 5 minutes | 429 RATE_LIMIT_EXCEEDED for that address |
| Daily requests | set by the plan (none today) | 429 DAILY_QUOTA_EXCEEDED, resets at midnight UTC |
| AI credits | the organization's daily allowance plus add-ons | 429 LLM_QUOTA_EXCEEDED on AI endpoints |
| Request body | 10 MB for image uploads, 1 MB otherwise | 413 VALIDATION_ERROR |
| Active API keys | 25 per organization | creating more is refused in the app |
| Workflow runs | one active run per organization | 409 CONFLICT with details.runId |
Responses carry RateLimit and RateLimit-Policy headers (IETF draft 7) so a client can slow down before it is limited. When a daily quota applies, X-Quota-Limit, X-Quota-Remaining and X-Quota-Reset are set too.
AI endpoints (marked with the ai:generate scope) can take up to a few minutes. Use a client timeout of at least 5 minutes for them and do not retry them on a timeout without checking whether the result already exists.
Recipes for agents
Short, tested sequences for common goals. Each step names the operationId from the OpenAPI document.
Before anything: know your key
getMe (GET /v1/me): check data.problems is empty, note data.credential.scopes and data.topic.available. If a step below needs a scope the key lacks, stop and tell the user which scope to add.
Write a story from a web article
Scopes: content-sources:write, content-sources:read, stories:write, ai:generate.
createContentSource(POST /v1/content-sources) with{ "title": "<headline>", "link": "<article URL>", "sourceType": "website" }. A409 CONFLICTmeans it exists: usedetails.existingContentSourceId.scrapeContentSource(POST /v1/content-sources/{id}/scrape) to fetch and parse the page.repurposeContentSource(POST /v1/content-sources/{id}/repurpose) with{ "languageCode": "en-US" }. It answersdata.storyIdof the new draft.getStoryById(GET /v1/stories/{id}) to read the result.
Check and improve a story before publishing
Scopes: stories:read, stories:write, ai:generate.
qualityCheck(POST /v1/stories/{storyId}/content/quality-check) andseoAudit(POST /v1/stories/{storyId}/content/seo-audit).- For each finding or fix worth applying:
applyQualityFixorapplySeoFixwith the finding orfixobject from the report. On422retry with"mode": "rewrite". updateStory(PATCH /v1/stories/{id}) with{ "status": "published" }when the story is ready. News Factory records the moment inpublishedAt.
Show the latest published stories
Scope: stories:read. listStories (GET /v1/stories?status=published&order=publishedAt DESC) answers the newest publications first; show each story's publishedAt as its date and rewrite the links between stories (/{slug}) to your own route, as Rendering stories on your site explains. For one language use listStoriesByLanguage with the same params; for a date window use filter with where.publishedAt (gte / lte).
Suggest what to read next
Scope: stories:read. getStoryById (GET /v1/stories/{id}) or getStoryBySlug answers data.relatedStories: up to 5 published stories in the same language, closest first, each with slug, title, summary and a score from 0 to 1. Use them for a Read next block, for internal links when you write or edit a story, or to spot that a topic was already covered. They are found within about a minute of publication: right after updateStory publishes a story, read it again a minute later for its list.
Publish in another language
Scopes: stories:write, ai:generate. translateStory (POST /v1/stories/{storyId}/content/translate) with { "languageCode": "de-DE" } creates the translated story. Read every language version with include=translations, or list one language with listStoriesByLanguage.
Pick relevant news from followed feeds
Scopes: feeds:read (and the scopes of the recipe above to write from it).
listFeeds(GET /v1/feeds) for the feeds the Topic Workspace follows;searchFeedCatalogandsubscribeFeedto follow more (feeds:write).listFeedArticles(GET /v1/feeds/articles?minConfidence=70) for articles already judged relevant to the Topic Workspace, highest confidence first withorder=matchConfidence DESC.- Write a story from an article's
urlwith the first recipe.
Run a workflow and wait for it
Scopes: workflows:read, workflows:run.
listWorkflows(GET /v1/workflows) and pick one (isActive=true).runWorkflow(POST /v1/workflows/{id}/run) answers202withdata.runId. A409means a run is already active: usedetails.runId.- Poll
getWorkflowRun(GET /v1/workflow-runs/{runId}) every 10 to 30 seconds untilstatusiscompleted,failedorcancelled.listNodeRunsshows per-step progress.pauseWorkflowRun,resumeWorkflowRunandcancelWorkflowRun(Stop) control it.
Good manners
- Read before you write; never create duplicates by retrying a
POSTafter a timeout. - Keep to 100 requests a minute and honour
Retry-After. - AI endpoints spend the organization's credits: run them only when the user asked for the result.
Images and provenance
Every image the client API returns (in images[] on stories, and from the
/stories/{storyId}/media endpoints) is a public Image object. Storage
internals are stripped and three provenance fields are added so a consumer can
decide which picture to show without knowing how News Factory stores files.
Public Image object
{
"id": "6a99370b892980259c5de946",
"url": "https://news-factory.s3.us-east-1.amazonaws.com/.../generated_openai_1788425993651_0.png",
"width": 1024,
"height": 576,
"format": "PNG",
"contentType": "image/png",
"fileSize": 1583022,
"imageType": "main",
"origin": "generated",
"aiGenerated": true,
"generation": {
"provider": "openai",
"model": "gpt-image-1",
"prompt": "Topic tags: Amazon, AI, Cloud Computing ... Image Requirements: Editorial illustration ..."
},
"sourceType": "generated",
"parentId": null,
"title": "AI Generated Image - openai",
"description": "...",
"altText": null,
"caption": null,
"author": "openai AI",
"source": "openai",
"license": null,
"creditLine": null,
"originalUrl": null,
"tags": ["ai-generated", "openai", "ai"],
"createdAt": "2026-09-03T08:59:55.424Z",
"updatedAt": "2026-09-03T08:59:55.424Z"
}
| Field | Type | Meaning |
|---|---|---|
url |
string | null | Public URL of the file. s3Url is kept as a deprecated alias with the same value. |
imageType |
string | Role of the image on the story: main, thumbnail, gallery, content, other. |
origin |
generated | stock | source | upload |
Normalized provenance (see below). |
aiGenerated |
boolean | Authoritative flag: true only when News Factory produced the image with an AI image model, including crops and thumbnails derived from such an image. |
generation |
object | null | { provider, model, prompt } when aiGenerated is true, otherwise null. Any of the three can be null for older images. |
sourceType |
string | Raw storage source (generated, stock, story, content-source, scraped, user-upload, url). Kept for backwards compatibility; do not use it to detect AI images, resized thumbnails were historically stored as generated too. |
parentId |
string | null | Id of the image this one was derived from (crop / thumbnail). |
originalUrl |
string | null | Where the bytes were fetched from, for images copied from an article or imported by URL. |
author, source, license, creditLine |
string | null | Attribution. For stock images source is the provider (unsplash, pexels, ...); for article images it is the article URL; for AI images the provider id. |
origin values
origin |
Comes from | Typical use |
|---|---|---|
generated |
An AI image model run by News Factory (aiGenerated: true), or a legacy row that only recorded sourceType: "generated" and whose parent is not in the same response |
Safe to publish under your own brand when aiGenerated is true; for legacy rows check aiGenerated first. |
stock |
A licensed stock provider (Unsplash, Pexels, ...) | Check license / creditLine for attribution requirements. |
source |
The original article / content source the story was written from | Usually not yours to republish; treat as a preview only. |
upload |
Uploaded by a user or imported by URL through the API | Whatever rights you have to the file you uploaded. |
Derived images (a parentId pointing at another image in the same
response, e.g. a resized thumbnail) take the origin of their parent when their
own record does not carry the AI flag, so a thumbnail of the publisher's photo
reports origin: "source" even though it is stored as sourceType: "generated".
Getting images with stories
Images are an opt-in include on every story read:
GET /v1/stories?include=images&language=en-US&limit=20
GET /v1/stories/by-language/es-CO?include=images
GET /v1/stories/{id}?include=images
GET /v1/stories/slug/{slug}?include=images
GET /v1/stories/{id}/media
The story itself also carries quick-access fields:
| Field | Meaning |
|---|---|
thumbnail |
URL of the current thumbnail. Match it against images[].url to learn its provenance. |
thumbnailImageId |
Id of the images[] entry backing thumbnail (set for stories processed after 2026-09-05). |
mainImageId, mainImageUrl |
The current main image (set when a main image is generated, picked from stock or reused from the source through the Image Gen workflow node, for stories processed after 2026-09-05). |
Choosing which image to show
Prefer the flag, never sourceType:
function pickHeroImage(story) {
const images = story.images ?? [];
const generated = images.filter((img) => img.aiGenerated && img.url);
return (
generated.find((img) => img.imageType === "main") ??
generated.find((img) => img.imageType === "thumbnail") ??
generated[0] ??
null // fall back to your own placeholder
);
}
To show any image but avoid republishing the publisher's photo:
images.filter((img) => img.origin !== "source" && img.url)
Media endpoints
GET /stories/{storyId}/media returns { success, data: PublicImage[], meta: { count } }.
POST /stories/{storyId}/media, POST .../media/from-url and POST .../media/{imageId} return the created/attached image in the same public shape. POST .../media/generate returns the raw generation result (provider, model, images[] with imageId and url); re-read the story with include=images to get the public objects with provenance.
Connect an AI agent (MCP)
The API is also a Model Context Protocol server at https://client-api.news-factory.app/mcp (Streamable HTTP, stateless). Every endpoint is a tool named after its operationId (getMe, listStories, translateStory, ...) with its parameters and body as the input, and a key only sees the tools its scopes allow. A tool call runs the same REST request with the same key, so scopes, the Topic Workspace, limits and errors are exactly the API's.
Claude Code:
claude mcp add --transport http news-factory https://client-api.news-factory.app/mcp \
--header "Authorization: Bearer $NEWS_FACTORY_API_KEY"
Any MCP client that can send a header works the same way: the URL above and Authorization: Bearer <API key>. Give the agent a key with only the scopes it needs, for example a Read only key for research, and add ai:generate only when it should spend AI credits.
Tool results are the API's JSON answers, prefixed with the HTTP status (HTTP 200). A failed call is a tool error carrying the API's error body.
Rendering stories on your site
A story's content is HTML, ready to place in your page. Three things need handling before you publish it: links to other stories, dates, and stories still being written. Every story also tells you which of your other stories to show next.
Links to other stories
News Factory links one story to another (for example the SEO step's internal links) with the linked story's slug at the site root:
<a href="/the-hidden-cost-of-ai-generated-workslop">AI-generated</a>
News Factory stores the slug, never a URL: your site decides where a story lives. Rewrite these links to your own story route before rendering, or they lead to a 404 on any site that does not serve stories at /{slug}.
- Recognise them. A story link's
hrefis exactly/followed by a slug (lowercase letters, digits and hyphens), optionally followed by?queryor#fragment. Leave every other link alone: other sites, your own pages, anchors. - Rewrite them to your route, for example
/{slug}to/news/{slug},/es/news/{slug}orhttps://example.com/blog/{slug}. Keep the query or fragment. - Translations. In a translated story, a link points at the linked story's version in the same language when that version existed at translation time, otherwise at the original.
GET /v1/stories/slug/{slug}finds a story in any language; itslanguageandtranslationstell you which of your pages to link. - Check the target. A linked story can be unpublished, deleted, hollow (no body yet) or outside what your site shows. Link only to stories you have a page for (your own list of slugs, or
GET /v1/stories/slug/{slug}not answering 404), and otherwise keep the text without the link.
// Point story links at your route; drop the ones you have no page for.
const STORY_LINK = /<a\b([^>]*?\bhref\s*=\s*)(["'])\/([a-z0-9][a-z0-9-]*)\/?([?#][^"']*)?\2([^>]*)>([\s\S]*?)<\/a>/gi;
function rewriteStoryLinks(html, { route = (slug) => `/news/${slug}`, hasPage = () => true } = {}) {
return html.replace(STORY_LINK, (link, before, quote, slug, suffix = "", after, text) =>
hasPage(slug) ? `<a${before}${quote}${route(slug)}${suffix}${quote}${after}>${text}</a>` : text,
);
}
// rewriteStoryLinks(story.content, { hasPage: (slug) => publishedSlugs.has(slug) })
If a body can link to one of your own top-level pages (for example /pricing), skip those paths before rewriting: they are pages, not stories.
Related stories (Read next)
Every story read one at a time (GET /v1/stories/{id}, GET /v1/stories/slug/{slug}) and every story of GET /v1/stories/by-language/{language} carries relatedStories: up to 5 published stories of the same Topic Workspace and the same language that are closest to it in meaning, closest first. No include is needed. The plain list (GET /v1/stories) leaves them out to stay light.
"relatedStories": [
{
"id": "68b9b7c2f1a2b3c4d5e6f740",
"title": "Hospitals Cut Waiting Times with AI Triage",
"slug": "hospitals-cut-waiting-times-with-ai-triage",
"summary": "Emergency departments using AI triage report shorter waits and fewer missed cases.",
"language": "en-US",
"publishedAt": "2026-08-12T09:10:00.000Z",
"mainImageUrl": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f740/generated_openai_1788125993651_0.png",
"thumbnail": null,
"score": 0.8412
}
]
- When they appear. News Factory finds them when a story is published, within about a minute, and again when a published story gets a new title or summary. Until then the list is empty, and it stays empty when no published story is close enough (
scorebelow 0.7). A story published later joins the lists of the older stories it is close to, so lists keep up without anyone republishing. - Always current. Only stories that are published right now are listed, with their current title and slug: one you unpublish or delete drops out at once and the next closest takes its place.
- Per language. A story's related stories are in its own language and never include another language version of the same story. A translation gets its own list, from the stories published in that language.
- Render them like any link to a story: point
slugat your own route (/news/{slug}), and skip the ones you have no page for, as above.
// Read next: up to 3 related stories your site has a page for.
const readNext = story.relatedStories.filter((r) => publishedSlugs.has(r.slug)).slice(0, 3);
// <a href={`/news/${r.slug}`}>{r.title}</a>
A static site built from GET /v1/stories/by-language/{language} gets every story's list in the same response, without a call per story; rebuild it now and then so new publications show up in older stories' Read next blocks.
Dates
publishedAt: when the story was first published (nulluntil then; kept if it is unpublished later). Show it as the publication date and order lists withstatus=published&order=publishedAt DESC.updatedAt: the last change, for "Updated on".firstReported: when the earliest source article was published, not your story.createdAt: when News Factory wrote the story.
Stories still being written
A workflow creates a story with its title first and writes the body a moment later, and a story can stay without one. Skip stories whose content has no real text (for example fewer than 80 words once the HTML is stripped) so a list never links to an empty page.
Account
Who the credential is and what it may do. Start here.
Who Am I
GET /v1/me · operationId getMe · scopes: any valid key
Describe the credential: the organization, the plan, the key's scopes, the Topic Workspaces it can act on, the rate limit and where the documentation lives. It answers even when the plan or the Topic Workspace would block every other endpoint, and lists the problem in problems, so call it first.
Responses
200The credential and what it may do
| Field | Type | Description |
|---|---|---|
data |
object | The description |
data.credential.type |
string | api-key, legacy-api-key (a key from before scoped keys, full access, replace it) or session (the in-app playground) |
data.credential.keyId |
string | null | Id of the API key |
data.credential.name |
string | null | Name given to the key in the app |
data.credential.scopes |
object[] | Granted scopes: { scope, description } |
data.credential.expiresAt |
string | null | When the key stops working (ISO 8601), or null |
data.credential.deprecated |
boolean | True for a legacy key |
data.organization.id |
string | Organization id |
data.organization.plan |
string | Plan slug: starter, pro, business, enterprise or free_org |
data.organization.apiIncluded |
boolean | Whether the plan includes the API (Pro, Business, Enterprise) |
data.topic.current |
string | null | Topic Workspace this request resolved to (honours X-Topic-Key) |
data.topic.boundTo |
string | null | Topic Workspace the key is bound to, or null when it may use any |
data.topic.available |
object[] | Topic Workspaces the key can act on: { key, name } |
data.topic.howToChoose |
string | How to pick a Topic Workspace with this key |
data.limits.requestsPerMinute |
number | Per-organization rate limit |
data.limits.dailyRequests |
number | null | Daily request quota, or null when the plan sets none |
data.problems |
object[] | Anything that blocks or will block requests: { code, message } (PLAN_REQUIRED, INVALID_TOPIC, NO_TOPIC_WORKSPACE, LEGACY_API_KEY) |
data.docs.reference |
string | HTML reference |
data.docs.openapi |
string | OpenAPI 3.1 specification |
data.docs.llms |
string | llms.txt index for AI agents |
data.docs.llmsFull |
string | The whole reference as one Markdown file |
{
"data": {
"credential": {
"type": "api-key",
"keyId": "68f0c1a2b3c4d5e6f7a8b9c0",
"name": "Website sync",
"scopes": [
{
"scope": "stories:read",
"description": "Read stories, their media, quality-check and SEO audit history"
},
{
"scope": "feeds:read",
"description": "Read followed feeds, the feed catalogue, feed articles and the scheduler status"
}
],
"expiresAt": null,
"deprecated": false
},
"organization": {
"id": "org_2abc123",
"plan": "pro",
"apiIncluded": true
},
"topic": {
"current": "tech",
"boundTo": "tech",
"available": [
{
"key": "tech",
"name": "Technology"
}
],
"howToChoose": "This key always acts on its Topic Workspace; X-Topic-Key is not needed."
},
"limits": {
"requestsPerMinute": 100,
"dailyRequests": null
},
"problems": [],
"docs": {
"reference": "https://client-api.news-factory.app/docs",
"openapi": "https://client-api.news-factory.app/openapi.json",
"llms": "https://client-api.news-factory.app/llms.txt",
"llmsFull": "https://client-api.news-factory.app/llms-full.txt"
}
}
}
401Unauthorized: missing, invalid, revoked or expired credentials429Rate limited: more than 100 requests in one minute per organization. Wait for the Retry-After seconds.
Example
curl -X GET "https://client-api.news-factory.app/v1/me" \
-H "Authorization: Bearer nfk_xxx..."
Stories
Read, create and publish the stories News Factory writes, with their images, translations, quality checks and SEO audits
List Stories
GET /v1/stories · operationId listStories · scopes: stories:read
Page through the Topic Workspace's original stories (translations are reached through include=translations or GET /stories/by-language/{language}). Filter with the flat params or the advanced filter JSON. The list is lightweight: content, bulletPoints, sentiment and metadata are only returned by the detail endpoints.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
language |
string (en-US, en-GB, de-DE, fr-FR, es-ES, es-MX, it-IT, pt-BR, pt-PT, nl-NL, pl-PL, sv-SE, da-DK, nb-NO, fi-FI, cs-CZ, ro-RO, hr-HR, tr-TR, ja-JP) |
no | Only stories in this language (full locale code such as en-US) |
tag |
string | no | Only stories carrying this tag (accent and case insensitive) |
search |
string | no | Full text search on title, summary and tags. Combines with the paging, order and include params. |
status |
string (draft, pending, approved, rejected, scheduled, published, archived, deleted) |
no | Only stories in this status |
dateFrom |
string | no | Only stories created on or after this instant (for publication dates use filter on publishedAt) |
dateTo |
string | no | Only stories created on or before this instant |
include |
string | no | Comma separated relations to embed: images, authors, categories, qualityChecks, seoAudits, translations, contentSources. Relations you do not ask for are never returned. images returns public Image objects with origin, aiGenerated and generation. |
limit |
number | no | Page size (default 20, max 100) |
page |
number | no | Zero-based page number (default 0) |
order |
string (createdAt DESC, createdAt ASC, publishedAt DESC, updatedAt DESC, firstReported DESC, title ASC, title DESC) |
no | Sort order as field ASC|DESC (default createdAt DESC). Allowed fields: createdAt, publishedAt, updatedAt, firstReported, title. |
filter |
string | no | Advanced LoopBack-style filter as a JSON string. Supports where (with and / or / nor, nested at most 3 levels with 20 conditions each, and the operators eq, neq, gt, gte, lt, lte, between, inq, nin, like, nlike, ilike, nilike, exists), include, order, limit, offset or skip (at most 10000), and fields. Allowed where fields: id, title, slug, summary, content, language, status, tags, thumbnail, thumbnailImageId, mainImageId, mainImageUrl, translationStatus, publishedAt, firstReported, createdAt, updatedAt. like patterns are regular expressions of at most 100 characters without groups or repetition counts; regexp and near are not supported; strings are at most 500 characters and inq / nin take at most 100 values. A filter that breaks a rule answers 400 VALIDATION_ERROR naming the rule. When present it replaces status, language, dateFrom, dateTo, include, limit, page and order (tag and search still apply). |
Responses
200A page of stories
| Field | Type | Description |
|---|---|---|
data |
Story[] | Stories, newest first by default |
data[].id |
string | Unique story identifier |
data[].title |
string | Story title |
data[].summary |
string | Short summary |
data[].slug |
string | URL-friendly identifier, unique per Topic Workspace and language |
data[].status |
string | draft, pending, approved, rejected, scheduled, published, archived or deleted |
data[].language |
string | Full locale code of the story text, e.g. en-US, de-DE, pt-BR |
data[].tags |
string[] | Tags |
data[].topicKey |
string | Topic Workspace the story belongs to |
data[].parentId |
string | null | Id of the original story when this one is a translation, else null |
data[].translationStatus |
string | null | pending, in-progress, completed or needs-update (translations only) |
data[].thumbnail |
string | URL of the current thumbnail. Match it against images[].url to learn its provenance |
data[].thumbnailImageId |
string | Id of the images[] entry backing thumbnail |
data[].mainImageId |
string | Id of the current main image, when one was generated, picked from stock or reused from the source |
data[].mainImageUrl |
string | URL of the current main image |
data[].publishedAt |
string | null | ISO 8601 timestamp of the first publication: set when status first becomes published, kept if the story is unpublished later, null until then |
data[].firstReported |
string | ISO 8601 timestamp of the earliest source publication |
data[].createdAt |
string | ISO 8601 creation timestamp |
data[].updatedAt |
string | ISO 8601 timestamp of the last update |
data[].images |
Image[] | All images attached to the story (only with include=images). Each item is a public Image object; prefer the ones with aiGenerated: true over the publisher's photo. |
data[].images[].id |
string | Unique image identifier |
data[].images[].url |
string | Public URL of the image file |
data[].images[].s3Url |
string | Deprecated alias of url with the same value; use url |
data[].images[].width |
number | Width in pixels |
data[].images[].height |
number | Height in pixels |
data[].images[].format |
string | File format (PNG, JPEG, WEBP) |
data[].images[].contentType |
string | MIME type of the image |
data[].images[].fileSize |
number | null | File size in bytes |
data[].images[].imageType |
string | Role on the story: main, thumbnail, gallery, content, other |
data[].images[].origin |
string | Normalized provenance: generated (made by News Factory with an AI image model), stock (licensed stock provider), source (taken from the original article, usually not yours to republish), upload (uploaded or imported by URL) |
data[].images[].aiGenerated |
boolean | Authoritative flag: true only when News Factory produced the image with an AI model, including crops and thumbnails derived from one. Use this (not sourceType) to pick images you can publish as your own. |
data[].images[].generation |
object | null | Present when aiGenerated is true: { provider, model, prompt } (each may be null for older images) |
data[].images[].sourceType |
string | Raw storage source (generated, stock, story, content-source, scraped, user-upload, url). Kept for compatibility; not a reliable AI indicator |
data[].images[].parentId |
string | null | Id of the image this one was cropped or resized from |
data[].images[].originalUrl |
string | null | Where the file was fetched from (article images, URL imports) |
data[].images[].title |
string | null | Image title |
data[].images[].description |
string | null | Image description |
data[].images[].altText |
string | null | Alt text |
data[].images[].caption |
string | null | Caption |
data[].images[].author |
string | null | Author or photographer |
data[].images[].source |
string | null | Attribution source: article URL, stock provider id, or AI provider id |
data[].images[].license |
string | null | License (stock images) |
data[].images[].creditLine |
string | null | Credit line to display (stock images) |
data[].images[].tags |
string[] | Tags associated with the image |
data[].images[].createdAt |
string | ISO 8601 creation timestamp |
data[].images[].updatedAt |
string | null | ISO 8601 timestamp of the last update |
data[].authors |
Author[] | Assigned authors (only with include=authors): { id, name, status, avatarUrl, bio, topicKey } |
data[].categories |
Category[] | Assigned categories (only with include=categories): { id, name, slug, description, parentId } |
data[].qualityChecks |
QualityCheck[] | Quality check history, newest first (only with include=qualityChecks). Same objects as GET /stories/{id}/content/quality-checks. |
data[].seoAudits |
SeoAudit[] | SEO audit history, newest first (only with include=seoAudits). Same objects as GET /stories/{id}/content/seo-audits. |
data[].translations |
Translation[] | Every language version of this story including itself (only with include=translations): { code, storyId, default, slug, title }. default marks the original. |
data[].contentSources |
ContentSourceRef[] | Sources the story was written from (only with include=contentSources): { id, title, link, sourceType, sourceMeta }. Add fullContentSources=true on the detail endpoints for the complete records. |
meta |
object | Pagination metadata |
meta.page |
number | Zero-based page returned |
meta.limit |
number | Page size used |
meta.hasMore |
boolean | Whether a next page may exist (the page was full) |
{
"data": [
{
"id": "68b9b7c2f1a2b3c4d5e6f700",
"title": "AI Revolution in Healthcare",
"summary": "How artificial intelligence is transforming medical diagnostics.",
"slug": "ai-revolution-in-healthcare",
"status": "published",
"language": "en-US",
"tags": [
"ai",
"healthcare",
"technology"
],
"topicKey": "tech",
"parentId": null,
"thumbnail": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/thumb_400x225.jpg",
"thumbnailImageId": "68b9b7c2f1a2b3c4d5e6f703",
"mainImageId": "68b9b7c2f1a2b3c4d5e6f702",
"mainImageUrl": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/generated_openai_1788425993651_0.png",
"publishedAt": "2026-08-15T11:45:00.000Z",
"firstReported": "2026-08-15T08:00:00.000Z",
"createdAt": "2026-08-15T10:30:00.000Z",
"updatedAt": "2026-08-15T12:00:00.000Z",
"images": [
{
"id": "68b9b7c2f1a2b3c4d5e6f701",
"url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/gettyimages-1445867611.jpg",
"s3Url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/gettyimages-1445867611.jpg",
"originalUrl": "https://techcrunch.com/wp-content/uploads/gettyimages-1445867611.jpg",
"width": 1024,
"height": 683,
"format": "JPEG",
"contentType": "image/jpeg",
"fileSize": 214532,
"imageType": "main",
"origin": "source",
"aiGenerated": false,
"generation": null,
"sourceType": "story",
"parentId": null,
"title": "AI Revolution in Healthcare",
"description": null,
"altText": null,
"caption": null,
"author": null,
"source": "https://techcrunch.com/2026/07/30/ai-healthcare/",
"license": null,
"creditLine": null,
"tags": [],
"createdAt": "2026-08-15T10:30:00.000Z",
"updatedAt": "2026-08-15T10:30:00.000Z"
},
{
"id": "68b9b7c2f1a2b3c4d5e6f702",
"url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/generated_openai_1788425993651_0.png",
"s3Url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/generated_openai_1788425993651_0.png",
"originalUrl": null,
"width": 1024,
"height": 576,
"format": "PNG",
"contentType": "image/png",
"fileSize": 1583022,
"imageType": "main",
"origin": "generated",
"aiGenerated": true,
"generation": {
"provider": "openai",
"model": "gpt-image-1",
"prompt": "Editorial illustration of AI-assisted medical diagnostics in a modern hospital"
},
"sourceType": "generated",
"parentId": null,
"title": "AI Generated Image - openai",
"description": null,
"altText": null,
"caption": null,
"author": "openai AI",
"source": "openai",
"license": null,
"creditLine": null,
"tags": [
"ai-generated",
"openai",
"healthcare"
],
"createdAt": "2026-08-15T11:02:00.000Z",
"updatedAt": "2026-08-15T11:02:00.000Z"
}
],
"translations": [
{
"code": "en-US",
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"default": true,
"slug": "ai-revolution-in-healthcare",
"title": "AI Revolution in Healthcare"
},
{
"code": "de-DE",
"storyId": "68b9b7c2f1a2b3c4d5e6f710",
"default": false,
"slug": "ki-revolution-im-gesundheitswesen",
"title": "KI-Revolution im Gesundheitswesen"
},
{
"code": "pt-BR",
"storyId": "68b9b7c2f1a2b3c4d5e6f711",
"default": false,
"slug": "revolucao-da-ia-na-saude",
"title": "Revolução da IA na saúde"
}
]
}
],
"meta": {
"page": 0,
"limit": 20,
"hasMore": true
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs429Rate limited: more than 100 requests in one minute per organization. Wait for the Retry-After seconds.
Example
curl -X GET "https://client-api.news-factory.app/v1/stories?status=published&language=en-US&include=images%2Ctranslations&limit=20" \
-H "Authorization: Bearer nfk_xxx..."
Create Story
POST /v1/stories · operationId createStory · scopes: stories:write
Create a story in the Topic Workspace. Only title is required; everything else defaults (draft status, the Topic Workspace's default language). Fields outside the documented list are ignored.
Request body (JSON): Story data
| Name | Type | Required | Description |
|---|---|---|---|
title |
string | yes | Story title |
summary |
string | no | Short summary |
content |
string | no | Full HTML body. Link another story of the Topic Workspace with <a href="/{slug}"> |
bulletPoints |
string[] | no | Key points |
language |
string | no | Full locale code (default: the Topic Workspace's default language) |
status |
string | no | draft, pending, approved, rejected, scheduled, published, archived or deleted (default draft) |
publishedAt |
string (ISO 8601) | null | no | When the story was published. Set automatically the first time status becomes published; send a date-time to backdate it, or null to clear it |
tags |
string[] | no | Tags |
slug |
string | no | URL slug (generated from the title when omitted) |
thumbnail |
string | no | Thumbnail URL |
metadata |
object | no | SEO metadata: title, description, keywords, canonical, ogImage and custom keys |
topicKey |
string | no | Topic Workspace to create the story in (default: the key's Topic Workspace or the organization's first Topic Workspace) |
{
"title": "AI Revolution in Healthcare",
"summary": "How AI is transforming medical diagnostics",
"content": "<p>Full story content here...</p>",
"language": "en-US",
"status": "draft",
"tags": [
"ai",
"healthcare"
]
}
Responses
201Story created
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data |
Story | The created story |
data.id |
string | Unique story identifier |
data.title |
string | Story title |
data.summary |
string | Short summary |
data.content |
string | Full HTML body of the story. Links to other stories are written as <a href="/{slug}"> (a slug, never a URL): point them at your own story route before rendering, as the guide Rendering stories on your site explains |
data.bulletPoints |
string[] | Key points |
data.slug |
string | URL-friendly identifier, unique per Topic Workspace and language |
data.status |
string | draft, pending, approved, rejected, scheduled, published, archived or deleted |
data.language |
string | Full locale code of the story text, e.g. en-US, de-DE, pt-BR |
data.tags |
string[] | Tags |
data.topicKey |
string | Topic Workspace the story belongs to |
data.parentId |
string | null | Id of the original story when this one is a translation, else null |
data.translationStatus |
string | null | pending, in-progress, completed or needs-update (translations only) |
data.sentiment |
number | Sentiment score from -1 (negative) to 1 (positive) |
data.thumbnail |
string | URL of the current thumbnail |
data.thumbnailImageId |
string | Id of the images[] entry backing thumbnail |
data.mainImageId |
string | Id of the current main image |
data.mainImageUrl |
string | URL of the current main image |
data.metadata |
object | SEO metadata and custom fields |
data.metadata.title |
string | Meta title |
data.metadata.description |
string | Meta description |
data.metadata.keywords |
string | Meta keywords, comma separated |
data.metadata.canonical |
string | Canonical URL |
data.metadata.ogImage |
string | Open Graph image URL |
data.publishedAt |
string | null | ISO 8601 timestamp of the first publication: set when status first becomes published, kept if the story is unpublished later, null until then |
data.firstReported |
string | ISO 8601 timestamp of the earliest source publication |
data.createdAt |
string | ISO 8601 creation timestamp |
data.updatedAt |
string | ISO 8601 timestamp of the last update |
data.images |
Image[] | All images attached to the story (only with include=images). Each item is a public Image object; prefer the ones with aiGenerated: true over the publisher's photo. |
data.images[].id |
string | Unique image identifier |
data.images[].url |
string | Public URL of the image file |
data.images[].s3Url |
string | Deprecated alias of url with the same value; use url |
data.images[].width |
number | Width in pixels |
data.images[].height |
number | Height in pixels |
data.images[].format |
string | File format (PNG, JPEG, WEBP) |
data.images[].contentType |
string | MIME type of the image |
data.images[].fileSize |
number | null | File size in bytes |
data.images[].imageType |
string | Role on the story: main, thumbnail, gallery, content, other |
data.images[].origin |
string | Normalized provenance: generated (made by News Factory with an AI image model), stock (licensed stock provider), source (taken from the original article, usually not yours to republish), upload (uploaded or imported by URL) |
data.images[].aiGenerated |
boolean | Authoritative flag: true only when News Factory produced the image with an AI model, including crops and thumbnails derived from one. Use this (not sourceType) to pick images you can publish as your own. |
data.images[].generation |
object | null | Present when aiGenerated is true: { provider, model, prompt } (each may be null for older images) |
data.images[].sourceType |
string | Raw storage source (generated, stock, story, content-source, scraped, user-upload, url). Kept for compatibility; not a reliable AI indicator |
data.images[].parentId |
string | null | Id of the image this one was cropped or resized from |
data.images[].originalUrl |
string | null | Where the file was fetched from (article images, URL imports) |
data.images[].title |
string | null | Image title |
data.images[].description |
string | null | Image description |
data.images[].altText |
string | null | Alt text |
data.images[].caption |
string | null | Caption |
data.images[].author |
string | null | Author or photographer |
data.images[].source |
string | null | Attribution source: article URL, stock provider id, or AI provider id |
data.images[].license |
string | null | License (stock images) |
data.images[].creditLine |
string | null | Credit line to display (stock images) |
data.images[].tags |
string[] | Tags associated with the image |
data.images[].createdAt |
string | ISO 8601 creation timestamp |
data.images[].updatedAt |
string | null | ISO 8601 timestamp of the last update |
data.authors |
Author[] | Assigned authors (only with include=authors): { id, name, status, avatarUrl, bio, topicKey } |
data.categories |
Category[] | Assigned categories (only with include=categories): { id, name, slug, description, parentId } |
data.qualityChecks |
QualityCheck[] | Quality check history, newest first (only with include=qualityChecks). Same objects as GET /stories/{id}/content/quality-checks. |
data.seoAudits |
SeoAudit[] | SEO audit history, newest first (only with include=seoAudits). Same objects as GET /stories/{id}/content/seo-audits. |
data.translations |
Translation[] | Every language version of this story including itself (only with include=translations): { code, storyId, default, slug, title }. default marks the original. |
data.contentSources |
ContentSourceRef[] | Sources the story was written from (only with include=contentSources): { id, title, link, sourceType, sourceMeta }. Add fullContentSources=true on the detail endpoints for the complete records. |
message |
string | Success message |
{
"success": true,
"data": {
"id": "68b9b7c2f1a2b3c4d5e6f700",
"title": "AI Revolution in Healthcare",
"summary": "How artificial intelligence is transforming medical diagnostics.",
"slug": "ai-revolution-in-healthcare",
"status": "draft",
"language": "en-US",
"tags": [
"ai",
"healthcare",
"technology"
],
"topicKey": "tech",
"parentId": null,
"thumbnail": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/thumb_400x225.jpg",
"thumbnailImageId": "68b9b7c2f1a2b3c4d5e6f703",
"mainImageId": "68b9b7c2f1a2b3c4d5e6f702",
"mainImageUrl": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/generated_openai_1788425993651_0.png",
"publishedAt": null,
"firstReported": "2026-08-15T08:00:00.000Z",
"createdAt": "2026-08-15T10:30:00.000Z",
"updatedAt": "2026-08-15T12:00:00.000Z",
"content": "<h3>Overview</h3><p>A comprehensive look at how AI is being deployed across diagnostics, triage and treatment planning.</p>",
"bulletPoints": [
"AI diagnostics accuracy reaches 95%",
"Cost reduction of 40% in pilot hospitals"
],
"sentiment": 0.8,
"metadata": {
"title": "AI Revolution in Healthcare: What Changes in 2026",
"description": "AI is transforming diagnostics, triage and treatment planning. Here is what hospitals report.",
"keywords": "ai, healthcare, diagnostics"
}
},
"message": "Story created successfully"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X POST "https://client-api.news-factory.app/v1/stories" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"title":"AI Revolution in Healthcare","summary":"How AI is transforming medical diagnostics","content":"<p>Full story content here...</p>","language":"en-US","status":"draft","tags":["ai","healthcare"]}'
List Stories by Language
GET /v1/stories/by-language/{language} · operationId listStoriesByLanguage · scopes: stories:read
Page through every story written in one language, originals and translations alike, with a stable order. Ideal for building a per-language site: pair it with include=translations to render language switchers. Every story carries its relatedStories in that language, so a static site can render Read next blocks without a call per story.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
language |
string (en-US, en-GB, de-DE, fr-FR, es-ES, es-MX, it-IT, pt-BR, pt-PT, nl-NL, pl-PL, sv-SE, da-DK, nb-NO, fi-FI, cs-CZ, ro-RO, hr-HR, tr-TR, ja-JP) |
yes | Full locale code, e.g. en-US, de-DE, pt-BR |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
status |
string (draft, pending, approved, rejected, scheduled, published, archived, deleted) |
no | Only stories in this status |
dateFrom |
string | no | Only stories created on or after this instant (for publication dates use filter on publishedAt) |
dateTo |
string | no | Only stories created on or before this instant |
include |
string | no | Comma separated relations to embed: images, authors, categories, qualityChecks, seoAudits, translations, contentSources. Relations you do not ask for are never returned. images returns public Image objects with origin, aiGenerated and generation. |
limit |
number | no | Page size (default 20, max 100) |
page |
number | no | Zero-based page number (default 0) |
order |
string (createdAt DESC, createdAt ASC, publishedAt DESC, updatedAt DESC, firstReported DESC, title ASC, title DESC) |
no | Sort order as field ASC|DESC (default createdAt DESC). Allowed fields: createdAt, publishedAt, updatedAt, firstReported, title. |
filter |
string | no | Advanced LoopBack-style filter as a JSON string. Supports where (with and / or / nor, nested at most 3 levels with 20 conditions each, and the operators eq, neq, gt, gte, lt, lte, between, inq, nin, like, nlike, ilike, nilike, exists), include, order, limit, offset or skip (at most 10000), and fields. Allowed where fields: id, title, slug, summary, content, language, status, tags, thumbnail, thumbnailImageId, mainImageId, mainImageUrl, translationStatus, publishedAt, firstReported, createdAt, updatedAt. like patterns are regular expressions of at most 100 characters without groups or repetition counts; regexp and near are not supported; strings are at most 500 characters and inq / nin take at most 100 values. A filter that breaks a rule answers 400 VALIDATION_ERROR naming the rule. When present it replaces status, language, dateFrom, dateTo, include, limit, page and order (tag and search still apply). |
Responses
200A page of stories in that language
| Field | Type | Description |
|---|---|---|
data |
Story[] | Stories in the requested language |
data[].id |
string | Unique story identifier |
data[].title |
string | Story title |
data[].summary |
string | Short summary |
data[].slug |
string | URL-friendly identifier, unique per Topic Workspace and language |
data[].status |
string | draft, pending, approved, rejected, scheduled, published, archived or deleted |
data[].language |
string | Full locale code of the story text, e.g. en-US, de-DE, pt-BR |
data[].tags |
string[] | Tags |
data[].topicKey |
string | Topic Workspace the story belongs to |
data[].parentId |
string | null | Id of the original story when this one is a translation, else null |
data[].translationStatus |
string | null | pending, in-progress, completed or needs-update (translations only) |
data[].thumbnail |
string | URL of the current thumbnail. Match it against images[].url to learn its provenance |
data[].thumbnailImageId |
string | Id of the images[] entry backing thumbnail |
data[].mainImageId |
string | Id of the current main image, when one was generated, picked from stock or reused from the source |
data[].mainImageUrl |
string | URL of the current main image |
data[].publishedAt |
string | null | ISO 8601 timestamp of the first publication: set when status first becomes published, kept if the story is unpublished later, null until then |
data[].firstReported |
string | ISO 8601 timestamp of the earliest source publication |
data[].createdAt |
string | ISO 8601 creation timestamp |
data[].updatedAt |
string | ISO 8601 timestamp of the last update |
data[].images |
Image[] | All images attached to the story (only with include=images). Each item is a public Image object; prefer the ones with aiGenerated: true over the publisher's photo. |
data[].images[].id |
string | Unique image identifier |
data[].images[].url |
string | Public URL of the image file |
data[].images[].s3Url |
string | Deprecated alias of url with the same value; use url |
data[].images[].width |
number | Width in pixels |
data[].images[].height |
number | Height in pixels |
data[].images[].format |
string | File format (PNG, JPEG, WEBP) |
data[].images[].contentType |
string | MIME type of the image |
data[].images[].fileSize |
number | null | File size in bytes |
data[].images[].imageType |
string | Role on the story: main, thumbnail, gallery, content, other |
data[].images[].origin |
string | Normalized provenance: generated (made by News Factory with an AI image model), stock (licensed stock provider), source (taken from the original article, usually not yours to republish), upload (uploaded or imported by URL) |
data[].images[].aiGenerated |
boolean | Authoritative flag: true only when News Factory produced the image with an AI model, including crops and thumbnails derived from one. Use this (not sourceType) to pick images you can publish as your own. |
data[].images[].generation |
object | null | Present when aiGenerated is true: { provider, model, prompt } (each may be null for older images) |
data[].images[].sourceType |
string | Raw storage source (generated, stock, story, content-source, scraped, user-upload, url). Kept for compatibility; not a reliable AI indicator |
data[].images[].parentId |
string | null | Id of the image this one was cropped or resized from |
data[].images[].originalUrl |
string | null | Where the file was fetched from (article images, URL imports) |
data[].images[].title |
string | null | Image title |
data[].images[].description |
string | null | Image description |
data[].images[].altText |
string | null | Alt text |
data[].images[].caption |
string | null | Caption |
data[].images[].author |
string | null | Author or photographer |
data[].images[].source |
string | null | Attribution source: article URL, stock provider id, or AI provider id |
data[].images[].license |
string | null | License (stock images) |
data[].images[].creditLine |
string | null | Credit line to display (stock images) |
data[].images[].tags |
string[] | Tags associated with the image |
data[].images[].createdAt |
string | ISO 8601 creation timestamp |
data[].images[].updatedAt |
string | null | ISO 8601 timestamp of the last update |
data[].authors |
Author[] | Assigned authors (only with include=authors): { id, name, status, avatarUrl, bio, topicKey } |
data[].categories |
Category[] | Assigned categories (only with include=categories): { id, name, slug, description, parentId } |
data[].qualityChecks |
QualityCheck[] | Quality check history, newest first (only with include=qualityChecks). Same objects as GET /stories/{id}/content/quality-checks. |
data[].seoAudits |
SeoAudit[] | SEO audit history, newest first (only with include=seoAudits). Same objects as GET /stories/{id}/content/seo-audits. |
data[].translations |
Translation[] | Every language version of this story including itself (only with include=translations): { code, storyId, default, slug, title }. default marks the original. |
data[].contentSources |
ContentSourceRef[] | Sources the story was written from (only with include=contentSources): { id, title, link, sourceType, sourceMeta }. Add fullContentSources=true on the detail endpoints for the complete records. |
data[].relatedStories |
RelatedStory[] | Up to 5 published stories of the same Topic Workspace and language that are closest to this one in meaning, closest first: ready for a Read next block. Always returned, no include needed. Found when the story is published (within about a minute) and again when a published story gets a new title or summary; empty until then and when no story is close enough. Only stories published right now are listed, with their current title and slug, and never another language version of this story. |
data[].relatedStories[].id |
string | Id of the related story |
data[].relatedStories[].title |
string | Its current title |
data[].relatedStories[].slug |
string | Its slug. Point it at your own story route, like the links inside content |
data[].relatedStories[].summary |
string | Its short summary |
data[].relatedStories[].language |
string | Full locale code, always the language of this story |
data[].relatedStories[].publishedAt |
string | null | ISO 8601 timestamp of its first publication |
data[].relatedStories[].mainImageUrl |
string | null | URL of its main image, when it has one |
data[].relatedStories[].thumbnail |
string | null | URL of its thumbnail, often the source publisher's photo |
data[].relatedStories[].score |
number | Similarity to this story from 0 to 1, higher is closer. Listed stories score 0.7 or more |
meta |
object | Pagination metadata |
meta.page |
number | Zero-based page returned |
meta.limit |
number | Page size used |
meta.hasMore |
boolean | Whether a next page may exist (the page was full) |
meta.language |
string | The language requested |
{
"data": [
{
"id": "68b9b7c2f1a2b3c4d5e6f700",
"title": "AI Revolution in Healthcare",
"summary": "How artificial intelligence is transforming medical diagnostics.",
"slug": "ai-revolution-in-healthcare",
"status": "published",
"language": "en-US",
"tags": [
"ai",
"healthcare",
"technology"
],
"topicKey": "tech",
"parentId": null,
"thumbnail": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/thumb_400x225.jpg",
"thumbnailImageId": "68b9b7c2f1a2b3c4d5e6f703",
"mainImageId": "68b9b7c2f1a2b3c4d5e6f702",
"mainImageUrl": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/generated_openai_1788425993651_0.png",
"publishedAt": "2026-08-15T11:45:00.000Z",
"firstReported": "2026-08-15T08:00:00.000Z",
"createdAt": "2026-08-15T10:30:00.000Z",
"updatedAt": "2026-08-15T12:00:00.000Z",
"translations": [
{
"code": "en-US",
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"default": true,
"slug": "ai-revolution-in-healthcare",
"title": "AI Revolution in Healthcare"
},
{
"code": "de-DE",
"storyId": "68b9b7c2f1a2b3c4d5e6f710",
"default": false,
"slug": "ki-revolution-im-gesundheitswesen",
"title": "KI-Revolution im Gesundheitswesen"
},
{
"code": "pt-BR",
"storyId": "68b9b7c2f1a2b3c4d5e6f711",
"default": false,
"slug": "revolucao-da-ia-na-saude",
"title": "Revolução da IA na saúde"
}
],
"relatedStories": [
{
"id": "68b9b7c2f1a2b3c4d5e6f740",
"title": "Hospitals Cut Waiting Times with AI Triage",
"slug": "hospitals-cut-waiting-times-with-ai-triage",
"summary": "Emergency departments using AI triage report shorter waits and fewer missed cases.",
"language": "en-US",
"publishedAt": "2026-08-12T09:10:00.000Z",
"mainImageUrl": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f740/generated_openai_1788125993651_0.png",
"thumbnail": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f740/thumb_400x225.jpg",
"score": 0.8412
},
{
"id": "68b9b7c2f1a2b3c4d5e6f741",
"title": "Regulators Draft Rules for Diagnostic AI",
"slug": "regulators-draft-rules-for-diagnostic-ai",
"summary": "A draft framework would require clinical trials before diagnostic models reach patients.",
"language": "en-US",
"publishedAt": "2026-08-09T15:30:00.000Z",
"mainImageUrl": null,
"thumbnail": null,
"score": 0.7761
}
]
}
],
"meta": {
"page": 0,
"limit": 20,
"hasMore": false,
"language": "en-US"
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs
Example
curl -X GET "https://client-api.news-factory.app/v1/stories/by-language/en-US?include=translations&limit=20" \
-H "Authorization: Bearer nfk_xxx..."
Translation Map
GET /v1/stories/translation-map · operationId translationMap · scopes: stories:read
A compact map from each story to all of its language versions, without loading the stories themselves. Use it to generate hreflang tags or language switchers. With lang, entries are anchored on the stories in that language so the map lines up with GET /stories/by-language/{language}.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
lang |
string (en-US, en-GB, de-DE, fr-FR, es-ES, es-MX, it-IT, pt-BR, pt-PT, nl-NL, pl-PL, sv-SE, da-DK, nb-NO, fi-FI, cs-CZ, ro-RO, hr-HR, tr-TR, ja-JP) |
no | Anchor the map on stories in this language (full locale code) |
limit |
number | no | Maximum entries (default 500, max 1000) |
Responses
200The map
| Field | Type | Description |
|---|---|---|
data |
object[] | One entry per anchor story |
data[].id |
string | Story id |
data[].translations |
object[] | Every language version of the family, the original first: { id, code, slug } |
meta.count |
number | Entries returned |
meta.lang |
string | null | Language filter applied |
meta.limit |
number | Limit applied |
{
"data": [
{
"id": "68b9b7c2f1a2b3c4d5e6f700",
"translations": [
{
"id": "68b9b7c2f1a2b3c4d5e6f700",
"code": "en-US",
"slug": "ai-revolution-in-healthcare"
},
{
"id": "68b9b7c2f1a2b3c4d5e6f710",
"code": "de-DE",
"slug": "ki-revolution-im-gesundheitswesen"
},
{
"id": "68b9b7c2f1a2b3c4d5e6f711",
"code": "pt-BR",
"slug": "revolucao-da-ia-na-saude"
}
]
}
],
"meta": {
"count": 1,
"lang": "en-US",
"limit": 500
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs
Example
curl -X GET "https://client-api.news-factory.app/v1/stories/translation-map?lang=en-US&limit=200" \
-H "Authorization: Bearer nfk_xxx..."
Get Story
GET /v1/stories/{id} · operationId getStoryById · scopes: stories:read
One story with its full HTML content, key points, sentiment, SEO metadata and relatedStories: up to 5 published stories in the same language that are closest to it, for a Read next block. Add relations with include.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The story id |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
include |
string | no | Comma separated relations to embed: images, authors, categories, qualityChecks, seoAudits, translations, contentSources. Relations you do not ask for are never returned. images returns public Image objects with origin, aiGenerated and generation. |
fullContentSources |
boolean | no | With include=contentSources, return the complete source records instead of the short references |
Responses
200The story
| Field | Type | Description |
|---|---|---|
data |
Story | The story with its full content |
data.id |
string | Unique story identifier |
data.title |
string | Story title |
data.summary |
string | Short summary |
data.content |
string | Full HTML body of the story. Links to other stories are written as <a href="/{slug}"> (a slug, never a URL): point them at your own story route before rendering, as the guide Rendering stories on your site explains |
data.bulletPoints |
string[] | Key points |
data.slug |
string | URL-friendly identifier, unique per Topic Workspace and language |
data.status |
string | draft, pending, approved, rejected, scheduled, published, archived or deleted |
data.language |
string | Full locale code of the story text, e.g. en-US, de-DE, pt-BR |
data.tags |
string[] | Tags |
data.topicKey |
string | Topic Workspace the story belongs to |
data.parentId |
string | null | Id of the original story when this one is a translation, else null |
data.translationStatus |
string | null | pending, in-progress, completed or needs-update (translations only) |
data.sentiment |
number | Sentiment score from -1 (negative) to 1 (positive) |
data.thumbnail |
string | URL of the current thumbnail |
data.thumbnailImageId |
string | Id of the images[] entry backing thumbnail |
data.mainImageId |
string | Id of the current main image |
data.mainImageUrl |
string | URL of the current main image |
data.metadata |
object | SEO metadata and custom fields |
data.metadata.title |
string | Meta title |
data.metadata.description |
string | Meta description |
data.metadata.keywords |
string | Meta keywords, comma separated |
data.metadata.canonical |
string | Canonical URL |
data.metadata.ogImage |
string | Open Graph image URL |
data.publishedAt |
string | null | ISO 8601 timestamp of the first publication: set when status first becomes published, kept if the story is unpublished later, null until then |
data.firstReported |
string | ISO 8601 timestamp of the earliest source publication |
data.createdAt |
string | ISO 8601 creation timestamp |
data.updatedAt |
string | ISO 8601 timestamp of the last update |
data.images |
Image[] | All images attached to the story (only with include=images). Each item is a public Image object; prefer the ones with aiGenerated: true over the publisher's photo. |
data.images[].id |
string | Unique image identifier |
data.images[].url |
string | Public URL of the image file |
data.images[].s3Url |
string | Deprecated alias of url with the same value; use url |
data.images[].width |
number | Width in pixels |
data.images[].height |
number | Height in pixels |
data.images[].format |
string | File format (PNG, JPEG, WEBP) |
data.images[].contentType |
string | MIME type of the image |
data.images[].fileSize |
number | null | File size in bytes |
data.images[].imageType |
string | Role on the story: main, thumbnail, gallery, content, other |
data.images[].origin |
string | Normalized provenance: generated (made by News Factory with an AI image model), stock (licensed stock provider), source (taken from the original article, usually not yours to republish), upload (uploaded or imported by URL) |
data.images[].aiGenerated |
boolean | Authoritative flag: true only when News Factory produced the image with an AI model, including crops and thumbnails derived from one. Use this (not sourceType) to pick images you can publish as your own. |
data.images[].generation |
object | null | Present when aiGenerated is true: { provider, model, prompt } (each may be null for older images) |
data.images[].sourceType |
string | Raw storage source (generated, stock, story, content-source, scraped, user-upload, url). Kept for compatibility; not a reliable AI indicator |
data.images[].parentId |
string | null | Id of the image this one was cropped or resized from |
data.images[].originalUrl |
string | null | Where the file was fetched from (article images, URL imports) |
data.images[].title |
string | null | Image title |
data.images[].description |
string | null | Image description |
data.images[].altText |
string | null | Alt text |
data.images[].caption |
string | null | Caption |
data.images[].author |
string | null | Author or photographer |
data.images[].source |
string | null | Attribution source: article URL, stock provider id, or AI provider id |
data.images[].license |
string | null | License (stock images) |
data.images[].creditLine |
string | null | Credit line to display (stock images) |
data.images[].tags |
string[] | Tags associated with the image |
data.images[].createdAt |
string | ISO 8601 creation timestamp |
data.images[].updatedAt |
string | null | ISO 8601 timestamp of the last update |
data.authors |
Author[] | Assigned authors (only with include=authors): { id, name, status, avatarUrl, bio, topicKey } |
data.categories |
Category[] | Assigned categories (only with include=categories): { id, name, slug, description, parentId } |
data.qualityChecks |
QualityCheck[] | Quality check history, newest first (only with include=qualityChecks). Same objects as GET /stories/{id}/content/quality-checks. |
data.seoAudits |
SeoAudit[] | SEO audit history, newest first (only with include=seoAudits). Same objects as GET /stories/{id}/content/seo-audits. |
data.translations |
Translation[] | Every language version of this story including itself (only with include=translations): { code, storyId, default, slug, title }. default marks the original. |
data.contentSources |
ContentSourceRef[] | Sources the story was written from (only with include=contentSources): { id, title, link, sourceType, sourceMeta }. Add fullContentSources=true on the detail endpoints for the complete records. |
data.relatedStories |
RelatedStory[] | Up to 5 published stories of the same Topic Workspace and language that are closest to this one in meaning, closest first: ready for a Read next block. Always returned, no include needed. Found when the story is published (within about a minute) and again when a published story gets a new title or summary; empty until then and when no story is close enough. Only stories published right now are listed, with their current title and slug, and never another language version of this story. |
data.relatedStories[].id |
string | Id of the related story |
data.relatedStories[].title |
string | Its current title |
data.relatedStories[].slug |
string | Its slug. Point it at your own story route, like the links inside content |
data.relatedStories[].summary |
string | Its short summary |
data.relatedStories[].language |
string | Full locale code, always the language of this story |
data.relatedStories[].publishedAt |
string | null | ISO 8601 timestamp of its first publication |
data.relatedStories[].mainImageUrl |
string | null | URL of its main image, when it has one |
data.relatedStories[].thumbnail |
string | null | URL of its thumbnail, often the source publisher's photo |
data.relatedStories[].score |
number | Similarity to this story from 0 to 1, higher is closer. Listed stories score 0.7 or more |
{
"data": {
"id": "68b9b7c2f1a2b3c4d5e6f700",
"title": "AI Revolution in Healthcare",
"summary": "How artificial intelligence is transforming medical diagnostics.",
"slug": "ai-revolution-in-healthcare",
"status": "published",
"language": "en-US",
"tags": [
"ai",
"healthcare",
"technology"
],
"topicKey": "tech",
"parentId": null,
"thumbnail": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/thumb_400x225.jpg",
"thumbnailImageId": "68b9b7c2f1a2b3c4d5e6f703",
"mainImageId": "68b9b7c2f1a2b3c4d5e6f702",
"mainImageUrl": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/generated_openai_1788425993651_0.png",
"publishedAt": "2026-08-15T11:45:00.000Z",
"firstReported": "2026-08-15T08:00:00.000Z",
"createdAt": "2026-08-15T10:30:00.000Z",
"updatedAt": "2026-08-15T12:00:00.000Z",
"content": "<h3>Overview</h3><p>A comprehensive look at how AI is being deployed across diagnostics, triage and treatment planning.</p>",
"bulletPoints": [
"AI diagnostics accuracy reaches 95%",
"Cost reduction of 40% in pilot hospitals"
],
"sentiment": 0.8,
"metadata": {
"title": "AI Revolution in Healthcare: What Changes in 2026",
"description": "AI is transforming diagnostics, triage and treatment planning. Here is what hospitals report.",
"keywords": "ai, healthcare, diagnostics"
},
"relatedStories": [
{
"id": "68b9b7c2f1a2b3c4d5e6f740",
"title": "Hospitals Cut Waiting Times with AI Triage",
"slug": "hospitals-cut-waiting-times-with-ai-triage",
"summary": "Emergency departments using AI triage report shorter waits and fewer missed cases.",
"language": "en-US",
"publishedAt": "2026-08-12T09:10:00.000Z",
"mainImageUrl": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f740/generated_openai_1788125993651_0.png",
"thumbnail": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f740/thumb_400x225.jpg",
"score": 0.8412
},
{
"id": "68b9b7c2f1a2b3c4d5e6f741",
"title": "Regulators Draft Rules for Diagnostic AI",
"slug": "regulators-draft-rules-for-diagnostic-ai",
"summary": "A draft framework would require clinical trials before diagnostic models reach patients.",
"language": "en-US",
"publishedAt": "2026-08-09T15:30:00.000Z",
"mainImageUrl": null,
"thumbnail": null,
"score": 0.7761
}
],
"images": [
{
"id": "68b9b7c2f1a2b3c4d5e6f701",
"url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/gettyimages-1445867611.jpg",
"s3Url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/gettyimages-1445867611.jpg",
"originalUrl": "https://techcrunch.com/wp-content/uploads/gettyimages-1445867611.jpg",
"width": 1024,
"height": 683,
"format": "JPEG",
"contentType": "image/jpeg",
"fileSize": 214532,
"imageType": "main",
"origin": "source",
"aiGenerated": false,
"generation": null,
"sourceType": "story",
"parentId": null,
"title": "AI Revolution in Healthcare",
"description": null,
"altText": null,
"caption": null,
"author": null,
"source": "https://techcrunch.com/2026/07/30/ai-healthcare/",
"license": null,
"creditLine": null,
"tags": [],
"createdAt": "2026-08-15T10:30:00.000Z",
"updatedAt": "2026-08-15T10:30:00.000Z"
},
{
"id": "68b9b7c2f1a2b3c4d5e6f702",
"url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/generated_openai_1788425993651_0.png",
"s3Url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/generated_openai_1788425993651_0.png",
"originalUrl": null,
"width": 1024,
"height": 576,
"format": "PNG",
"contentType": "image/png",
"fileSize": 1583022,
"imageType": "main",
"origin": "generated",
"aiGenerated": true,
"generation": {
"provider": "openai",
"model": "gpt-image-1",
"prompt": "Editorial illustration of AI-assisted medical diagnostics in a modern hospital"
},
"sourceType": "generated",
"parentId": null,
"title": "AI Generated Image - openai",
"description": null,
"altText": null,
"caption": null,
"author": "openai AI",
"source": "openai",
"license": null,
"creditLine": null,
"tags": [
"ai-generated",
"openai",
"healthcare"
],
"createdAt": "2026-08-15T11:02:00.000Z",
"updatedAt": "2026-08-15T11:02:00.000Z"
}
],
"authors": [
{
"id": "68b9b7c2f1a2b3c4d5e6f720",
"name": "Jane Doe",
"status": "active",
"avatarUrl": "https://cdn.news-factory.app/authors/jane.jpg",
"topicKey": "tech"
}
],
"categories": [
{
"id": "68b9b7c2f1a2b3c4d5e6f730",
"name": "Health Tech",
"slug": "health-tech",
"parentId": null
}
],
"translations": [
{
"code": "en-US",
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"default": true,
"slug": "ai-revolution-in-healthcare",
"title": "AI Revolution in Healthcare"
},
{
"code": "de-DE",
"storyId": "68b9b7c2f1a2b3c4d5e6f710",
"default": false,
"slug": "ki-revolution-im-gesundheitswesen",
"title": "KI-Revolution im Gesundheitswesen"
},
{
"code": "pt-BR",
"storyId": "68b9b7c2f1a2b3c4d5e6f711",
"default": false,
"slug": "revolucao-da-ia-na-saude",
"title": "Revolução da IA na saúde"
}
]
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found
Example
curl -X GET "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700?include=images%2Cauthors%2Ccategories%2Ctranslations" \
-H "Authorization: Bearer nfk_xxx..."
Update Story
PATCH /v1/stories/{id} · operationId updateStory · scopes: stories:write
Update the provided fields and return the fresh story. Arrays such as tags replace the existing value. Publishing is a status change: set status to published. The related stories of a story you publish (or retitle) are found within about a minute, so the response still shows the previous relatedStories: read the story again later for the new ones.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The story id |
Request body (JSON): Fields to update (at least one)
| Name | Type | Required | Description |
|---|---|---|---|
title |
string | no | Story title |
summary |
string | no | Short summary |
content |
string | no | Full HTML body. Link another story of the Topic Workspace with <a href="/{slug}"> |
bulletPoints |
string[] | no | Key points |
language |
string | no | Full locale code (default: the Topic Workspace's default language) |
status |
string | no | draft, pending, approved, rejected, scheduled, published, archived or deleted (default draft) |
publishedAt |
string (ISO 8601) | null | no | When the story was published. Set automatically the first time status becomes published; send a date-time to backdate it, or null to clear it |
tags |
string[] | no | Tags |
slug |
string | no | URL slug (generated from the title when omitted) |
thumbnail |
string | no | Thumbnail URL |
metadata |
object | no | SEO metadata: title, description, keywords, canonical, ogImage and custom keys |
{
"title": "AI Revolution in Healthcare: 2026 Update",
"status": "published",
"tags": [
"ai",
"healthcare",
"2026"
]
}
Responses
200Story updated
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data |
Story | The updated story (no relations) |
data.id |
string | Unique story identifier |
data.title |
string | Story title |
data.summary |
string | Short summary |
data.content |
string | Full HTML body of the story. Links to other stories are written as <a href="/{slug}"> (a slug, never a URL): point them at your own story route before rendering, as the guide Rendering stories on your site explains |
data.bulletPoints |
string[] | Key points |
data.slug |
string | URL-friendly identifier, unique per Topic Workspace and language |
data.status |
string | draft, pending, approved, rejected, scheduled, published, archived or deleted |
data.language |
string | Full locale code of the story text, e.g. en-US, de-DE, pt-BR |
data.tags |
string[] | Tags |
data.topicKey |
string | Topic Workspace the story belongs to |
data.parentId |
string | null | Id of the original story when this one is a translation, else null |
data.translationStatus |
string | null | pending, in-progress, completed or needs-update (translations only) |
data.sentiment |
number | Sentiment score from -1 (negative) to 1 (positive) |
data.thumbnail |
string | URL of the current thumbnail |
data.thumbnailImageId |
string | Id of the images[] entry backing thumbnail |
data.mainImageId |
string | Id of the current main image |
data.mainImageUrl |
string | URL of the current main image |
data.metadata |
object | SEO metadata and custom fields |
data.metadata.title |
string | Meta title |
data.metadata.description |
string | Meta description |
data.metadata.keywords |
string | Meta keywords, comma separated |
data.metadata.canonical |
string | Canonical URL |
data.metadata.ogImage |
string | Open Graph image URL |
data.publishedAt |
string | null | ISO 8601 timestamp of the first publication: set when status first becomes published, kept if the story is unpublished later, null until then |
data.firstReported |
string | ISO 8601 timestamp of the earliest source publication |
data.createdAt |
string | ISO 8601 creation timestamp |
data.updatedAt |
string | ISO 8601 timestamp of the last update |
data.relatedStories |
RelatedStory[] | Up to 5 published stories of the same Topic Workspace and language that are closest to this one in meaning, closest first: ready for a Read next block. Always returned, no include needed. Found when the story is published (within about a minute) and again when a published story gets a new title or summary; empty until then and when no story is close enough. Only stories published right now are listed, with their current title and slug, and never another language version of this story. |
message |
string | Success message |
{
"success": true,
"data": {
"id": "68b9b7c2f1a2b3c4d5e6f700",
"title": "AI Revolution in Healthcare: 2026 Update",
"summary": "How artificial intelligence is transforming medical diagnostics.",
"slug": "ai-revolution-in-healthcare",
"status": "published",
"language": "en-US",
"tags": [
"ai",
"healthcare",
"technology"
],
"topicKey": "tech",
"parentId": null,
"thumbnail": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/thumb_400x225.jpg",
"thumbnailImageId": "68b9b7c2f1a2b3c4d5e6f703",
"mainImageId": "68b9b7c2f1a2b3c4d5e6f702",
"mainImageUrl": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/generated_openai_1788425993651_0.png",
"publishedAt": "2026-08-15T11:45:00.000Z",
"firstReported": "2026-08-15T08:00:00.000Z",
"createdAt": "2026-08-15T10:30:00.000Z",
"updatedAt": "2026-08-15T12:00:00.000Z",
"content": "<h3>Overview</h3><p>A comprehensive look at how AI is being deployed across diagnostics, triage and treatment planning.</p>",
"bulletPoints": [
"AI diagnostics accuracy reaches 95%",
"Cost reduction of 40% in pilot hospitals"
],
"sentiment": 0.8,
"metadata": {
"title": "AI Revolution in Healthcare: What Changes in 2026",
"description": "AI is transforming diagnostics, triage and treatment planning. Here is what hospitals report.",
"keywords": "ai, healthcare, diagnostics"
},
"relatedStories": [
{
"id": "68b9b7c2f1a2b3c4d5e6f740",
"title": "Hospitals Cut Waiting Times with AI Triage",
"slug": "hospitals-cut-waiting-times-with-ai-triage",
"summary": "Emergency departments using AI triage report shorter waits and fewer missed cases.",
"language": "en-US",
"publishedAt": "2026-08-12T09:10:00.000Z",
"mainImageUrl": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f740/generated_openai_1788125993651_0.png",
"thumbnail": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f740/thumb_400x225.jpg",
"score": 0.8412
},
{
"id": "68b9b7c2f1a2b3c4d5e6f741",
"title": "Regulators Draft Rules for Diagnostic AI",
"slug": "regulators-draft-rules-for-diagnostic-ai",
"summary": "A draft framework would require clinical trials before diagnostic models reach patients.",
"language": "en-US",
"publishedAt": "2026-08-09T15:30:00.000Z",
"mainImageUrl": null,
"thumbnail": null,
"score": 0.7761
}
]
},
"message": "Story updated successfully"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found
Example
curl -X PATCH "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"title":"AI Revolution in Healthcare: 2026 Update","status":"published","tags":["ai","healthcare","2026"]}'
Delete Story
DELETE /v1/stories/{id} · operationId deleteStory · scopes: stories:write
Delete a story permanently. Images that no other story or source uses are removed from storage as well. Translations of the story are not deleted.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The story id |
Responses
200Story deleted
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
message |
string | Success message |
{
"success": true,
"message": "Story deleted successfully"
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X DELETE "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700" \
-H "Authorization: Bearer nfk_xxx..."
Get Story by Slug
GET /v1/stories/slug/{slug} · operationId getStoryBySlug · scopes: stories:read
The same detail as GET /stories/{id}, related stories included, looked up by the story's URL slug. Slugs are unique per Topic Workspace and language, so send the X-Topic-Key header when your key spans Topic Workspaces.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
slug |
string | yes | The story slug |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
include |
string | no | Comma separated relations to embed: images, authors, categories, qualityChecks, seoAudits, translations, contentSources. Relations you do not ask for are never returned. images returns public Image objects with origin, aiGenerated and generation. |
fullContentSources |
boolean | no | With include=contentSources, return the complete source records instead of the short references |
Responses
200The story
| Field | Type | Description |
|---|---|---|
data |
Story | The story with its full content |
data.id |
string | Unique story identifier |
data.title |
string | Story title |
data.summary |
string | Short summary |
data.content |
string | Full HTML body of the story. Links to other stories are written as <a href="/{slug}"> (a slug, never a URL): point them at your own story route before rendering, as the guide Rendering stories on your site explains |
data.bulletPoints |
string[] | Key points |
data.slug |
string | URL-friendly identifier, unique per Topic Workspace and language |
data.status |
string | draft, pending, approved, rejected, scheduled, published, archived or deleted |
data.language |
string | Full locale code of the story text, e.g. en-US, de-DE, pt-BR |
data.tags |
string[] | Tags |
data.topicKey |
string | Topic Workspace the story belongs to |
data.parentId |
string | null | Id of the original story when this one is a translation, else null |
data.translationStatus |
string | null | pending, in-progress, completed or needs-update (translations only) |
data.sentiment |
number | Sentiment score from -1 (negative) to 1 (positive) |
data.thumbnail |
string | URL of the current thumbnail |
data.thumbnailImageId |
string | Id of the images[] entry backing thumbnail |
data.mainImageId |
string | Id of the current main image |
data.mainImageUrl |
string | URL of the current main image |
data.metadata |
object | SEO metadata and custom fields |
data.metadata.title |
string | Meta title |
data.metadata.description |
string | Meta description |
data.metadata.keywords |
string | Meta keywords, comma separated |
data.metadata.canonical |
string | Canonical URL |
data.metadata.ogImage |
string | Open Graph image URL |
data.publishedAt |
string | null | ISO 8601 timestamp of the first publication: set when status first becomes published, kept if the story is unpublished later, null until then |
data.firstReported |
string | ISO 8601 timestamp of the earliest source publication |
data.createdAt |
string | ISO 8601 creation timestamp |
data.updatedAt |
string | ISO 8601 timestamp of the last update |
data.images |
Image[] | All images attached to the story (only with include=images). Each item is a public Image object; prefer the ones with aiGenerated: true over the publisher's photo. |
data.images[].id |
string | Unique image identifier |
data.images[].url |
string | Public URL of the image file |
data.images[].s3Url |
string | Deprecated alias of url with the same value; use url |
data.images[].width |
number | Width in pixels |
data.images[].height |
number | Height in pixels |
data.images[].format |
string | File format (PNG, JPEG, WEBP) |
data.images[].contentType |
string | MIME type of the image |
data.images[].fileSize |
number | null | File size in bytes |
data.images[].imageType |
string | Role on the story: main, thumbnail, gallery, content, other |
data.images[].origin |
string | Normalized provenance: generated (made by News Factory with an AI image model), stock (licensed stock provider), source (taken from the original article, usually not yours to republish), upload (uploaded or imported by URL) |
data.images[].aiGenerated |
boolean | Authoritative flag: true only when News Factory produced the image with an AI model, including crops and thumbnails derived from one. Use this (not sourceType) to pick images you can publish as your own. |
data.images[].generation |
object | null | Present when aiGenerated is true: { provider, model, prompt } (each may be null for older images) |
data.images[].sourceType |
string | Raw storage source (generated, stock, story, content-source, scraped, user-upload, url). Kept for compatibility; not a reliable AI indicator |
data.images[].parentId |
string | null | Id of the image this one was cropped or resized from |
data.images[].originalUrl |
string | null | Where the file was fetched from (article images, URL imports) |
data.images[].title |
string | null | Image title |
data.images[].description |
string | null | Image description |
data.images[].altText |
string | null | Alt text |
data.images[].caption |
string | null | Caption |
data.images[].author |
string | null | Author or photographer |
data.images[].source |
string | null | Attribution source: article URL, stock provider id, or AI provider id |
data.images[].license |
string | null | License (stock images) |
data.images[].creditLine |
string | null | Credit line to display (stock images) |
data.images[].tags |
string[] | Tags associated with the image |
data.images[].createdAt |
string | ISO 8601 creation timestamp |
data.images[].updatedAt |
string | null | ISO 8601 timestamp of the last update |
data.authors |
Author[] | Assigned authors (only with include=authors): { id, name, status, avatarUrl, bio, topicKey } |
data.categories |
Category[] | Assigned categories (only with include=categories): { id, name, slug, description, parentId } |
data.qualityChecks |
QualityCheck[] | Quality check history, newest first (only with include=qualityChecks). Same objects as GET /stories/{id}/content/quality-checks. |
data.seoAudits |
SeoAudit[] | SEO audit history, newest first (only with include=seoAudits). Same objects as GET /stories/{id}/content/seo-audits. |
data.translations |
Translation[] | Every language version of this story including itself (only with include=translations): { code, storyId, default, slug, title }. default marks the original. |
data.contentSources |
ContentSourceRef[] | Sources the story was written from (only with include=contentSources): { id, title, link, sourceType, sourceMeta }. Add fullContentSources=true on the detail endpoints for the complete records. |
data.relatedStories |
RelatedStory[] | Up to 5 published stories of the same Topic Workspace and language that are closest to this one in meaning, closest first: ready for a Read next block. Always returned, no include needed. Found when the story is published (within about a minute) and again when a published story gets a new title or summary; empty until then and when no story is close enough. Only stories published right now are listed, with their current title and slug, and never another language version of this story. |
data.relatedStories[].id |
string | Id of the related story |
data.relatedStories[].title |
string | Its current title |
data.relatedStories[].slug |
string | Its slug. Point it at your own story route, like the links inside content |
data.relatedStories[].summary |
string | Its short summary |
data.relatedStories[].language |
string | Full locale code, always the language of this story |
data.relatedStories[].publishedAt |
string | null | ISO 8601 timestamp of its first publication |
data.relatedStories[].mainImageUrl |
string | null | URL of its main image, when it has one |
data.relatedStories[].thumbnail |
string | null | URL of its thumbnail, often the source publisher's photo |
data.relatedStories[].score |
number | Similarity to this story from 0 to 1, higher is closer. Listed stories score 0.7 or more |
{
"data": {
"id": "68b9b7c2f1a2b3c4d5e6f700",
"title": "AI Revolution in Healthcare",
"summary": "How artificial intelligence is transforming medical diagnostics.",
"slug": "ai-revolution-in-healthcare",
"status": "published",
"language": "en-US",
"tags": [
"ai",
"healthcare",
"technology"
],
"topicKey": "tech",
"parentId": null,
"thumbnail": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/thumb_400x225.jpg",
"thumbnailImageId": "68b9b7c2f1a2b3c4d5e6f703",
"mainImageId": "68b9b7c2f1a2b3c4d5e6f702",
"mainImageUrl": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/generated_openai_1788425993651_0.png",
"publishedAt": "2026-08-15T11:45:00.000Z",
"firstReported": "2026-08-15T08:00:00.000Z",
"createdAt": "2026-08-15T10:30:00.000Z",
"updatedAt": "2026-08-15T12:00:00.000Z",
"content": "<h3>Overview</h3><p>A comprehensive look at how AI is being deployed across diagnostics, triage and treatment planning.</p>",
"bulletPoints": [
"AI diagnostics accuracy reaches 95%",
"Cost reduction of 40% in pilot hospitals"
],
"sentiment": 0.8,
"metadata": {
"title": "AI Revolution in Healthcare: What Changes in 2026",
"description": "AI is transforming diagnostics, triage and treatment planning. Here is what hospitals report.",
"keywords": "ai, healthcare, diagnostics"
},
"relatedStories": [
{
"id": "68b9b7c2f1a2b3c4d5e6f740",
"title": "Hospitals Cut Waiting Times with AI Triage",
"slug": "hospitals-cut-waiting-times-with-ai-triage",
"summary": "Emergency departments using AI triage report shorter waits and fewer missed cases.",
"language": "en-US",
"publishedAt": "2026-08-12T09:10:00.000Z",
"mainImageUrl": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f740/generated_openai_1788125993651_0.png",
"thumbnail": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f740/thumb_400x225.jpg",
"score": 0.8412
},
{
"id": "68b9b7c2f1a2b3c4d5e6f741",
"title": "Regulators Draft Rules for Diagnostic AI",
"slug": "regulators-draft-rules-for-diagnostic-ai",
"summary": "A draft framework would require clinical trials before diagnostic models reach patients.",
"language": "en-US",
"publishedAt": "2026-08-09T15:30:00.000Z",
"mainImageUrl": null,
"thumbnail": null,
"score": 0.7761
}
],
"images": [
{
"id": "68b9b7c2f1a2b3c4d5e6f701",
"url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/gettyimages-1445867611.jpg",
"s3Url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/gettyimages-1445867611.jpg",
"originalUrl": "https://techcrunch.com/wp-content/uploads/gettyimages-1445867611.jpg",
"width": 1024,
"height": 683,
"format": "JPEG",
"contentType": "image/jpeg",
"fileSize": 214532,
"imageType": "main",
"origin": "source",
"aiGenerated": false,
"generation": null,
"sourceType": "story",
"parentId": null,
"title": "AI Revolution in Healthcare",
"description": null,
"altText": null,
"caption": null,
"author": null,
"source": "https://techcrunch.com/2026/07/30/ai-healthcare/",
"license": null,
"creditLine": null,
"tags": [],
"createdAt": "2026-08-15T10:30:00.000Z",
"updatedAt": "2026-08-15T10:30:00.000Z"
},
{
"id": "68b9b7c2f1a2b3c4d5e6f702",
"url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/generated_openai_1788425993651_0.png",
"s3Url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/generated_openai_1788425993651_0.png",
"originalUrl": null,
"width": 1024,
"height": 576,
"format": "PNG",
"contentType": "image/png",
"fileSize": 1583022,
"imageType": "main",
"origin": "generated",
"aiGenerated": true,
"generation": {
"provider": "openai",
"model": "gpt-image-1",
"prompt": "Editorial illustration of AI-assisted medical diagnostics in a modern hospital"
},
"sourceType": "generated",
"parentId": null,
"title": "AI Generated Image - openai",
"description": null,
"altText": null,
"caption": null,
"author": "openai AI",
"source": "openai",
"license": null,
"creditLine": null,
"tags": [
"ai-generated",
"openai",
"healthcare"
],
"createdAt": "2026-08-15T11:02:00.000Z",
"updatedAt": "2026-08-15T11:02:00.000Z"
}
],
"authors": [
{
"id": "68b9b7c2f1a2b3c4d5e6f720",
"name": "Jane Doe",
"status": "active",
"avatarUrl": "https://cdn.news-factory.app/authors/jane.jpg",
"topicKey": "tech"
}
],
"categories": [
{
"id": "68b9b7c2f1a2b3c4d5e6f730",
"name": "Health Tech",
"slug": "health-tech",
"parentId": null
}
],
"translations": [
{
"code": "en-US",
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"default": true,
"slug": "ai-revolution-in-healthcare",
"title": "AI Revolution in Healthcare"
},
{
"code": "de-DE",
"storyId": "68b9b7c2f1a2b3c4d5e6f710",
"default": false,
"slug": "ki-revolution-im-gesundheitswesen",
"title": "KI-Revolution im Gesundheitswesen"
},
{
"code": "pt-BR",
"storyId": "68b9b7c2f1a2b3c4d5e6f711",
"default": false,
"slug": "revolucao-da-ia-na-saude",
"title": "Revolução da IA na saúde"
}
]
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found
Example
curl -X GET "https://client-api.news-factory.app/v1/stories/slug/ai-revolution-in-healthcare?include=images%2Ctranslations" \
-H "Authorization: Bearer nfk_xxx..."
List Story Media
GET /v1/stories/{storyId}/media · operationId listStoryMedia · scopes: stories:read
All images attached to a story as public Image objects. Each carries origin, aiGenerated and generation, so you can tell the images News Factory generated (safe to publish as your own) from the publisher's photo (origin: "source").
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Responses
200The story's images
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data |
Image[] | Public Image objects. Filter on aiGenerated or origin to pick what to publish. |
data[].id |
string | Unique image identifier |
data[].url |
string | Public URL of the image file |
data[].s3Url |
string | Deprecated alias of url with the same value; use url |
data[].width |
number | Width in pixels |
data[].height |
number | Height in pixels |
data[].format |
string | File format (PNG, JPEG, WEBP) |
data[].contentType |
string | MIME type of the image |
data[].fileSize |
number | null | File size in bytes |
data[].imageType |
string | Role on the story: main, thumbnail, gallery, content, other |
data[].origin |
string | Normalized provenance: generated (made by News Factory with an AI image model), stock (licensed stock provider), source (taken from the original article, usually not yours to republish), upload (uploaded or imported by URL) |
data[].aiGenerated |
boolean | Authoritative flag: true only when News Factory produced the image with an AI model, including crops and thumbnails derived from one. Use this (not sourceType) to pick images you can publish as your own. |
data[].generation |
object | null | Present when aiGenerated is true: { provider, model, prompt } (each may be null for older images) |
data[].sourceType |
string | Raw storage source (generated, stock, story, content-source, scraped, user-upload, url). Kept for compatibility; not a reliable AI indicator |
data[].parentId |
string | null | Id of the image this one was cropped or resized from |
data[].originalUrl |
string | null | Where the file was fetched from (article images, URL imports) |
data[].title |
string | null | Image title |
data[].description |
string | null | Image description |
data[].altText |
string | null | Alt text |
data[].caption |
string | null | Caption |
data[].author |
string | null | Author or photographer |
data[].source |
string | null | Attribution source: article URL, stock provider id, or AI provider id |
data[].license |
string | null | License (stock images) |
data[].creditLine |
string | null | Credit line to display (stock images) |
data[].tags |
string[] | Tags associated with the image |
data[].createdAt |
string | ISO 8601 creation timestamp |
data[].updatedAt |
string | null | ISO 8601 timestamp of the last update |
meta.count |
number | Number of images |
{
"success": true,
"data": [
{
"id": "68b9b7c2f1a2b3c4d5e6f701",
"url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/gettyimages-1445867611.jpg",
"s3Url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/gettyimages-1445867611.jpg",
"originalUrl": "https://techcrunch.com/wp-content/uploads/gettyimages-1445867611.jpg",
"width": 1024,
"height": 683,
"format": "JPEG",
"contentType": "image/jpeg",
"fileSize": 214532,
"imageType": "main",
"origin": "source",
"aiGenerated": false,
"generation": null,
"sourceType": "story",
"parentId": null,
"title": "AI Revolution in Healthcare",
"description": null,
"altText": null,
"caption": null,
"author": null,
"source": "https://techcrunch.com/2026/07/30/ai-healthcare/",
"license": null,
"creditLine": null,
"tags": [],
"createdAt": "2026-08-15T10:30:00.000Z",
"updatedAt": "2026-08-15T10:30:00.000Z"
},
{
"id": "68b9b7c2f1a2b3c4d5e6f702",
"url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/generated_openai_1788425993651_0.png",
"s3Url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/generated_openai_1788425993651_0.png",
"originalUrl": null,
"width": 1024,
"height": 576,
"format": "PNG",
"contentType": "image/png",
"fileSize": 1583022,
"imageType": "main",
"origin": "generated",
"aiGenerated": true,
"generation": {
"provider": "openai",
"model": "gpt-image-1",
"prompt": "Editorial illustration of AI-assisted medical diagnostics in a modern hospital"
},
"sourceType": "generated",
"parentId": null,
"title": "AI Generated Image - openai",
"description": null,
"altText": null,
"caption": null,
"author": "openai AI",
"source": "openai",
"license": null,
"creditLine": null,
"tags": [
"ai-generated",
"openai",
"healthcare"
],
"createdAt": "2026-08-15T11:02:00.000Z",
"updatedAt": "2026-08-15T11:02:00.000Z"
}
],
"meta": {
"count": 2
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found
Example
curl -X GET "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/media" \
-H "Authorization: Bearer nfk_xxx..."
Upload Media
POST /v1/stories/{storyId}/media · operationId uploadStoryMedia · scopes: stories:write
Store an image and attach it to the story. Send either a base64 fileData with fileName and mimeType, or a url to import from.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Request body (JSON): Either fileData (with fileName and mimeType) or url
| Name | Type | Required | Description |
|---|---|---|---|
title |
string | no | Image title |
description |
string | no | Image description |
tags |
string[] | no | Tags |
imageType |
string | no | Role on the story (default content) |
fileData |
file | no | Base64 encoded image (PNG, JPEG, WEBP, GIF). 10 MB request limit. |
fileName |
string | no | Original file name (required with fileData) |
mimeType |
string | no | MIME type (required with fileData) |
url |
string | no | Public image URL to import instead of fileData |
{
"title": "Hero image",
"tags": [
"photo",
"hero"
],
"imageType": "content",
"url": "https://example.com/image.jpg"
}
Responses
201Image stored and attached
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data |
Image | The public Image object (origin: "upload", aiGenerated: false) |
data.id |
string | Unique image identifier |
data.url |
string | Public URL of the image file |
data.s3Url |
string | Deprecated alias of url with the same value; use url |
data.width |
number | Width in pixels |
data.height |
number | Height in pixels |
data.format |
string | File format (PNG, JPEG, WEBP) |
data.contentType |
string | MIME type of the image |
data.fileSize |
number | null | File size in bytes |
data.imageType |
string | Role on the story: main, thumbnail, gallery, content, other |
data.origin |
string | Normalized provenance: generated (made by News Factory with an AI image model), stock (licensed stock provider), source (taken from the original article, usually not yours to republish), upload (uploaded or imported by URL) |
data.aiGenerated |
boolean | Authoritative flag: true only when News Factory produced the image with an AI model, including crops and thumbnails derived from one. Use this (not sourceType) to pick images you can publish as your own. |
data.generation |
object | null | Present when aiGenerated is true: { provider, model, prompt } (each may be null for older images) |
data.sourceType |
string | Raw storage source (generated, stock, story, content-source, scraped, user-upload, url). Kept for compatibility; not a reliable AI indicator |
data.parentId |
string | null | Id of the image this one was cropped or resized from |
data.originalUrl |
string | null | Where the file was fetched from (article images, URL imports) |
data.title |
string | null | Image title |
data.description |
string | null | Image description |
data.altText |
string | null | Alt text |
data.caption |
string | null | Caption |
data.author |
string | null | Author or photographer |
data.source |
string | null | Attribution source: article URL, stock provider id, or AI provider id |
data.license |
string | null | License (stock images) |
data.creditLine |
string | null | Credit line to display (stock images) |
data.tags |
string[] | Tags associated with the image |
data.createdAt |
string | ISO 8601 creation timestamp |
data.updatedAt |
string | null | ISO 8601 timestamp of the last update |
message |
string | Success message |
{
"success": true,
"data": {
"id": "68b9b7c2f1a2b3c4d5e6f705",
"url": "https://cdn.news-factory.app/organizations/acme/hero.jpg",
"s3Url": "https://cdn.news-factory.app/organizations/acme/hero.jpg",
"originalUrl": null,
"width": 1024,
"height": 683,
"format": "JPEG",
"contentType": "image/jpeg",
"fileSize": 214532,
"imageType": "content",
"origin": "upload",
"aiGenerated": false,
"generation": null,
"sourceType": "user-upload",
"parentId": null,
"title": "Hero image",
"description": null,
"altText": null,
"caption": null,
"author": null,
"source": null,
"license": null,
"creditLine": null,
"tags": [
"photo",
"hero"
],
"createdAt": "2026-08-15T10:30:00.000Z",
"updatedAt": "2026-08-15T10:30:00.000Z"
},
"message": "Image imported from URL and attached to story"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X POST "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/media" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"title":"Hero image","tags":["photo","hero"],"imageType":"content","url":"https://example.com/image.jpg"}'
Import Image from URL
POST /v1/stories/{storyId}/media/from-url · operationId importImageUrl · scopes: stories:write
Download an image from a public URL, store it and attach it to the story.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Request body (JSON): The URL and optional metadata
| Name | Type | Required | Description |
|---|---|---|---|
url |
string | yes | Public image URL |
title |
string | no | Image title |
description |
string | no | Image description |
tags |
string[] | no | Tags |
imageType |
string | no | Role on the story (default content) |
{
"url": "https://example.com/image.jpg",
"title": "Hero image",
"imageType": "main"
}
Responses
201Image stored and attached
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data |
Image | The public Image object (origin: "upload", aiGenerated: false) |
data.id |
string | Unique image identifier |
data.url |
string | Public URL of the image file |
data.s3Url |
string | Deprecated alias of url with the same value; use url |
data.width |
number | Width in pixels |
data.height |
number | Height in pixels |
data.format |
string | File format (PNG, JPEG, WEBP) |
data.contentType |
string | MIME type of the image |
data.fileSize |
number | null | File size in bytes |
data.imageType |
string | Role on the story: main, thumbnail, gallery, content, other |
data.origin |
string | Normalized provenance: generated (made by News Factory with an AI image model), stock (licensed stock provider), source (taken from the original article, usually not yours to republish), upload (uploaded or imported by URL) |
data.aiGenerated |
boolean | Authoritative flag: true only when News Factory produced the image with an AI model, including crops and thumbnails derived from one. Use this (not sourceType) to pick images you can publish as your own. |
data.generation |
object | null | Present when aiGenerated is true: { provider, model, prompt } (each may be null for older images) |
data.sourceType |
string | Raw storage source (generated, stock, story, content-source, scraped, user-upload, url). Kept for compatibility; not a reliable AI indicator |
data.parentId |
string | null | Id of the image this one was cropped or resized from |
data.originalUrl |
string | null | Where the file was fetched from (article images, URL imports) |
data.title |
string | null | Image title |
data.description |
string | null | Image description |
data.altText |
string | null | Alt text |
data.caption |
string | null | Caption |
data.author |
string | null | Author or photographer |
data.source |
string | null | Attribution source: article URL, stock provider id, or AI provider id |
data.license |
string | null | License (stock images) |
data.creditLine |
string | null | Credit line to display (stock images) |
data.tags |
string[] | Tags associated with the image |
data.createdAt |
string | ISO 8601 creation timestamp |
data.updatedAt |
string | null | ISO 8601 timestamp of the last update |
message |
string | Success message |
{
"success": true,
"data": {
"id": "68b9b7c2f1a2b3c4d5e6f705",
"url": "https://cdn.news-factory.app/organizations/acme/hero.jpg",
"s3Url": "https://cdn.news-factory.app/organizations/acme/hero.jpg",
"originalUrl": null,
"width": 1024,
"height": 683,
"format": "JPEG",
"contentType": "image/jpeg",
"fileSize": 214532,
"imageType": "content",
"origin": "upload",
"aiGenerated": false,
"generation": null,
"sourceType": "user-upload",
"parentId": null,
"title": "Hero image",
"description": null,
"altText": null,
"caption": null,
"author": null,
"source": null,
"license": null,
"creditLine": null,
"tags": [
"photo",
"hero"
],
"createdAt": "2026-08-15T10:30:00.000Z",
"updatedAt": "2026-08-15T10:30:00.000Z"
},
"message": "Image imported from URL and attached to story"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X POST "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/media/from-url" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/image.jpg","title":"Hero image","imageType":"main"}'
Generate Image (AI)
POST /v1/stories/{storyId}/media/generate · operationId generateImage · scopes: ai:generate + stories:write
Generate an image with an AI model and attach it to the story. Providers: OpenAI (GPT Image), Google Gemini and Ideogram. Story context (title, summary) is added to the prompt unless includeStoryContext is false. The saved image is flagged aiGenerated: true with origin: "generated"; read it back with GET /stories/{storyId}/media. Costs AI credits.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Request body (JSON): Generation parameters
| Name | Type | Required | Description |
|---|---|---|---|
prompt |
string | yes | What to generate |
provider |
string | yes | AI provider |
aspectRatio |
string | no | Aspect ratio |
quality |
string | no | Quality level (default high) |
style |
string | no | Visual style (default photographic) |
negativePrompt |
string | no | What to avoid |
numImages |
string | no | Number of images (default 1) |
imageType |
string | no | Role of the generated image (default main) |
includeStoryContext |
string | no | Add the story title and summary to the prompt (default true) |
{
"prompt": "A photorealistic editorial photograph of a hospital diagnostics lab",
"provider": "openai",
"aspectRatio": "16:9",
"quality": "high"
}
Responses
200Image generated and attached
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data.storyId |
string | The story |
data.provider |
string | Provider used |
data.model |
string | null | Model used |
data.processingTimeMs |
number | null | Generation time |
data.images |
object[] | Generated images: { imageId, url, width, height, format, prompt }. Fetch the full public Image objects with GET /stories/{storyId}/media. |
message |
string | Success message |
{
"success": true,
"data": {
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"provider": "openai",
"model": "gpt-image-1",
"processingTimeMs": 4500,
"images": [
{
"imageId": "68b9b7c2f1a2b3c4d5e6f702",
"url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/generated_openai_1788425993651_0.png",
"width": 1792,
"height": 1024,
"format": "png",
"prompt": "A photorealistic editorial photograph of a hospital diagnostics lab. Story: AI Revolution in Healthcare"
}
]
},
"message": "Image generated successfully"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found429The organization's AI credits are exhausted503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X POST "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/media/generate" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"prompt":"A photorealistic editorial photograph of a hospital diagnostics lab","provider":"openai","aspectRatio":"16:9","quality":"high"}'
Set Thumbnail
POST /v1/stories/{storyId}/media/thumbnail · operationId setThumbnail · scopes: stories:write
Generate the story thumbnail from an attached image (imageId), from any image URL (imageUrl), or from the story's first attached image when both are omitted. The previous thumbnail image is deleted when no other story uses it.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Request body (JSON): All fields optional
| Name | Type | Required | Description |
|---|---|---|---|
imageId |
string | no | Attached image to crop from |
imageUrl |
string | no | Image URL to crop from |
preset |
string | no | Size preset (default small, 400 by 225) |
width |
number | no | Custom width in pixels (overrides the preset) |
height |
number | no | Custom height in pixels |
quality |
number | no | JPEG quality 1 to 100 (default 80) |
{
"imageId": "68b9b7c2f1a2b3c4d5e6f702",
"preset": "standard",
"quality": 85
}
Responses
200Thumbnail set
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data.storyId |
string | The story |
data.thumbnailUrl |
string | Public URL of the new thumbnail (also written to the story's thumbnail) |
data.thumbnailImageId |
string | Id of the new thumbnail Image (also written to thumbnailImageId) |
data.width |
number | Width in pixels |
data.height |
number | Height in pixels |
data.format |
string | File format |
data.fileSize |
number | Bytes |
data.preset |
string | Preset used |
data.sourceImage |
string | The image id or URL the thumbnail was cropped from |
data.previousThumbnail |
object | null | { imageId, deleted } when a previous thumbnail existed |
message |
string | Success message |
{
"success": true,
"data": {
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"thumbnailUrl": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/thumb_800x450.jpg",
"thumbnailImageId": "68b9b7c2f1a2b3c4d5e6f703",
"width": 800,
"height": 450,
"format": "jpeg",
"fileSize": 61234,
"preset": "standard",
"sourceImage": "68b9b7c2f1a2b3c4d5e6f702",
"previousThumbnail": {
"imageId": "68b9b7c2f1a2b3c4d5e6f6ff",
"deleted": true
}
},
"message": "Thumbnail set successfully"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X POST "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/media/thumbnail" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"imageId":"68b9b7c2f1a2b3c4d5e6f702","preset":"standard","quality":85}'
Remove Thumbnail
DELETE /v1/stories/{storyId}/media/thumbnail · operationId removeThumbnail · scopes: stories:write
Clear the story's thumbnail and thumbnailImageId. The image record itself is kept.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Responses
200Thumbnail cleared
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
message |
string | Success message |
{
"success": true,
"message": "Thumbnail removed"
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found
Example
curl -X DELETE "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/media/thumbnail" \
-H "Authorization: Bearer nfk_xxx..."
Attach Media
POST /v1/stories/{storyId}/media/{imageId} · operationId attachStoryMedia · scopes: stories:write
Attach an image that already exists in the organization (for example one uploaded to another story) to this story.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
imageId |
string | yes | The image id |
Responses
200Image attached
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data.storyId |
string | The story |
data.imageId |
string | The attached image |
data.imageIds |
string[] | All image ids now attached to the story |
message |
string | Success message |
{
"success": true,
"data": {
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"imageId": "68b9b7c2f1a2b3c4d5e6f702",
"imageIds": [
"68b9b7c2f1a2b3c4d5e6f701",
"68b9b7c2f1a2b3c4d5e6f702"
]
},
"message": "Image attached to story"
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story or image not found
Example
curl -X POST "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/media/68b9b7c2f1a2b3c4d5e6f702" \
-H "Authorization: Bearer nfk_xxx..."
Detach Media
DELETE /v1/stories/{storyId}/media/{imageId} · operationId removeStoryMedia · scopes: stories:write
Detach an image from the story. The image record is kept and stays attached to any other story that uses it.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
imageId |
string | yes | The image id |
Responses
200Image detached
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
message |
string | Success message |
{
"success": true,
"message": "Image removed from story"
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story or image not found
Example
curl -X DELETE "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/media/68b9b7c2f1a2b3c4d5e6f702" \
-H "Authorization: Bearer nfk_xxx..."
Suggest Titles
POST /v1/stories/{storyId}/content/suggest-titles · operationId suggestTitles · scopes: ai:generate + stories:read
AI title suggestions grounded in the story's content sources. The story needs at least one content source. Costs AI credits.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Request body (JSON): Options
| Name | Type | Required | Description |
|---|---|---|---|
count |
number | no | How many suggestions (1 to 10, default 3) |
language |
string | no | Language of the titles as a full locale code (default: the story language) |
titles |
string[] | no | Titles to avoid repeating |
{
"count": 3,
"language": "en-US"
}
Responses
200Suggestions
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
storyId |
string | The story |
storyTitle |
string | Current title |
count |
number | Suggestions returned |
titleSuggestions |
string[] | Suggested titles |
language |
string | Language used |
model |
string | LLM model used |
{
"success": true,
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"storyTitle": "AI Revolution in Healthcare",
"count": 3,
"titleSuggestions": [
"AI Revolution Reshapes Modern Healthcare",
"How Artificial Intelligence Is Transforming Medicine",
"The Future of Healthcare: AI-Driven Diagnostics"
],
"language": "en-US",
"model": "claude-sonnet-5"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found429The organization's AI credits are exhausted
Example
curl -X POST "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/content/suggest-titles" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"count":3,"language":"en-US"}'
Suggest Summary
POST /v1/stories/{storyId}/content/suggest-summary · operationId suggestSummary · scopes: ai:generate + stories:read
AI summary suggestions grounded in the story's content sources. Costs AI credits.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Request body (JSON): Options
| Name | Type | Required | Description |
|---|---|---|---|
count |
number | no | How many suggestions (1 to 5, default 3) |
language |
string | no | Language of the summaries as a full locale code (default: the story language) |
{
"count": 3,
"language": "en-US"
}
Responses
200Suggestions
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
storyId |
string | The story |
storySummary |
string | Current summary |
count |
number | Suggestions returned |
summarySuggestions |
string[] | Suggested summaries |
language |
string | Language used |
model |
string | LLM model used |
{
"success": true,
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"storySummary": "How artificial intelligence is transforming medical diagnostics.",
"count": 2,
"summarySuggestions": [
"Hospitals across Europe report faster triage and lower costs after adopting AI diagnostics.",
"AI diagnostic tools now reach 95% accuracy in pilot programmes, cutting costs by 40%."
],
"language": "en-US",
"model": "claude-sonnet-5"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found429The organization's AI credits are exhausted
Example
curl -X POST "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/content/suggest-summary" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"count":3,"language":"en-US"}'
Suggest SEO Title
POST /v1/stories/{storyId}/content/suggest-seo-title · operationId suggestSeoTitle · scopes: ai:generate + stories:read
Search engine optimised title suggestions (under 60 characters, keyword first). Write the pick to metadata.title with PATCH /stories/{id}. Costs AI credits.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Request body (JSON): Options
| Name | Type | Required | Description |
|---|---|---|---|
count |
number | no | How many suggestions (1 to 5, default 3) |
language |
string | no | Language of the titles as a full locale code (default: the story language) |
{
"count": 3
}
Responses
200Suggestions
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
count |
number | Suggestions returned |
seoTitleSuggestions |
string[] | Suggested SEO titles |
language |
string | Language used |
{
"success": true,
"count": 3,
"seoTitleSuggestions": [
"AI in Healthcare 2026: Diagnostics, Costs and Results",
"AI Diagnostics Hit 95% Accuracy in Hospital Pilots",
"How AI Is Cutting Hospital Costs by 40%"
],
"language": "en-US"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found429The organization's AI credits are exhausted
Example
curl -X POST "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/content/suggest-seo-title" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"count":3}'
Suggest Meta Description
POST /v1/stories/{storyId}/content/suggest-meta-description · operationId suggestMetaDescription · scopes: ai:generate + stories:read
Meta description suggestions (under 160 characters). Write the pick to metadata.description with PATCH /stories/{id}. Costs AI credits.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Request body (JSON): Options
| Name | Type | Required | Description |
|---|---|---|---|
count |
number | no | How many suggestions (1 to 5, default 3) |
language |
string | no | Language of the descriptions as a full locale code (default: the story language) |
{
"count": 3
}
Responses
200Suggestions
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
count |
number | Suggestions returned |
metaDescriptionSuggestions |
string[] | Suggested meta descriptions |
language |
string | Language used |
{
"success": true,
"count": 2,
"metaDescriptionSuggestions": [
"AI diagnostics reach 95% accuracy and cut hospital costs by 40%. See what the European pilots found.",
"How hospitals use AI for triage and diagnostics, and what it means for patients in 2026."
],
"language": "en-US"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found429The organization's AI credits are exhausted
Example
curl -X POST "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/content/suggest-meta-description" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"count":3}'
Generate Social Post
POST /v1/stories/{storyId}/content/social-post/{platform} · operationId generateSocialPost · scopes: ai:generate + stories:read
Platform specific social media posts for the story. Generated posts are also saved on the story for the app's social tab. Costs AI credits.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
platform |
string (facebook, linkedin, x, instagram) |
yes | Target platform |
Request body (JSON): Options
| Name | Type | Required | Description |
|---|---|---|---|
count |
number | no | How many suggestions (1 to 5, default 3) |
targetUrl |
string | no | Link to include in the posts |
additionalInstructions |
string | no | Tone or content instructions |
{
"count": 2,
"targetUrl": "https://news.example.com/ai-revolution-in-healthcare"
}
Responses
200Posts
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
storyId |
string | The story |
storyTitle |
string | Current title |
platform |
string | Platform |
count |
number | Posts returned |
targetURL |
string | null | Link included |
socialMediaPosts |
string[] | Generated posts |
model |
string | LLM model used |
additionalInstructions |
string | null | Instructions applied |
{
"success": true,
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"storyTitle": "AI Revolution in Healthcare",
"platform": "linkedin",
"count": 2,
"targetURL": "https://news.example.com/ai-revolution-in-healthcare",
"socialMediaPosts": [
"AI is no longer the future of healthcare. It is the present. From diagnostics to treatment planning, here is what European hospitals report. https://news.example.com/ai-revolution-in-healthcare",
"95% diagnostic accuracy. 40% lower costs. The numbers behind AI in hospitals: https://news.example.com/ai-revolution-in-healthcare"
],
"model": "claude-sonnet-5",
"additionalInstructions": null
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found429The organization's AI credits are exhausted
Example
curl -X POST "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/content/social-post/linkedin" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"count":2,"targetUrl":"https://news.example.com/ai-revolution-in-healthcare"}'
Translate Story
POST /v1/stories/{storyId}/content/translate · operationId translateStory · scopes: ai:generate + stories:write
Create a translated copy of the story as a new story linked through parentId. The target must be one of the organization's supported languages, given as a full locale code. Translating a language that already exists answers 409. Costs AI credits.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Request body (JSON): Target language and options
| Name | Type | Required | Description |
|---|---|---|---|
languageCode |
string | yes | Target language as a full locale code (de-DE, es-ES, pt-BR, ...) |
effort |
string | no | Model effort (default normal) |
{
"languageCode": "de-DE"
}
Responses
200Translation created
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
storyId |
string | Id of the new translated story |
translatedStory |
object | { id, title, language, slug } of the new story. Load it with GET /stories/{id}. |
{
"success": true,
"storyId": "68b9b7c2f1a2b3c4d5e6f710",
"translatedStory": {
"id": "68b9b7c2f1a2b3c4d5e6f710",
"title": "KI-Revolution im Gesundheitswesen",
"language": "de-DE",
"slug": "ki-revolution-im-gesundheitswesen"
}
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403Forbidden: the credential cannot act on this organization or Topic Workspace Or: The API key lacks a scope this endpoint needs404Story not found409Conflict429The organization's AI credits are exhausted
Example
curl -X POST "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/content/translate" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"languageCode":"de-DE"}'
Run Quality Check
POST /v1/stories/{storyId}/content/quality-check · operationId qualityCheck · scopes: ai:generate + stories:read
Run the editorial quality check: fact check against the content sources, writing quality, and topic match. The result is stored in the story's history (GET /stories/{storyId}/content/quality-checks) and each finding can be applied with the apply-fix endpoint. Stories under minWords words fail without calling the model. Costs AI credits.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Request body (JSON): Options
| Name | Type | Required | Description |
|---|---|---|---|
effort |
string | no | Model effort (default normal) |
language |
string | no | Language of the report (verdict and explanations) as a full locale code (default: the story language) |
minWords |
number | no | Minimum story length (default 100) |
factCheck |
object | no | { enabled, customInstructions } for the fact check (default enabled) |
writingQuality |
object | no | { enabled, customInstructions } for the writing quality check (default enabled) |
topicMatch |
object | no | { enabled, customInstructions } for the topic match check (default enabled) |
globalInstructions |
string | no | Instructions applied to every check |
{
"effort": "normal",
"language": "en-US"
}
Responses
200Quality report
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
storyId |
string | The story |
overall |
string | pass, warn or fail |
summary |
string | Short verdict in the report language |
minimumContentOk |
boolean | Whether the story met minWords |
wordCounts.story |
number | Story word count |
wordCounts.minRequired |
number | Minimum applied |
wordCounts.sources |
object[] | { id, wordCount } per content source |
qualityFindings.summary |
string | Report summary |
qualityFindings.factCheck.findings |
object[] | { field, severity, claim, sourceEvidence, action, suggestedFix } |
qualityFindings.writingQuality.score |
number | 0 to 100 |
qualityFindings.writingQuality.findings |
object[] | { field, severity, category, excerpt, rewrite } |
qualityFindings.topicMatch |
object | { expectedTopicKey, matches, modelTopicKey, confidence, recommendedAction, details } |
findings |
object | Legacy flat shape: discrepancies[], writingQuality, topicMatch |
{
"success": true,
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"wordCounts": {
"story": 640,
"minRequired": 100,
"sources": [
{
"id": "68b9b7c2f1a2b3c4d5e6f760",
"wordCount": 1250
}
]
},
"minimumContentOk": true,
"qualityFindings": {
"summary": "Two claims need a source and one paragraph repeats the lead.",
"factCheck": {
"findings": [
{
"field": "content",
"severity": "high",
"claim": "AI diagnostics accuracy reaches 99%",
"sourceEvidence": "The source reports 95% accuracy in the pilot.",
"action": "rewrite",
"suggestedFix": "AI diagnostics accuracy reaches 95%"
}
]
},
"writingQuality": {
"score": 78,
"findings": [
{
"field": "content",
"severity": "low",
"category": "repetition",
"excerpt": "AI is transforming healthcare.",
"rewrite": ""
}
]
},
"topicMatch": {
"expectedTopicKey": "tech",
"matches": true,
"modelTopicKey": "tech",
"confidence": 92,
"recommendedAction": "keep",
"details": "The story is about AI in healthcare, which matches the technology topic."
}
},
"findings": {
"discrepancies": [
{
"description": "AI diagnostics accuracy reaches 99% (source: 95%)",
"severity": "high"
}
],
"writingQuality": {
"score": 78,
"issues": [
"Repeated lead paragraph"
],
"flags": []
},
"topicMatch": {
"confidence": 92,
"details": "Matches the technology topic."
}
},
"overall": "warn",
"summary": "Two claims need a source and one paragraph repeats the lead."
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found429The organization's AI credits are exhausted
Example
curl -X POST "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/content/quality-check" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"effort":"normal","language":"en-US"}'
Quality Check History
GET /v1/stories/{storyId}/content/quality-checks · operationId listQualityChecks · scopes: stories:read
Every quality check that ran on the story, newest first, including checks run by workflows. The same records are available inline with include=qualityChecks on the story endpoints.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit |
number | no | Page size (default 20, max 100) |
page |
number | no | Zero-based page number (default 0) |
Responses
200History
| Field | Type | Description |
|---|---|---|
data |
QualityCheck[] | Quality check records |
data[].id |
string | Record id |
data[].storyId |
string | Story the check ran on |
data[].overall |
string | pass, warn or fail |
data[].summary |
string | Short verdict written in the report language |
data[].qualityFindings |
object | Modular findings: factCheck.findings[], writingQuality.score and findings[], topicMatch. Each finding can be applied with POST /stories/{id}/content/quality-check/apply-fix |
data[].discrepancies |
object[] | Legacy flat list of { description, severity } (kept for older integrations) |
data[].writingQuality |
object | Legacy { score, issues[], flags[] } |
data[].topicMatch |
object | Legacy { confidence, details } |
data[].effort |
string | normal or high |
data[].checksRun |
string[] | Which checks were enabled: factCheck, writingQuality, topicMatch |
data[].provider |
string | LLM provider used |
data[].model |
string | LLM model used |
data[].credits |
number | Credits charged |
data[].createdAt |
string | ISO 8601 timestamp |
meta |
object | Pagination metadata |
meta.page |
number | Zero-based page returned |
meta.limit |
number | Page size used |
meta.hasMore |
boolean | Whether a next page may exist (the page was full) |
meta.storyId |
string | The story |
{
"data": [
{
"id": "68b9b7c2f1a2b3c4d5e6f740",
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"overall": "warn",
"summary": "Two claims need a source and one paragraph repeats the lead.",
"qualityFindings": {
"summary": "Two claims need a source and one paragraph repeats the lead.",
"factCheck": {
"findings": [
{
"field": "content",
"severity": "high",
"claim": "AI diagnostics accuracy reaches 99%",
"sourceEvidence": "The source reports 95% accuracy in the pilot.",
"action": "rewrite",
"suggestedFix": "AI diagnostics accuracy reaches 95%"
}
]
},
"writingQuality": {
"score": 78,
"findings": [
{
"field": "content",
"severity": "low",
"category": "repetition",
"excerpt": "AI is transforming healthcare.",
"rewrite": ""
}
]
},
"topicMatch": {
"expectedTopicKey": "tech",
"matches": true,
"modelTopicKey": "tech",
"confidence": 92,
"recommendedAction": "keep",
"details": "The story is about AI in healthcare, which matches the technology topic."
}
},
"discrepancies": [
{
"description": "AI diagnostics accuracy reaches 99% (source: 95%)",
"severity": "high"
}
],
"writingQuality": {
"score": 78,
"issues": [
"Repeated lead paragraph"
],
"flags": []
},
"topicMatch": {
"confidence": 92,
"details": "Matches the technology topic."
},
"effort": "normal",
"checksRun": [
"factCheck",
"writingQuality",
"topicMatch"
],
"provider": "anthropic",
"model": "claude-sonnet-5",
"credits": 4,
"createdAt": "2026-08-16T09:00:00.000Z"
}
],
"meta": {
"page": 0,
"limit": 20,
"hasMore": false,
"storyId": "68b9b7c2f1a2b3c4d5e6f700"
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found
Example
curl -X GET "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/content/quality-checks" \
-H "Authorization: Bearer nfk_xxx..."
Apply Quality Fix
POST /v1/stories/{storyId}/content/quality-check/apply-fix · operationId applyQualityFix · scopes: ai:generate + stories:write
Apply one finding from a quality check to the story. direct mode replaces the flagged text without a model; rewrite mode lets the model rewrite the field around the finding (costs credits). Topic findings with recommendedAction: reassign move the story to the suggested Topic Workspace.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Request body (JSON): The finding and the mode
| Name | Type | Required | Description |
|---|---|---|---|
finding |
object | yes | A finding object from qualityFindings, with a type of factCheck, writingQuality or topicMatch |
mode |
string | no | direct (default) or rewrite |
effort |
string | no | Model effort for rewrite mode |
{
"finding": {
"type": "factCheck",
"field": "content",
"severity": "high",
"claim": "AI diagnostics accuracy reaches 99%",
"sourceEvidence": "The source reports 95% accuracy.",
"action": "rewrite",
"suggestedFix": "AI diagnostics accuracy reaches 95%"
},
"mode": "direct"
}
Responses
200Result
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
storyId |
string | The story |
applied |
boolean | False when the text was already fixed or the claim was not found |
method |
string | direct or rewrite: |
field |
string | Field changed: title, summary, content or bulletPoints[n] |
before |
string | Field value before |
after |
string | Field value after |
{
"success": true,
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"applied": true,
"method": "direct",
"field": "content",
"before": "<p>AI diagnostics accuracy reaches 99% in the pilot.</p>",
"after": "<p>AI diagnostics accuracy reaches 95% in the pilot.</p>"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found429The organization's AI credits are exhausted
Example
curl -X POST "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/content/quality-check/apply-fix" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"finding":{"type":"factCheck","field":"content","severity":"high","claim":"AI diagnostics accuracy reaches 99%","sourceEvidence":"The source reports 95% accuracy.","action":"rewrite","suggestedFix":"AI diagnostics accuracy reaches 95%"},"mode":"direct"}'
Run SEO Audit
POST /v1/stories/{storyId}/content/seo-audit · operationId seoAudit · scopes: ai:generate + stories:read
Audit the story for search and answer engines: on-page checks, keyword placement against the Topic Workspace's SEO keywords, internal link candidates and AEO signals (direct answer, question headings, FAQs, entities). A model drafts concrete fixes unless skipSuggestions is true (then the audit is free). The report is stored in the story's history and each fix can be applied with the apply-fix endpoint.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Request body (JSON): Options
| Name | Type | Required | Description |
|---|---|---|---|
effort |
string | no | Model effort (default normal) |
language |
string | no | Language of the report summary as a full locale code (default: the story language) |
skipSuggestions |
string | no | Skip the model pass and return the deterministic checks only (no credits) |
seoConfig |
object | no | { groups: { onPage, keywords, links, aeo: { enabled } }, globalInstructions, maxLinkCandidates }. All groups default to enabled. |
{
"effort": "normal",
"language": "en-US"
}
Responses
200Audit report
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
storyId |
string | The story |
overallScore |
number | 0 to 100 |
overall |
string | pass, warn or fail |
summary |
string | Short summary in the report language |
credits |
number | Credits charged |
report.overallScore |
number | 0 to 100 (also at the top level) |
report.overall |
string | pass, warn or fail (also at the top level) |
report.summary |
string | Short summary in the report language (also at the top level) |
report.storySnapshot |
object | Title, SEO title, meta description, slug, word count, headings, images, locale, topic |
report.checks |
object[] | { id, group, status, impact, effort, data?, fix? }. Apply a fix with the apply-fix endpoint. |
report.keywords |
object[] | { term, primary, bodyCount, present[], suggestion? } per topic keyword |
report.linkCandidates |
object[] | Stories worth linking: { storyId, title, slug, relevance, anchor, paragraphIndex } |
report.aeo |
object | Answer engine signals: direct answer, question headings, FAQs, entities, cited claims |
report.groupScores |
object | Per group 0 to 100 |
report.suggestionsAvailable |
boolean | Whether the model pass ran |
{
"success": true,
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"report": {
"storySnapshot": {
"title": "AI Revolution in Healthcare",
"seoTitle": "",
"metaDescription": "",
"slug": "ai-revolution-in-healthcare",
"wordCount": 640,
"locale": "en-US",
"topicKey": "tech"
},
"checks": [
{
"id": "seoTitlePresent",
"group": "onPage",
"status": "fail",
"impact": "high",
"effort": "instant",
"fix": {
"kind": "setMetadataField",
"target": "title",
"after": "AI Revolution in Healthcare: What Changes in 2026"
}
},
{
"id": "metaDescriptionLength",
"group": "onPage",
"status": "warn",
"impact": "medium",
"effort": "instant"
}
],
"keywords": [
{
"term": "ai diagnostics",
"primary": true,
"bodyCount": 3,
"present": [
"h1",
"body"
],
"suggestion": {
"placement": "seoTitle",
"before": "",
"after": "AI diagnostics"
}
}
],
"linkCandidates": [
{
"storyId": "68b9b7c2f1a2b3c4d5e6f790",
"title": "Hospitals adopt AI triage",
"slug": "hospitals-adopt-ai-triage",
"relevance": 81,
"inboundLinks": 0,
"anchor": "AI triage",
"paragraphIndex": 2
}
],
"aeo": {
"hasDirectAnswer": false,
"proposedDirectAnswer": "AI is now used for diagnostics, triage and treatment planning in most large hospitals.",
"questionHeadingCount": 0,
"proposedQuestionHeadings": [],
"faqs": [],
"entities": [],
"citedClaims": 2,
"totalClaims": 5
},
"groupScores": {
"onPage": 55,
"keywords": 60,
"links": 40,
"aeo": 30
},
"overallScore": 48,
"overall": "warn",
"summary": "Add an SEO title, a meta description and one internal link.",
"suggestionsAvailable": true
},
"overall": "warn",
"overallScore": 48,
"summary": "Add an SEO title, a meta description and one internal link.",
"credits": 4
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found429The organization's AI credits are exhausted
Example
curl -X POST "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/content/seo-audit" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"effort":"normal","language":"en-US"}'
SEO Audit History
GET /v1/stories/{storyId}/content/seo-audits · operationId listSeoAudits · scopes: stories:read
Every SEO audit that ran on the story, newest first, with the full report. Also available inline with include=seoAudits on the story endpoints.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit |
number | no | Page size (default 20, max 100) |
page |
number | no | Zero-based page number (default 0) |
Responses
200History
| Field | Type | Description |
|---|---|---|
data |
SeoAudit[] | SEO audit records |
data[].id |
string | Record id |
data[].storyId |
string | Audited story |
data[].overallScore |
number | 0 to 100 readiness score |
data[].overall |
string | pass, warn or fail |
data[].summary |
string | Short summary written in the report language |
data[].groupScores |
object | Per group 0 to 100 scores: onPage, keywords, links, aeo |
data[].checksRun |
string[] | Groups that were enabled |
data[].report |
object | Full report: storySnapshot, checks[] (each may carry a fix you can apply with POST /stories/{id}/content/seo-audit/apply-fix), keywords[], linkCandidates[], aeo |
data[].effort |
string | normal or high |
data[].provider |
string | LLM provider used for suggestions |
data[].model |
string | LLM model used |
data[].credits |
number | Credits charged (0 when suggestions were skipped) |
data[].createdAt |
string | ISO 8601 timestamp |
meta |
object | Pagination metadata |
meta.page |
number | Zero-based page returned |
meta.limit |
number | Page size used |
meta.hasMore |
boolean | Whether a next page may exist (the page was full) |
meta.storyId |
string | The story |
{
"data": [
{
"id": "68b9b7c2f1a2b3c4d5e6f750",
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"overallScore": 48,
"overall": "warn",
"summary": "Add an SEO title, a meta description and one internal link.",
"groupScores": {
"onPage": 55,
"keywords": 60,
"links": 40,
"aeo": 30
},
"checksRun": [
"onPage",
"keywords",
"links",
"aeo"
],
"report": {
"storySnapshot": {
"title": "AI Revolution in Healthcare",
"seoTitle": "",
"metaDescription": "",
"slug": "ai-revolution-in-healthcare",
"wordCount": 640,
"locale": "en-US",
"topicKey": "tech"
},
"checks": [
{
"id": "seoTitlePresent",
"group": "onPage",
"status": "fail",
"impact": "high",
"effort": "instant",
"fix": {
"kind": "setMetadataField",
"target": "title",
"after": "AI Revolution in Healthcare: What Changes in 2026"
}
},
{
"id": "metaDescriptionLength",
"group": "onPage",
"status": "warn",
"impact": "medium",
"effort": "instant"
}
],
"keywords": [
{
"term": "ai diagnostics",
"primary": true,
"bodyCount": 3,
"present": [
"h1",
"body"
],
"suggestion": {
"placement": "seoTitle",
"before": "",
"after": "AI diagnostics"
}
}
],
"linkCandidates": [
{
"storyId": "68b9b7c2f1a2b3c4d5e6f790",
"title": "Hospitals adopt AI triage",
"slug": "hospitals-adopt-ai-triage",
"relevance": 81,
"inboundLinks": 0,
"anchor": "AI triage",
"paragraphIndex": 2
}
],
"aeo": {
"hasDirectAnswer": false,
"proposedDirectAnswer": "AI is now used for diagnostics, triage and treatment planning in most large hospitals.",
"questionHeadingCount": 0,
"proposedQuestionHeadings": [],
"faqs": [],
"entities": [],
"citedClaims": 2,
"totalClaims": 5
},
"groupScores": {
"onPage": 55,
"keywords": 60,
"links": 40,
"aeo": 30
},
"overallScore": 48,
"overall": "warn",
"summary": "Add an SEO title, a meta description and one internal link.",
"suggestionsAvailable": true
},
"effort": "normal",
"provider": "anthropic",
"model": "claude-sonnet-5",
"credits": 4,
"createdAt": "2026-08-16T09:30:00.000Z"
}
],
"meta": {
"page": 0,
"limit": 20,
"hasMore": false,
"storyId": "68b9b7c2f1a2b3c4d5e6f700"
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found
Example
curl -X GET "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/content/seo-audits" \
-H "Authorization: Bearer nfk_xxx..."
Apply SEO Fix
POST /v1/stories/{storyId}/content/seo-audit/apply-fix · operationId applySeoFix · scopes: ai:generate + stories:write
Apply one fix from an audit report. Fix kinds: setMetadataField, setSlug, setImageAlt, replaceText, insertParagraphTop, insertHeading, insertLink, setFaqs, setEntities. direct mode applies the fix without a model and answers 422 when the text changed since the audit; retry with rewrite mode to let the model apply it (costs credits).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
storyId |
string | yes | The story id |
Request body (JSON): The fix and the mode
| Name | Type | Required | Description |
|---|---|---|---|
fix |
object | yes | A fix object from report.checks[] or report.keywords[].suggestion: { kind, target?, before?, after, paragraphIndex?, payload? } |
mode |
string | no | direct (default) or rewrite |
{
"fix": {
"kind": "setMetadataField",
"target": "title",
"after": "AI in Healthcare 2026: Diagnostics, Costs and Results"
},
"mode": "direct"
}
Responses
200Result
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
storyId |
string | The story |
applied |
boolean | Whether the story changed |
method |
string | direct or rewrite: |
field |
string | Field changed, e.g. metadata.title, slug, content |
before |
string | Value before |
after |
string | Value after |
{
"success": true,
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"applied": true,
"method": "direct",
"field": "metadata.title",
"before": "",
"after": "AI in Healthcare 2026: Diagnostics, Costs and Results"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Story not found422The story text drifted since the audit; retry with mode rewrite429The organization's AI credits are exhausted
Example
curl -X POST "https://client-api.news-factory.app/v1/stories/68b9b7c2f1a2b3c4d5e6f700/content/seo-audit/apply-fix" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"fix":{"kind":"setMetadataField","target":"title","after":"AI in Healthcare 2026: Diagnostics, Costs and Results"},"mode":"direct"}'
Content Sources
Articles, web pages, documents and images that stories are written from
List Content Sources
GET /v1/content-sources · operationId listContentSources · scopes: content-sources:read
Page through the Topic Workspace's content sources: articles saved from feeds, web pages, uploaded documents and images. usedIn tells how many stories were written from each source.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
sourceType |
string (rss, website, image, document) |
no | Only this kind of source |
language |
string (en-US, en-GB, de-DE, fr-FR, es-ES, es-MX, it-IT, pt-BR, pt-PT, nl-NL, pl-PL, sv-SE, da-DK, nb-NO, fi-FI, cs-CZ, ro-RO, hr-HR, tr-TR, ja-JP) |
no | Only sources in this language (full locale code) |
flagged |
boolean | no | Only flagged (true) or unflagged (false) sources |
processed |
boolean | no | Only sources that were (true) or were not (false) analysed |
limit |
number | no | Page size (default 20, max 100) |
page |
number | no | Zero-based page number (default 0) |
order |
string (createdAt DESC, createdAt ASC, pubDate DESC, updatedAt DESC, title ASC) |
no | Sort order as field ASC|DESC (default createdAt DESC). Allowed fields: createdAt, pubDate, updatedAt, title. |
Responses
200A page of content sources
| Field | Type | Description |
|---|---|---|
data |
ContentSource[] | Content sources |
data[].id |
string | Unique content source identifier |
data[].title |
string | Title |
data[].link |
string | Source URL (unique per Topic Workspace) |
data[].sourceType |
string | rss, website, image or document |
data[].summary |
string | Short summary or excerpt |
data[].author |
string | null | Author of the source |
data[].thumbnail |
string | null | Thumbnail image URL |
data[].language |
string | Full locale code of the source text |
data[].topicKey |
string | Topic Workspace the source belongs to |
data[].pubDate |
string | ISO 8601 publication date |
data[].scraped |
string | null | When the page was fetched and parsed |
data[].processed |
string | null | When AI analysis (summary, embeddings) ran |
data[].repurposed |
string | null | When a story was last written from it |
data[].flagged |
boolean | Flagged by a user or a workflow |
data[].flagReason |
string | null | Why it was flagged |
data[].sourceMeta |
object | Source specific metadata: feedId, feedName, feedUrl, feedIcon for RSS articles, documentType or imageType for uploads |
data[].usedIn |
number | How many stories reference this source |
data[].createdAt |
string | ISO 8601 creation timestamp |
data[].updatedAt |
string | ISO 8601 timestamp of the last update |
meta |
object | Pagination metadata |
meta.page |
number | Zero-based page returned |
meta.limit |
number | Page size used |
meta.hasMore |
boolean | Whether a next page may exist (the page was full) |
{
"data": [
{
"id": "68b9b7c2f1a2b3c4d5e6f760",
"title": "Hospitals report 40% cost reduction with AI triage",
"link": "https://techcrunch.com/2026/07/30/ai-healthcare/",
"sourceType": "rss",
"summary": "Pilot programmes across Europe report faster triage and lower costs.",
"author": "Sarah Perez",
"thumbnail": "https://techcrunch.com/wp-content/uploads/gettyimages-1445867611.jpg",
"language": "en-US",
"topicKey": "tech",
"pubDate": "2026-07-30T06:00:00.000Z",
"scraped": "2026-07-30T07:10:00.000Z",
"processed": "2026-07-30T07:12:00.000Z",
"repurposed": "2026-08-15T10:30:00.000Z",
"flagged": false,
"flagReason": null,
"sourceMeta": {
"feedId": "68b9b7c2f1a2b3c4d5e6f770",
"feedName": "TechCrunch",
"feedUrl": "https://techcrunch.com/feed/",
"feedIcon": "https://cdn.news-factory.app/feeds/techcrunch/favicon.ico"
},
"usedIn": 1,
"createdAt": "2026-07-30T07:00:00.000Z",
"updatedAt": "2026-08-15T10:30:00.000Z"
}
],
"meta": {
"page": 0,
"limit": 20,
"hasMore": false
}
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs
Example
curl -X GET "https://client-api.news-factory.app/v1/content-sources?sourceType=rss&limit=20" \
-H "Authorization: Bearer nfk_xxx..."
Create Content Source
POST /v1/content-sources · operationId createContentSource · scopes: content-sources:write
Register an article, page or document as a source in the Topic Workspace. Scrape it afterwards to fetch and parse the page, then repurpose it into a story.
Request body (JSON): Content source data
| Name | Type | Required | Description |
|---|---|---|---|
title |
string | yes | Title |
link |
string | yes | Absolute URL of the article or page. Unique per Topic Workspace; a duplicate answers 409 with the existing id. |
sourceType |
string | yes | rss, website, image or document |
summary |
string | no | Summary or excerpt |
content |
string | no | Full text when you already have it (otherwise use the scrape endpoint) |
author |
string | no | Author |
thumbnail |
string | no | Thumbnail URL |
language |
string | no | Full locale code of the source text |
pubDate |
string (ISO 8601) | no | Publication date (default now) |
topicKey |
string | no | Topic Workspace to save the source in (default: the key's Topic Workspace) |
{
"title": "Hospitals report 40% cost reduction with AI triage",
"link": "https://techcrunch.com/2026/07/30/ai-healthcare/",
"sourceType": "website",
"summary": "Pilot programmes across Europe report faster triage and lower costs.",
"language": "en-US"
}
Responses
201Content source created
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data |
ContentSource | The created source |
data.id |
string | Unique content source identifier |
data.title |
string | Title |
data.link |
string | Source URL (unique per Topic Workspace) |
data.sourceType |
string | rss, website, image or document |
data.summary |
string | Short summary or excerpt |
data.author |
string | null | Author of the source |
data.thumbnail |
string | null | Thumbnail image URL |
data.language |
string | Full locale code of the source text |
data.topicKey |
string | Topic Workspace the source belongs to |
data.pubDate |
string | ISO 8601 publication date |
data.scraped |
string | null | When the page was fetched and parsed |
data.processed |
string | null | When AI analysis (summary, embeddings) ran |
data.repurposed |
string | null | When a story was last written from it |
data.flagged |
boolean | Flagged by a user or a workflow |
data.flagReason |
string | null | Why it was flagged |
data.sourceMeta |
object | Source specific metadata: feedId, feedName, feedUrl, feedIcon for RSS articles, documentType or imageType for uploads |
data.usedIn |
number | How many stories reference this source |
data.createdAt |
string | ISO 8601 creation timestamp |
data.updatedAt |
string | ISO 8601 timestamp of the last update |
message |
string | Success message |
{
"success": true,
"data": {
"id": "68b9b7c2f1a2b3c4d5e6f760",
"title": "Hospitals report 40% cost reduction with AI triage",
"link": "https://techcrunch.com/2026/07/30/ai-healthcare/",
"sourceType": "website",
"summary": "Pilot programmes across Europe report faster triage and lower costs.",
"author": "Sarah Perez",
"thumbnail": "https://techcrunch.com/wp-content/uploads/gettyimages-1445867611.jpg",
"language": "en-US",
"topicKey": "tech",
"pubDate": "2026-07-30T06:00:00.000Z",
"scraped": null,
"processed": null,
"repurposed": null,
"flagged": false,
"flagReason": null,
"sourceMeta": {},
"usedIn": 0,
"createdAt": "2026-07-30T07:00:00.000Z",
"updatedAt": "2026-08-15T10:30:00.000Z"
},
"message": "Content source created successfully"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs409Conflict503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X POST "https://client-api.news-factory.app/v1/content-sources" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"title":"Hospitals report 40% cost reduction with AI triage","link":"https://techcrunch.com/2026/07/30/ai-healthcare/","sourceType":"website","summary":"Pilot programmes across Europe report faster triage and lower costs.","language":"en-US"}'
Get Content Source
GET /v1/content-sources/{id} · operationId getContentSource · scopes: content-sources:read
One content source with its scraped text, images, catalogue feed and the stories written from it.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The content source id |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
full |
boolean | no | Also return the raw HTML (rawSource) of scraped pages |
Responses
200The content source
| Field | Type | Description |
|---|---|---|
data |
ContentSource | The content source |
data.id |
string | Unique content source identifier |
data.title |
string | Title |
data.link |
string | Source URL (unique per Topic Workspace) |
data.sourceType |
string | rss, website, image or document |
data.summary |
string | Short summary or excerpt |
data.author |
string | null | Author of the source |
data.thumbnail |
string | null | Thumbnail image URL |
data.language |
string | Full locale code of the source text |
data.topicKey |
string | Topic Workspace the source belongs to |
data.pubDate |
string | ISO 8601 publication date |
data.scraped |
string | null | When the page was fetched and parsed |
data.processed |
string | null | When AI analysis (summary, embeddings) ran |
data.repurposed |
string | null | When a story was last written from it |
data.flagged |
boolean | Flagged by a user or a workflow |
data.flagReason |
string | null | Why it was flagged |
data.sourceMeta |
object | Source specific metadata: feedId, feedName, feedUrl, feedIcon for RSS articles, documentType or imageType for uploads |
data.usedIn |
number | How many stories reference this source |
data.createdAt |
string | ISO 8601 creation timestamp |
data.updatedAt |
string | ISO 8601 timestamp of the last update |
data.content |
string | null | Full text when available (RSS body or document text) |
data.parsedSource |
object | null | Scrape result: title, contentHTML, contentText, mainImage, excerpt, wordCount |
data.images |
Image[] | Images attached to the source as public Image objects |
data.feed |
Feed | null | The catalogue feed for RSS articles |
data.stories |
object[] | Stories written from this source: { id, title, createdAt } |
data.images[].id |
string | Unique image identifier |
data.images[].url |
string | Public URL of the image file |
data.images[].s3Url |
string | Deprecated alias of url with the same value; use url |
data.images[].width |
number | Width in pixels |
data.images[].height |
number | Height in pixels |
data.images[].format |
string | File format (PNG, JPEG, WEBP) |
data.images[].contentType |
string | MIME type of the image |
data.images[].fileSize |
number | null | File size in bytes |
data.images[].imageType |
string | Role on the story: main, thumbnail, gallery, content, other |
data.images[].origin |
string | Normalized provenance: generated (made by News Factory with an AI image model), stock (licensed stock provider), source (taken from the original article, usually not yours to republish), upload (uploaded or imported by URL) |
data.images[].aiGenerated |
boolean | Authoritative flag: true only when News Factory produced the image with an AI model, including crops and thumbnails derived from one. Use this (not sourceType) to pick images you can publish as your own. |
data.images[].generation |
object | null | Present when aiGenerated is true: { provider, model, prompt } (each may be null for older images) |
data.images[].sourceType |
string | Raw storage source (generated, stock, story, content-source, scraped, user-upload, url). Kept for compatibility; not a reliable AI indicator |
data.images[].parentId |
string | null | Id of the image this one was cropped or resized from |
data.images[].originalUrl |
string | null | Where the file was fetched from (article images, URL imports) |
data.images[].title |
string | null | Image title |
data.images[].description |
string | null | Image description |
data.images[].altText |
string | null | Alt text |
data.images[].caption |
string | null | Caption |
data.images[].author |
string | null | Author or photographer |
data.images[].source |
string | null | Attribution source: article URL, stock provider id, or AI provider id |
data.images[].license |
string | null | License (stock images) |
data.images[].creditLine |
string | null | Credit line to display (stock images) |
data.images[].tags |
string[] | Tags associated with the image |
data.images[].createdAt |
string | ISO 8601 creation timestamp |
data.images[].updatedAt |
string | null | ISO 8601 timestamp of the last update |
{
"data": {
"id": "68b9b7c2f1a2b3c4d5e6f760",
"title": "Hospitals report 40% cost reduction with AI triage",
"link": "https://techcrunch.com/2026/07/30/ai-healthcare/",
"sourceType": "rss",
"summary": "Pilot programmes across Europe report faster triage and lower costs.",
"author": "Sarah Perez",
"thumbnail": "https://techcrunch.com/wp-content/uploads/gettyimages-1445867611.jpg",
"language": "en-US",
"topicKey": "tech",
"pubDate": "2026-07-30T06:00:00.000Z",
"scraped": "2026-07-30T07:10:00.000Z",
"processed": "2026-07-30T07:12:00.000Z",
"repurposed": "2026-08-15T10:30:00.000Z",
"flagged": false,
"flagReason": null,
"sourceMeta": {
"feedId": "68b9b7c2f1a2b3c4d5e6f770",
"feedName": "TechCrunch",
"feedUrl": "https://techcrunch.com/feed/",
"feedIcon": "https://cdn.news-factory.app/feeds/techcrunch/favicon.ico"
},
"usedIn": 1,
"createdAt": "2026-07-30T07:00:00.000Z",
"updatedAt": "2026-08-15T10:30:00.000Z",
"content": null,
"parsedSource": {
"title": "Hospitals report 40% cost reduction with AI triage",
"contentHTML": "<p>Pilot programmes across Europe...</p>",
"contentText": "Pilot programmes across Europe...",
"mainImage": "https://techcrunch.com/wp-content/uploads/gettyimages-1445867611.jpg",
"excerpt": "Pilot programmes across Europe report faster triage and lower costs.",
"wordCount": 1250
},
"images": [
{
"id": "68b9b7c2f1a2b3c4d5e6f701",
"url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/gettyimages-1445867611.jpg",
"s3Url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/gettyimages-1445867611.jpg",
"originalUrl": "https://techcrunch.com/wp-content/uploads/gettyimages-1445867611.jpg",
"width": 1024,
"height": 683,
"format": "JPEG",
"contentType": "image/jpeg",
"fileSize": 214532,
"imageType": "main",
"origin": "source",
"aiGenerated": false,
"generation": null,
"sourceType": "story",
"parentId": null,
"title": "AI Revolution in Healthcare",
"description": null,
"altText": null,
"caption": null,
"author": null,
"source": "https://techcrunch.com/2026/07/30/ai-healthcare/",
"license": null,
"creditLine": null,
"tags": [],
"createdAt": "2026-08-15T10:30:00.000Z",
"updatedAt": "2026-08-15T10:30:00.000Z"
}
],
"feed": {
"id": "68b9b7c2f1a2b3c4d5e6f770",
"name": "TechCrunch",
"url": "https://techcrunch.com/feed/",
"favicon": "https://cdn.news-factory.app/feeds/techcrunch/favicon.ico"
},
"stories": [
{
"id": "68b9b7c2f1a2b3c4d5e6f700",
"title": "AI Revolution in Healthcare",
"createdAt": "2026-08-15T10:30:00.000Z"
}
]
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Content source not found
Example
curl -X GET "https://client-api.news-factory.app/v1/content-sources/68b9b7c2f1a2b3c4d5e6f760" \
-H "Authorization: Bearer nfk_xxx..."
Update Content Source
PATCH /v1/content-sources/{id} · operationId updateContentSource · scopes: content-sources:write
Update editable fields (title, summary, content, author, thumbnail, language, pubDate, flagged, flagReason) and return the fresh record.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The content source id |
Request body (JSON): Fields to update (at least one)
| Name | Type | Required | Description |
|---|---|---|---|
title |
string | no | Title |
summary |
string | no | Summary or excerpt |
content |
string | no | Full text when you already have it (otherwise use the scrape endpoint) |
author |
string | no | Author |
thumbnail |
string | no | Thumbnail URL |
language |
string | no | Full locale code of the source text |
pubDate |
string (ISO 8601) | no | Publication date (default now) |
flagged |
boolean | no | Flag the source so workflows skip it |
flagReason |
string | no | Why it is flagged |
{
"flagged": true,
"flagReason": "Duplicate of another source"
}
Responses
200Content source updated
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data |
ContentSource | The updated source |
data.id |
string | Unique content source identifier |
data.title |
string | Title |
data.link |
string | Source URL (unique per Topic Workspace) |
data.sourceType |
string | rss, website, image or document |
data.summary |
string | Short summary or excerpt |
data.author |
string | null | Author of the source |
data.thumbnail |
string | null | Thumbnail image URL |
data.language |
string | Full locale code of the source text |
data.topicKey |
string | Topic Workspace the source belongs to |
data.pubDate |
string | ISO 8601 publication date |
data.scraped |
string | null | When the page was fetched and parsed |
data.processed |
string | null | When AI analysis (summary, embeddings) ran |
data.repurposed |
string | null | When a story was last written from it |
data.flagged |
boolean | Flagged by a user or a workflow |
data.flagReason |
string | null | Why it was flagged |
data.sourceMeta |
object | Source specific metadata: feedId, feedName, feedUrl, feedIcon for RSS articles, documentType or imageType for uploads |
data.usedIn |
number | How many stories reference this source |
data.createdAt |
string | ISO 8601 creation timestamp |
data.updatedAt |
string | ISO 8601 timestamp of the last update |
data.content |
string | null | Full text when available (RSS body or document text) |
data.parsedSource |
object | null | Scrape result: title, contentHTML, contentText, mainImage, excerpt, wordCount |
data.images |
Image[] | Images attached to the source as public Image objects |
data.feed |
Feed | null | The catalogue feed for RSS articles |
data.stories |
object[] | Stories written from this source: { id, title, createdAt } |
data.images[].id |
string | Unique image identifier |
data.images[].url |
string | Public URL of the image file |
data.images[].s3Url |
string | Deprecated alias of url with the same value; use url |
data.images[].width |
number | Width in pixels |
data.images[].height |
number | Height in pixels |
data.images[].format |
string | File format (PNG, JPEG, WEBP) |
data.images[].contentType |
string | MIME type of the image |
data.images[].fileSize |
number | null | File size in bytes |
data.images[].imageType |
string | Role on the story: main, thumbnail, gallery, content, other |
data.images[].origin |
string | Normalized provenance: generated (made by News Factory with an AI image model), stock (licensed stock provider), source (taken from the original article, usually not yours to republish), upload (uploaded or imported by URL) |
data.images[].aiGenerated |
boolean | Authoritative flag: true only when News Factory produced the image with an AI model, including crops and thumbnails derived from one. Use this (not sourceType) to pick images you can publish as your own. |
data.images[].generation |
object | null | Present when aiGenerated is true: { provider, model, prompt } (each may be null for older images) |
data.images[].sourceType |
string | Raw storage source (generated, stock, story, content-source, scraped, user-upload, url). Kept for compatibility; not a reliable AI indicator |
data.images[].parentId |
string | null | Id of the image this one was cropped or resized from |
data.images[].originalUrl |
string | null | Where the file was fetched from (article images, URL imports) |
data.images[].title |
string | null | Image title |
data.images[].description |
string | null | Image description |
data.images[].altText |
string | null | Alt text |
data.images[].caption |
string | null | Caption |
data.images[].author |
string | null | Author or photographer |
data.images[].source |
string | null | Attribution source: article URL, stock provider id, or AI provider id |
data.images[].license |
string | null | License (stock images) |
data.images[].creditLine |
string | null | Credit line to display (stock images) |
data.images[].tags |
string[] | Tags associated with the image |
data.images[].createdAt |
string | ISO 8601 creation timestamp |
data.images[].updatedAt |
string | null | ISO 8601 timestamp of the last update |
message |
string | Success message |
{
"success": true,
"data": {
"id": "68b9b7c2f1a2b3c4d5e6f760",
"title": "Hospitals report 40% cost reduction with AI triage",
"link": "https://techcrunch.com/2026/07/30/ai-healthcare/",
"sourceType": "rss",
"summary": "Pilot programmes across Europe report faster triage and lower costs.",
"author": "Sarah Perez",
"thumbnail": "https://techcrunch.com/wp-content/uploads/gettyimages-1445867611.jpg",
"language": "en-US",
"topicKey": "tech",
"pubDate": "2026-07-30T06:00:00.000Z",
"scraped": "2026-07-30T07:10:00.000Z",
"processed": "2026-07-30T07:12:00.000Z",
"repurposed": "2026-08-15T10:30:00.000Z",
"flagged": true,
"flagReason": "Duplicate of another source",
"sourceMeta": {
"feedId": "68b9b7c2f1a2b3c4d5e6f770",
"feedName": "TechCrunch",
"feedUrl": "https://techcrunch.com/feed/",
"feedIcon": "https://cdn.news-factory.app/feeds/techcrunch/favicon.ico"
},
"usedIn": 1,
"createdAt": "2026-07-30T07:00:00.000Z",
"updatedAt": "2026-08-15T10:30:00.000Z",
"content": null,
"parsedSource": {
"title": "Hospitals report 40% cost reduction with AI triage",
"contentHTML": "<p>Pilot programmes across Europe...</p>",
"contentText": "Pilot programmes across Europe...",
"mainImage": "https://techcrunch.com/wp-content/uploads/gettyimages-1445867611.jpg",
"excerpt": "Pilot programmes across Europe report faster triage and lower costs.",
"wordCount": 1250
},
"images": [
{
"id": "68b9b7c2f1a2b3c4d5e6f701",
"url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/gettyimages-1445867611.jpg",
"s3Url": "https://cdn.news-factory.app/stories/68b9b7c2f1a2b3c4d5e6f700/gettyimages-1445867611.jpg",
"originalUrl": "https://techcrunch.com/wp-content/uploads/gettyimages-1445867611.jpg",
"width": 1024,
"height": 683,
"format": "JPEG",
"contentType": "image/jpeg",
"fileSize": 214532,
"imageType": "main",
"origin": "source",
"aiGenerated": false,
"generation": null,
"sourceType": "story",
"parentId": null,
"title": "AI Revolution in Healthcare",
"description": null,
"altText": null,
"caption": null,
"author": null,
"source": "https://techcrunch.com/2026/07/30/ai-healthcare/",
"license": null,
"creditLine": null,
"tags": [],
"createdAt": "2026-08-15T10:30:00.000Z",
"updatedAt": "2026-08-15T10:30:00.000Z"
}
],
"feed": {
"id": "68b9b7c2f1a2b3c4d5e6f770",
"name": "TechCrunch",
"url": "https://techcrunch.com/feed/",
"favicon": "https://cdn.news-factory.app/feeds/techcrunch/favicon.ico"
},
"stories": [
{
"id": "68b9b7c2f1a2b3c4d5e6f700",
"title": "AI Revolution in Healthcare",
"createdAt": "2026-08-15T10:30:00.000Z"
}
]
},
"message": "Content source updated successfully"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Content source not found
Example
curl -X PATCH "https://client-api.news-factory.app/v1/content-sources/68b9b7c2f1a2b3c4d5e6f760" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"flagged":true,"flagReason":"Duplicate of another source"}'
Delete Content Source
DELETE /v1/content-sources/{id} · operationId deleteContentSource · scopes: content-sources:write
Delete a content source. Its images are removed from storage when no story uses them. Stories written from the source are kept.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The content source id |
Responses
200Content source deleted
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
message |
string | Success message |
{
"success": true,
"message": "Content source deleted successfully"
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Content source not found503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X DELETE "https://client-api.news-factory.app/v1/content-sources/68b9b7c2f1a2b3c4d5e6f760" \
-H "Authorization: Bearer nfk_xxx..."
Scrape Content Source
POST /v1/content-sources/{id}/scrape · operationId scrapeContentSource · scopes: content-sources:write
Fetch the source page and extract its main text, title and image into parsedSource. Sources that were already scraped are left alone unless force is true. Read the result with GET /content-sources/{id}.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The content source id |
Request body (JSON): Options
| Name | Type | Required | Description |
|---|---|---|---|
force |
boolean | no | Fetch again even if the page was scraped before |
{
"force": false
}
Responses
200Scrape result
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data.id |
string | The content source |
data.scraped |
boolean | Whether a fetch happened in this call |
data.scrapedAt |
string | null | When the page was fetched |
data.parsedSource |
object | null | { title, contentHTML, contentText, mainImage, excerpt, wordCount } when a fetch happened |
message |
string | What happened |
{
"success": true,
"data": {
"id": "68b9b7c2f1a2b3c4d5e6f760",
"scraped": true,
"scrapedAt": "2026-07-30T07:10:00.000Z",
"parsedSource": {
"title": "Hospitals report 40% cost reduction with AI triage",
"contentHTML": "<p>Pilot programmes across Europe...</p>",
"contentText": "Pilot programmes across Europe...",
"mainImage": "https://techcrunch.com/wp-content/uploads/gettyimages-1445867611.jpg",
"excerpt": "Pilot programmes across Europe report faster triage and lower costs.",
"wordCount": 1250
}
},
"message": "Source scraped"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Content source not found503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X POST "https://client-api.news-factory.app/v1/content-sources/68b9b7c2f1a2b3c4d5e6f760/scrape" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"force":false}'
Repurpose into a Story
POST /v1/content-sources/{id}/repurpose · operationId repurposeContentSource · scopes: ai:generate + content-sources:read + stories:write
Write a new story from the content source with the Topic Workspace's story settings (tone, length, structure). Scrape the source first so the model has the full text. Costs AI credits.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The content source id |
Request body (JSON): Options
| Name | Type | Required | Description |
|---|---|---|---|
languageCode |
string | no | Language of the new story as a full locale code (default: the Topic Workspace's default language) |
extraInstructions |
string | no | Extra editorial instructions for this story |
authorId |
string | no | Author to attribute the story to (also applies the author's writing style) |
temperature |
number | no | Model temperature 0 to 1 |
{
"languageCode": "en-US"
}
Responses
200Story written
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data.contentSourceId |
string | The source |
data.storyId |
string | Id of the new story (load it with GET /stories/{id}) |
data.storyTitle |
string | Title of the new story |
data.contentSourceIds |
string[] | Sources the story was written from |
data.authorId |
string | null | Attributed author |
message |
string | Success message |
{
"success": true,
"data": {
"contentSourceId": "68b9b7c2f1a2b3c4d5e6f760",
"storyId": "68b9b7c2f1a2b3c4d5e6f700",
"storyTitle": "AI Revolution in Healthcare",
"contentSourceIds": [
"68b9b7c2f1a2b3c4d5e6f760"
],
"authorId": null
},
"message": "Content source repurposed into a story"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Content source not found429The organization's AI credits are exhausted503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X POST "https://client-api.news-factory.app/v1/content-sources/68b9b7c2f1a2b3c4d5e6f760/repurpose" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"languageCode":"en-US"}'
RSS Feeds
Subscribe to catalogue feeds and read their articles with the topic's evaluation
List Subscribed Feeds
GET /v1/feeds · operationId listFeeds · scopes: feeds:read
The feeds the Topic Workspace follows. Articles from these feeds are what the News Hub and workflows evaluate. Browse other feeds with the catalogue endpoint and subscribe with POST /feeds.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit |
number | no | Page size (default 50, max 100) |
page |
number | no | Zero-based page number (default 0) |
Responses
200The subscribed feeds
| Field | Type | Description |
|---|---|---|
data |
Feed[] | Feeds (all with subscribed: true) |
data[].id |
string | Unique feed identifier |
data[].url |
string | RSS or Atom feed URL |
data[].name |
string | Feed name |
data[].website |
string | null | Publisher website |
data[].favicon |
string | null | Favicon URL |
data[].language |
string | null | Language of the feed as published by the catalogue (bare ISO code, the catalogue does not carry regions) |
data[].country |
string | null | ISO country code |
data[].category |
string | null | news, technology, sports, business, science, ... |
data[].contentType |
string | null | news, wire, magazine, trade, local or blog |
data[].trustTier |
number | null | Editorial trust tier (1 is highest) |
data[].qualityScore |
number | null | Catalogue quality score |
data[].articlesPerDay |
number | null | Average publishing rate |
data[].enabled |
boolean | Whether the platform fetches this feed |
data[].checkFrequency |
number | Seconds between fetches |
data[].lastFetchedAt |
string | null | Last successful fetch |
data[].nextFetchDue |
string | null | Next scheduled fetch |
data[].subscribed |
boolean | Whether the current Topic Workspace follows this feed |
data[].createdAt |
string | ISO 8601 creation timestamp |
meta |
object | Pagination metadata |
meta.page |
number | Zero-based page returned |
meta.limit |
number | Page size used |
meta.hasMore |
boolean | Whether a next page may exist (the page was full) |
meta.total |
number | Number of subscribed feeds |
{
"data": [
{
"id": "68b9b7c2f1a2b3c4d5e6f770",
"url": "https://techcrunch.com/feed/",
"name": "TechCrunch",
"website": "https://techcrunch.com",
"favicon": "https://cdn.news-factory.app/feeds/techcrunch/favicon.ico",
"language": "en",
"country": "US",
"category": "technology",
"contentType": "news",
"trustTier": 2,
"qualityScore": 82,
"articlesPerDay": 38,
"enabled": true,
"checkFrequency": 3600,
"lastFetchedAt": "2026-09-05T10:00:00.000Z",
"nextFetchDue": "2026-09-05T11:00:00.000Z",
"subscribed": true,
"createdAt": "2026-01-15T09:00:00.000Z"
}
],
"meta": {
"page": 0,
"limit": 50,
"hasMore": false,
"total": 1
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs
Example
curl -X GET "https://client-api.news-factory.app/v1/feeds" \
-H "Authorization: Bearer nfk_xxx..."
Subscribe to Feed
POST /v1/feeds · operationId subscribeFeed · scopes: feeds:write
Follow a catalogue feed for the Topic Workspace, by feedId or by its feed url. Feeds outside the catalogue answer 404: the catalogue is curated, ask support to add a source. Subscribing twice is harmless.
Request body (JSON): feedId or url
| Name | Type | Required | Description |
|---|---|---|---|
feedId |
string | no | Catalogue feed id |
url |
string | no | Feed URL exactly as listed in the catalogue |
{
"feedId": "68b9b7c2f1a2b3c4d5e6f770"
}
Responses
200Subscribed
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data.feed |
Feed | The feed |
data.feed.id |
string | Unique feed identifier |
data.feed.url |
string | RSS or Atom feed URL |
data.feed.name |
string | Feed name |
data.feed.website |
string | null | Publisher website |
data.feed.favicon |
string | null | Favicon URL |
data.feed.language |
string | null | Language of the feed as published by the catalogue (bare ISO code, the catalogue does not carry regions) |
data.feed.country |
string | null | ISO country code |
data.feed.category |
string | null | news, technology, sports, business, science, ... |
data.feed.contentType |
string | null | news, wire, magazine, trade, local or blog |
data.feed.trustTier |
number | null | Editorial trust tier (1 is highest) |
data.feed.qualityScore |
number | null | Catalogue quality score |
data.feed.articlesPerDay |
number | null | Average publishing rate |
data.feed.enabled |
boolean | Whether the platform fetches this feed |
data.feed.checkFrequency |
number | Seconds between fetches |
data.feed.lastFetchedAt |
string | null | Last successful fetch |
data.feed.nextFetchDue |
string | null | Next scheduled fetch |
data.feed.subscribed |
boolean | Whether the current Topic Workspace follows this feed |
data.feed.createdAt |
string | ISO 8601 creation timestamp |
data.topicKey |
string | Topic Workspace that now follows the feed |
data.followedFeeds |
string[] | All feed ids the Topic Workspace follows |
message |
string | Success message |
{
"success": true,
"data": {
"feed": {
"id": "68b9b7c2f1a2b3c4d5e6f770",
"url": "https://techcrunch.com/feed/",
"name": "TechCrunch",
"website": "https://techcrunch.com",
"favicon": "https://cdn.news-factory.app/feeds/techcrunch/favicon.ico",
"language": "en",
"country": "US",
"category": "technology",
"contentType": "news",
"trustTier": 2,
"qualityScore": 82,
"articlesPerDay": 38,
"enabled": true,
"checkFrequency": 3600,
"lastFetchedAt": "2026-09-05T10:00:00.000Z",
"nextFetchDue": "2026-09-05T11:00:00.000Z",
"subscribed": true,
"createdAt": "2026-01-15T09:00:00.000Z"
},
"topicKey": "tech",
"followedFeeds": [
"68b9b7c2f1a2b3c4d5e6f770"
]
},
"message": "Successfully followed feed"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Feed not found503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X POST "https://client-api.news-factory.app/v1/feeds" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{"feedId":"68b9b7c2f1a2b3c4d5e6f770"}'
Search Feed Catalog
GET /v1/feeds/catalog · operationId searchFeedCatalog · scopes: feeds:read
Search the curated catalogue of feeds by name, URL, language, country or category. Use a result's id with POST /feeds to subscribe.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
search |
string | no | Matches the feed name, URL or website (case insensitive) |
language |
string | no | Catalogue language code (bare ISO code such as en, de, pt) |
country |
string | no | ISO country code |
category |
string | no | news, technology, business, science, sports, ... |
limit |
number | no | Page size (default 20, max 100) |
page |
number | no | Zero-based page number (default 0) |
order |
string (name ASC, qualityScore DESC, articlesPerDay DESC, trustTier ASC) |
no | Sort order as field ASC|DESC (default name ASC). Allowed fields: name, qualityScore, articlesPerDay, trustTier. |
Responses
200Matching feeds
| Field | Type | Description |
|---|---|---|
data |
Feed[] | Catalogue feeds (subscribed is not included here) |
data[].id |
string | Unique feed identifier |
data[].url |
string | RSS or Atom feed URL |
data[].name |
string | Feed name |
data[].website |
string | null | Publisher website |
data[].favicon |
string | null | Favicon URL |
data[].language |
string | null | Language of the feed as published by the catalogue (bare ISO code, the catalogue does not carry regions) |
data[].country |
string | null | ISO country code |
data[].category |
string | null | news, technology, sports, business, science, ... |
data[].contentType |
string | null | news, wire, magazine, trade, local or blog |
data[].trustTier |
number | null | Editorial trust tier (1 is highest) |
data[].qualityScore |
number | null | Catalogue quality score |
data[].articlesPerDay |
number | null | Average publishing rate |
data[].enabled |
boolean | Whether the platform fetches this feed |
data[].checkFrequency |
number | Seconds between fetches |
data[].lastFetchedAt |
string | null | Last successful fetch |
data[].nextFetchDue |
string | null | Next scheduled fetch |
data[].createdAt |
string | ISO 8601 creation timestamp |
meta |
object | Pagination metadata |
meta.page |
number | Zero-based page returned |
meta.limit |
number | Page size used |
meta.hasMore |
boolean | Whether a next page may exist (the page was full) |
{
"data": [
{
"id": "68b9b7c2f1a2b3c4d5e6f770",
"url": "https://techcrunch.com/feed/",
"name": "TechCrunch",
"website": "https://techcrunch.com",
"favicon": "https://cdn.news-factory.app/feeds/techcrunch/favicon.ico",
"language": "en",
"country": "US",
"category": "technology",
"contentType": "news",
"trustTier": 2,
"qualityScore": 82,
"articlesPerDay": 38,
"enabled": true,
"checkFrequency": 3600,
"lastFetchedAt": "2026-09-05T10:00:00.000Z",
"nextFetchDue": "2026-09-05T11:00:00.000Z",
"createdAt": "2026-01-15T09:00:00.000Z"
}
],
"meta": {
"page": 0,
"limit": 20,
"hasMore": false
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs
Example
curl -X GET "https://client-api.news-factory.app/v1/feeds/catalog?search=techcrunch&language=en&limit=10" \
-H "Authorization: Bearer nfk_xxx..."
Get Feed
GET /v1/feeds/{id} · operationId getFeed · scopes: feeds:read
One catalogue feed with its fetch state and whether the Topic Workspace follows it.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The feed id |
Responses
200The feed
| Field | Type | Description |
|---|---|---|
data |
Feed | The feed |
data.id |
string | Unique feed identifier |
data.url |
string | RSS or Atom feed URL |
data.name |
string | Feed name |
data.website |
string | null | Publisher website |
data.favicon |
string | null | Favicon URL |
data.language |
string | null | Language of the feed as published by the catalogue (bare ISO code, the catalogue does not carry regions) |
data.country |
string | null | ISO country code |
data.category |
string | null | news, technology, sports, business, science, ... |
data.contentType |
string | null | news, wire, magazine, trade, local or blog |
data.trustTier |
number | null | Editorial trust tier (1 is highest) |
data.qualityScore |
number | null | Catalogue quality score |
data.articlesPerDay |
number | null | Average publishing rate |
data.enabled |
boolean | Whether the platform fetches this feed |
data.checkFrequency |
number | Seconds between fetches |
data.lastFetchedAt |
string | null | Last successful fetch |
data.nextFetchDue |
string | null | Next scheduled fetch |
data.subscribed |
boolean | Whether the current Topic Workspace follows this feed |
data.createdAt |
string | ISO 8601 creation timestamp |
{
"data": {
"id": "68b9b7c2f1a2b3c4d5e6f770",
"url": "https://techcrunch.com/feed/",
"name": "TechCrunch",
"website": "https://techcrunch.com",
"favicon": "https://cdn.news-factory.app/feeds/techcrunch/favicon.ico",
"language": "en",
"country": "US",
"category": "technology",
"contentType": "news",
"trustTier": 2,
"qualityScore": 82,
"articlesPerDay": 38,
"enabled": true,
"checkFrequency": 3600,
"lastFetchedAt": "2026-09-05T10:00:00.000Z",
"nextFetchDue": "2026-09-05T11:00:00.000Z",
"subscribed": true,
"createdAt": "2026-01-15T09:00:00.000Z"
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Feed not found
Example
curl -X GET "https://client-api.news-factory.app/v1/feeds/68b9b7c2f1a2b3c4d5e6f770" \
-H "Authorization: Bearer nfk_xxx..."
Unsubscribe from Feed
DELETE /v1/feeds/{id} · operationId unsubscribeFeed · scopes: feeds:write
Stop following a feed for the Topic Workspace. Articles already saved as content sources are kept.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The feed id |
Responses
200Unsubscribed
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
message |
string | Success message |
{
"success": true,
"message": "Unsubscribed from feed"
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404The Topic Workspace does not follow this feed503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X DELETE "https://client-api.news-factory.app/v1/feeds/68b9b7c2f1a2b3c4d5e6f770" \
-H "Authorization: Bearer nfk_xxx..."
Fetch Feed Now
POST /v1/feeds/{id}/fetch · operationId fetchFeedArticles · scopes: feeds:write
Fetch the feed immediately instead of waiting for the scheduler, and store new articles in the catalogue. Unchanged feeds (HTTP 304) answer with modified: false.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The feed id |
Responses
200Fetch result
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data.feedId |
string | The feed |
data.modified |
boolean | False when the publisher answered 304 Not Modified |
data.statusCode |
number | null | HTTP status from the publisher |
data.feedTitle |
string | null | Title from the feed |
data.itemCount |
number | Items in the feed |
data.items |
object[] | Parsed items: { title, link, pubDate, description, author, thumbnail } |
data.persistence |
object | null | How many items were created or updated in the catalogue |
data.linkedToContentSources |
number | Items matched to existing content sources |
message |
string | What happened |
{
"success": true,
"data": {
"feedId": "68b9b7c2f1a2b3c4d5e6f770",
"modified": true,
"statusCode": 200,
"feedTitle": "TechCrunch",
"itemCount": 20,
"items": [
{
"title": "OpenAI announces new medical reasoning model",
"link": "https://techcrunch.com/2026/09/04/openai-medical-model/",
"pubDate": "2026-09-04T15:00:00.000Z",
"description": "The company says the model is tuned for clinical documentation.",
"author": "Kyle Wiggers",
"thumbnail": "https://techcrunch.com/wp-content/uploads/openai-medical.jpg"
}
],
"persistence": {
"created": 3,
"updated": 17
},
"linkedToContentSources": 0
},
"message": "Feed fetched"
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Feed not found503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X POST "https://client-api.news-factory.app/v1/feeds/68b9b7c2f1a2b3c4d5e6f770/fetch" \
-H "Authorization: Bearer nfk_xxx..."
List Articles
GET /v1/feeds/articles · operationId listFeedArticles · scopes: feeds:read
Articles from the Topic Workspace's subscribed feeds together with their topic evaluation. By default returns evaluated articles (newest evaluation first), filterable by minConfidence; feedId pages through one feed's articles evaluated or not; unevaluated=true returns what the Topic Workspace has not looked at yet.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
feedId |
string | no | Only this feed's articles (evaluated and unevaluated) |
minConfidence |
number | no | Only articles evaluated at this topic match or higher (0 to 100) |
evaluatedBy |
string (agent, human) |
no | Only evaluations by the agent or by a person |
unevaluated |
boolean | no | Only articles without an evaluation for this Topic Workspace |
limit |
number | no | Page size (default 20, max 100) |
page |
number | no | Zero-based page number (default 0) |
order |
string (createdAt DESC, pubDate DESC, matchConfidence DESC) |
no | Sort order as field ASC|DESC (default createdAt DESC). Allowed fields: createdAt, pubDate, matchConfidence. |
Responses
200A page of articles
| Field | Type | Description |
|---|---|---|
data |
Article[] | Articles |
data[].id |
string | Unique article identifier |
data[].title |
string | Article title |
data[].url |
string | Article URL |
data[].description |
string | null | Description or summary from the feed |
data[].author |
string | null | Author |
data[].thumbnail |
string | null | Image URL from the feed |
data[].pubDate |
string | ISO 8601 publication date |
data[].language |
string | null | Language of the article as published by the feed |
data[].feedId |
string | Feed the article came from |
data[].matchConfidence |
number | null | Topic match from 0 to 100. null means not evaluated yet for this Topic Workspace; 0 means evaluated as no match |
data[].evaluatedBy |
string | null | agent, human or null |
data[].evaluatedAt |
string | null | When the evaluation was recorded |
data[].createdAt |
string | null | When the article entered the catalogue |
meta |
object | Pagination metadata |
meta.page |
number | Zero-based page returned |
meta.limit |
number | Page size used |
meta.hasMore |
boolean | Whether a next page may exist (the page was full) |
meta.unevaluated |
boolean | Whether the unevaluated view was returned |
meta.feedId |
string | Feed filter applied (feed view only) |
meta.total |
number | Total unevaluated articles (unevaluated view only) |
{
"data": [
{
"id": "68b9b7c2f1a2b3c4d5e6f780",
"title": "OpenAI announces new medical reasoning model",
"url": "https://techcrunch.com/2026/09/04/openai-medical-model/",
"description": "The company says the model is tuned for clinical documentation.",
"author": "Kyle Wiggers",
"thumbnail": "https://techcrunch.com/wp-content/uploads/openai-medical.jpg",
"pubDate": "2026-09-04T15:00:00.000Z",
"language": "en",
"feedId": "68b9b7c2f1a2b3c4d5e6f770",
"matchConfidence": 88,
"evaluatedBy": "agent",
"evaluatedAt": "2026-09-04T15:30:00.000Z",
"createdAt": "2026-09-04T15:05:00.000Z"
}
],
"meta": {
"page": 0,
"limit": 20,
"hasMore": false,
"unevaluated": false
}
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs
Example
curl -X GET "https://client-api.news-factory.app/v1/feeds/articles?minConfidence=50&limit=20" \
-H "Authorization: Bearer nfk_xxx..."
Get Article
GET /v1/feeds/articles/{id} · operationId getArticleDetails · scopes: feeds:read
One article with its evaluation for the Topic Workspace. Articles the Topic Workspace has not evaluated yet are only found when you also pass their feedId.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The article id |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
feedId |
string | no | The article's feed, needed for unevaluated articles |
Responses
200The article
| Field | Type | Description |
|---|---|---|
data |
Article | The article |
data.id |
string | Unique article identifier |
data.title |
string | Article title |
data.url |
string | Article URL |
data.description |
string | null | Description or summary from the feed |
data.author |
string | null | Author |
data.thumbnail |
string | null | Image URL from the feed |
data.pubDate |
string | ISO 8601 publication date |
data.language |
string | null | Language of the article as published by the feed |
data.feedId |
string | Feed the article came from |
data.matchConfidence |
number | null | Topic match from 0 to 100. null means not evaluated yet for this Topic Workspace; 0 means evaluated as no match |
data.evaluatedBy |
string | null | agent, human or null |
data.evaluatedAt |
string | null | When the evaluation was recorded |
data.createdAt |
string | null | When the article entered the catalogue |
{
"data": {
"id": "68b9b7c2f1a2b3c4d5e6f780",
"title": "OpenAI announces new medical reasoning model",
"url": "https://techcrunch.com/2026/09/04/openai-medical-model/",
"description": "The company says the model is tuned for clinical documentation.",
"author": "Kyle Wiggers",
"thumbnail": "https://techcrunch.com/wp-content/uploads/openai-medical.jpg",
"pubDate": "2026-09-04T15:00:00.000Z",
"language": "en",
"feedId": "68b9b7c2f1a2b3c4d5e6f770",
"matchConfidence": 88,
"evaluatedBy": "agent",
"evaluatedAt": "2026-09-04T15:30:00.000Z",
"createdAt": "2026-09-04T15:05:00.000Z"
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Not found, or not evaluated yet and no feedId given
Example
curl -X GET "https://client-api.news-factory.app/v1/feeds/articles/68b9b7c2f1a2b3c4d5e6f780" \
-H "Authorization: Bearer nfk_xxx..."
Scheduler Status
GET /v1/feeds/scheduler/status · operationId schedulerStatus · scopes: feeds:read
State of the platform's feed fetch scheduler: whether it runs, how often, and when the next global fetch is due.
Responses
200Scheduler state
| Field | Type | Description |
|---|---|---|
data.active |
boolean | Whether the scheduler job is registered |
data.nextRun |
string | null | ISO 8601 time of the next global fetch |
data.intervalMinutes |
number | null | Minutes between fetch rounds |
data.jobCount |
number | Scheduled jobs in the queue |
data.timestamp |
string | When this status was read |
{
"data": {
"active": true,
"nextRun": "2026-09-05T13:00:00.000Z",
"intervalMinutes": 30,
"jobCount": 2,
"timestamp": "2026-09-05T12:41:03.000Z"
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X GET "https://client-api.news-factory.app/v1/feeds/scheduler/status" \
-H "Authorization: Bearer nfk_xxx..."
Workflows
Run the automation workflows designed in the app and follow their runs step by step
List Workflows
GET /v1/workflows · operationId listWorkflows · scopes: workflows:read
The Topic Workspace's workflows with their node list. Workflows are designed in the app; the API lets you run them and follow their runs.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
isActive |
boolean | no | Only active (true) or inactive (false) workflows |
limit |
number | no | Page size (default 20, max 100) |
page |
number | no | Zero-based page number (default 0) |
order |
string (createdAt DESC, updatedAt DESC, name ASC) |
no | Sort order as field ASC|DESC (default createdAt DESC). Allowed fields: createdAt, updatedAt, name. |
Responses
200A page of workflows
| Field | Type | Description |
|---|---|---|
data |
Workflow[] | Workflows |
data[].id |
string | Unique workflow identifier |
data[].name |
string | Workflow name |
data[].description |
string | null | Description |
data[].isActive |
boolean | Inactive workflows cannot be run |
data[].isDefault |
boolean | The Topic Workspace's default workflow |
data[].topicKey |
string | Topic Workspace the workflow belongs to |
data[].feedIds |
string[] | undefined | The workflow's own feed list, up to 10 feed ids. Absent when the workflow reads the Topic Workspace's followed feeds |
data[].triggerConfig |
object | { type: manual | scheduled, schedule?: { frequency, days, times, timezone } } |
data[].scheduleState |
object | null | Server managed schedule state: nextRunAt, lastTriggeredAt, lastRunId, lastSkipReason |
data[].createdAt |
string | ISO 8601 creation timestamp |
data[].updatedAt |
string | ISO 8601 timestamp of the last update |
data[].nodes |
Node[] | Nodes in execution order. type is one of evaluate, process, repurpose, quality-check, seo, image-generation, translate, publish, social-distribution |
data[].nodes[].id |
string | Node id |
data[].nodes[].type |
string | Node type |
data[].nodes[].label |
string | Display label |
data[].nodes[].order |
number | Execution order |
data[].nodes[].enabled |
boolean | Disabled nodes are skipped |
meta |
object | Pagination metadata |
meta.page |
number | Zero-based page returned |
meta.limit |
number | Page size used |
meta.hasMore |
boolean | Whether a next page may exist (the page was full) |
{
"data": [
{
"id": "68b9b7c2f1a2b3c4d5e6f7a0",
"name": "Daily AI digest",
"description": "Evaluate new articles, write stories, check quality, translate to German.",
"isActive": true,
"isDefault": true,
"topicKey": "tech",
"triggerConfig": {
"type": "scheduled",
"schedule": {
"frequency": "daily",
"days": [
"mon",
"tue",
"wed",
"thu",
"fri",
"sat",
"sun"
],
"times": [
"06:00"
],
"timezone": "Europe/London"
}
},
"scheduleState": {
"nextRunAt": "2026-09-06T05:00:00.000Z",
"lastTriggeredAt": "2026-09-05T05:00:00.000Z",
"lastRunId": "68b9b7c2f1a2b3c4d5e6f7b0",
"lastSkipReason": null
},
"nodes": [
{
"id": "68b9b7c2f1a2b3c4d5e6f7a1",
"type": "evaluate",
"label": "Evaluate articles",
"order": 0,
"enabled": true
},
{
"id": "68b9b7c2f1a2b3c4d5e6f7a2",
"type": "repurpose",
"label": "Write stories",
"order": 1,
"enabled": true
},
{
"id": "68b9b7c2f1a2b3c4d5e6f7a3",
"type": "quality-check",
"label": "Quality check",
"order": 2,
"enabled": true
},
{
"id": "68b9b7c2f1a2b3c4d5e6f7a4",
"type": "translate",
"label": "Translate",
"order": 3,
"enabled": true
}
],
"createdAt": "2026-08-01T09:00:00.000Z",
"updatedAt": "2026-09-02T18:00:00.000Z"
}
],
"meta": {
"page": 0,
"limit": 20,
"hasMore": false
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs
Example
curl -X GET "https://client-api.news-factory.app/v1/workflows?isActive=true" \
-H "Authorization: Bearer nfk_xxx..."
Get Workflow
GET /v1/workflows/{id} · operationId getWorkflow · scopes: workflows:read
One workflow with its full graph: nodes (with their configuration and canvas position) and the edges connecting them.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The workflow id |
Responses
200The workflow graph
| Field | Type | Description |
|---|---|---|
data |
Workflow | The workflow |
data.id |
string | Unique workflow identifier |
data.name |
string | Workflow name |
data.description |
string | null | Description |
data.isActive |
boolean | Inactive workflows cannot be run |
data.isDefault |
boolean | The Topic Workspace's default workflow |
data.topicKey |
string | Topic Workspace the workflow belongs to |
data.feedIds |
string[] | undefined | The workflow's own feed list, up to 10 feed ids. Absent when the workflow reads the Topic Workspace's followed feeds |
data.triggerConfig |
object | { type: manual | scheduled, schedule?: { frequency, days, times, timezone } } |
data.scheduleState |
object | null | Server managed schedule state: nextRunAt, lastTriggeredAt, lastRunId, lastSkipReason |
data.createdAt |
string | ISO 8601 creation timestamp |
data.updatedAt |
string | ISO 8601 timestamp of the last update |
data.nodes |
Node[] | Nodes in execution order. type is one of evaluate, process, repurpose, quality-check, seo, image-generation, translate, publish, social-distribution |
data.nodes[].id |
string | Node id |
data.nodes[].type |
string | Node type |
data.nodes[].label |
string | Display label |
data.nodes[].order |
number | Execution order |
data.nodes[].enabled |
boolean | Disabled nodes are skipped |
data.nodes[].config |
object | Node configuration (thresholds, languages, providers, prompts) |
data.nodes[].position |
object | Canvas position { x, y } |
data.edges |
Edge[] | Connections: { id, sourceNodeId, targetNodeId } |
{
"data": {
"id": "68b9b7c2f1a2b3c4d5e6f7a0",
"name": "Daily AI digest",
"description": "Evaluate new articles, write stories, check quality, translate to German.",
"isActive": true,
"isDefault": true,
"topicKey": "tech",
"triggerConfig": {
"type": "scheduled",
"schedule": {
"frequency": "daily",
"days": [
"mon",
"tue",
"wed",
"thu",
"fri",
"sat",
"sun"
],
"times": [
"06:00"
],
"timezone": "Europe/London"
}
},
"scheduleState": {
"nextRunAt": "2026-09-06T05:00:00.000Z",
"lastTriggeredAt": "2026-09-05T05:00:00.000Z",
"lastRunId": "68b9b7c2f1a2b3c4d5e6f7b0",
"lastSkipReason": null
},
"nodes": [
{
"id": "68b9b7c2f1a2b3c4d5e6f7a1",
"type": "evaluate",
"label": "Evaluate articles",
"order": 0,
"enabled": true,
"position": {
"x": 0,
"y": 0
},
"config": {
"scoreThreshold": 60,
"processLimit": 25
}
},
{
"id": "68b9b7c2f1a2b3c4d5e6f7a2",
"type": "repurpose",
"label": "Write stories",
"order": 1,
"enabled": true,
"position": {
"x": 320,
"y": 0
},
"config": {
"languageCode": "en-US"
}
},
{
"id": "68b9b7c2f1a2b3c4d5e6f7a3",
"type": "quality-check",
"label": "Quality check",
"order": 2,
"enabled": true,
"position": {
"x": 640,
"y": 0
},
"config": {
"effort": "normal"
}
},
{
"id": "68b9b7c2f1a2b3c4d5e6f7a4",
"type": "translate",
"label": "Translate",
"order": 3,
"enabled": true,
"position": {
"x": 960,
"y": 0
},
"config": {
"targetLanguages": [
"de-DE"
]
}
}
],
"createdAt": "2026-08-01T09:00:00.000Z",
"updatedAt": "2026-09-02T18:00:00.000Z",
"edges": [
{
"id": "68b9b7c2f1a2b3c4d5e6f7c1",
"sourceNodeId": "68b9b7c2f1a2b3c4d5e6f7a1",
"targetNodeId": "68b9b7c2f1a2b3c4d5e6f7a2"
},
{
"id": "68b9b7c2f1a2b3c4d5e6f7c2",
"sourceNodeId": "68b9b7c2f1a2b3c4d5e6f7a2",
"targetNodeId": "68b9b7c2f1a2b3c4d5e6f7a3"
},
{
"id": "68b9b7c2f1a2b3c4d5e6f7c3",
"sourceNodeId": "68b9b7c2f1a2b3c4d5e6f7a3",
"targetNodeId": "68b9b7c2f1a2b3c4d5e6f7a4"
}
]
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Workflow not found
Example
curl -X GET "https://client-api.news-factory.app/v1/workflows/68b9b7c2f1a2b3c4d5e6f7a0" \
-H "Authorization: Bearer nfk_xxx..."
Run Workflow
POST /v1/workflows/{id}/run · operationId runWorkflow · scopes: workflows:run
Start a run. The run reads the workflow's own feeds (or the Topic Workspace's followed feeds when it has no list) and snapshots their unevaluated articles as its input unless you pass explicit ids. Only one run can be active per organization: starting another answers 409 with the active run id. Follow progress with the workflow run endpoints. Runs that write stories cost AI credits.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The workflow id |
Request body (JSON): Optional input
| Name | Type | Required | Description |
|---|---|---|---|
input |
object | no | { feedArticleIds?: string[], storyIds?: string[] } to run on specific items instead of the unevaluated pool |
{}
Responses
202Run started
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data.runId |
string | The new run |
data.workflowId |
string | The workflow |
data.status |
string | queued or running |
message |
string | Success message |
{
"success": true,
"data": {
"runId": "68b9b7c2f1a2b3c4d5e6f7b0",
"workflowId": "68b9b7c2f1a2b3c4d5e6f7a0",
"status": "running"
},
"message": "Workflow run started"
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Workflow not found409Conflict503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X POST "https://client-api.news-factory.app/v1/workflows/68b9b7c2f1a2b3c4d5e6f7a0/run" \
-H "Authorization: Bearer nfk_xxx..." \
-H "Content-Type: application/json" \
-d '{}'
List Workflow Runs
GET /v1/workflows/{id}/runs · operationId listWorkflowRuns · scopes: workflows:read
Runs of one workflow, newest first.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The workflow id |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
status |
string (queued, running, paused, completed, failed, cancelled) |
no | Only runs in this status |
limit |
number | no | Page size (default 20, max 100) |
page |
number | no | Zero-based page number (default 0) |
order |
string (createdAt DESC, startedAt DESC, completedAt DESC) |
no | Sort order as field ASC|DESC (default createdAt DESC). Allowed fields: createdAt, startedAt, completedAt. |
Responses
200A page of runs
| Field | Type | Description |
|---|---|---|
data |
WorkflowRun[] | Runs |
data[].id |
string | Run id |
data[].workflowId |
string | Workflow that ran |
data[].status |
string | queued, running, paused, completed, failed or cancelled |
data[].triggeredBy |
string | manual or scheduled |
data[].topicKey |
string | Topic Workspace the run worked on |
data[].input |
object | Snapshot the run started from: feedArticleIds[], storyIds[] |
data[].output |
object | null | processedCount, filteredCount, failedCount, and on a completed run whose steps lost items issues[] (nodeId, label, failed, passed): the run still produced output, so it is completed, not failed |
data[].error |
string | null | Failure reason |
data[].metadata |
object | totalDuration in seconds and per node overrides |
data[].startedAt |
string | null | ISO 8601 start time |
data[].completedAt |
string | null | ISO 8601 end time |
data[].createdAt |
string | ISO 8601 creation timestamp |
meta |
object | Pagination metadata |
meta.page |
number | Zero-based page returned |
meta.limit |
number | Page size used |
meta.hasMore |
boolean | Whether a next page may exist (the page was full) |
meta.workflowId |
string | The workflow |
{
"data": [
{
"id": "68b9b7c2f1a2b3c4d5e6f7b0",
"workflowId": "68b9b7c2f1a2b3c4d5e6f7a0",
"status": "completed",
"triggeredBy": "scheduled",
"topicKey": "tech",
"input": {
"feedArticleIds": [
"68b9b7c2f1a2b3c4d5e6f780",
"68b9b7c2f1a2b3c4d5e6f781"
]
},
"output": {
"processedCount": 2,
"filteredCount": 1,
"failedCount": 0
},
"error": null,
"metadata": {
"totalDuration": 509
},
"startedAt": "2026-09-05T05:00:02.000Z",
"completedAt": "2026-09-05T05:08:31.000Z",
"createdAt": "2026-09-05T05:00:01.000Z"
}
],
"meta": {
"page": 0,
"limit": 20,
"hasMore": false,
"workflowId": "68b9b7c2f1a2b3c4d5e6f7a0"
}
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs
Example
curl -X GET "https://client-api.news-factory.app/v1/workflows/68b9b7c2f1a2b3c4d5e6f7a0/runs" \
-H "Authorization: Bearer nfk_xxx..."
Get Workflow Run
GET /v1/workflow-runs/{runId} · operationId getWorkflowRun · scopes: workflows:read
One run with its status, input snapshot, output counts and timing. Poll it while a run is active; list its node runs for per-step progress.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
runId |
string | yes | The workflow run id |
Responses
200The run
| Field | Type | Description |
|---|---|---|
data |
WorkflowRun | The run |
data.id |
string | Run id |
data.workflowId |
string | Workflow that ran |
data.status |
string | queued, running, paused, completed, failed or cancelled |
data.triggeredBy |
string | manual or scheduled |
data.topicKey |
string | Topic Workspace the run worked on |
data.input |
object | Snapshot the run started from: feedArticleIds[], storyIds[] |
data.output |
object | null | processedCount, filteredCount, failedCount, and on a completed run whose steps lost items issues[] (nodeId, label, failed, passed): the run still produced output, so it is completed, not failed |
data.error |
string | null | Failure reason |
data.metadata |
object | totalDuration in seconds and per node overrides |
data.startedAt |
string | null | ISO 8601 start time |
data.completedAt |
string | null | ISO 8601 end time |
data.createdAt |
string | ISO 8601 creation timestamp |
{
"data": {
"id": "68b9b7c2f1a2b3c4d5e6f7b0",
"workflowId": "68b9b7c2f1a2b3c4d5e6f7a0",
"status": "completed",
"triggeredBy": "scheduled",
"topicKey": "tech",
"input": {
"feedArticleIds": [
"68b9b7c2f1a2b3c4d5e6f780",
"68b9b7c2f1a2b3c4d5e6f781"
]
},
"output": {
"processedCount": 2,
"filteredCount": 1,
"failedCount": 0
},
"error": null,
"metadata": {
"totalDuration": 509
},
"startedAt": "2026-09-05T05:00:02.000Z",
"completedAt": "2026-09-05T05:08:31.000Z",
"createdAt": "2026-09-05T05:00:01.000Z"
}
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Workflow run not found
Example
curl -X GET "https://client-api.news-factory.app/v1/workflow-runs/68b9b7c2f1a2b3c4d5e6f7b0" \
-H "Authorization: Bearer nfk_xxx..."
List Node Runs
GET /v1/workflow-runs/{runId}/node-runs · operationId listNodeRuns · scopes: workflows:read
Per node progress of a run in execution order, with item counts and the produced items (article and story ids) in metadata.outputItems.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
runId |
string | yes | The workflow run id |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
status |
string (pending, draft, preparing, executing, paused, finished, failed, skipped, cancelled) |
no | Only node runs in this status |
Responses
200Node runs
| Field | Type | Description |
|---|---|---|
data |
NodeRun[] | Node runs |
data[].id |
string | Node run id |
data[].workflowRunId |
string | Parent run |
data[].workflowNodeId |
string | Node definition |
data[].nodeType |
string | evaluate, process, repurpose, quality-check, seo, image-generation, translate, publish, social-distribution |
data[].nodeLabel |
string | Node label at run time |
data[].status |
string | pending, draft, preparing, executing, paused, finished, failed, skipped or cancelled |
data[].progress |
number | 0 to 100 |
data[].itemsIn |
number | Items received |
data[].itemsOut |
number | Items produced |
data[].itemsPassed |
number | Items that passed the node's filter |
data[].itemsFailed |
number | Items that failed |
data[].error |
string | null | Failure reason |
data[].metadata |
object | duration in seconds, llmTokensUsed, and outputItems[] (the produced articles or stories with their ids) |
data[].startedAt |
string | null | ISO 8601 start time |
data[].completedAt |
string | null | ISO 8601 end time |
meta.count |
number | Node runs returned |
meta.runId |
string | The run |
{
"data": [
{
"id": "68b9b7c2f1a2b3c4d5e6f7d0",
"workflowRunId": "68b9b7c2f1a2b3c4d5e6f7b0",
"workflowNodeId": "68b9b7c2f1a2b3c4d5e6f7a2",
"nodeType": "repurpose",
"nodeLabel": "Write stories",
"status": "finished",
"progress": 100,
"itemsIn": 2,
"itemsOut": 2,
"itemsPassed": 2,
"itemsFailed": 0,
"error": null,
"metadata": {
"duration": 301,
"llmTokensUsed": 18402,
"outputItems": [
{
"id": "68b9b7c2f1a2b3c4d5e6f700",
"type": "story",
"data": {
"title": "AI Revolution in Healthcare"
}
}
]
},
"startedAt": "2026-09-05T05:01:00.000Z",
"completedAt": "2026-09-05T05:06:01.000Z"
}
],
"meta": {
"count": 1,
"runId": "68b9b7c2f1a2b3c4d5e6f7b0"
}
}
400Validation error: the request is missing or misusing a field401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Workflow run not found
Example
curl -X GET "https://client-api.news-factory.app/v1/workflow-runs/68b9b7c2f1a2b3c4d5e6f7b0/node-runs" \
-H "Authorization: Bearer nfk_xxx..."
Stop Run
POST /v1/workflow-runs/{runId}/cancel · operationId cancelWorkflowRun · scopes: workflows:run
Stop a queued, running or paused run. The current node finishes its in-flight operation, then the run stops and its status becomes cancelled. Finished runs answer 409.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
runId |
string | yes | The workflow run id |
Responses
200Workflow run cancelled
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data.runId |
string | The run |
data.status |
string | New run status |
message |
string | Success message |
{
"success": true,
"data": {
"runId": "68b9b7c2f1a2b3c4d5e6f7b0",
"status": "cancelled"
},
"message": "Workflow run cancelled"
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Workflow run not found409Conflict503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X POST "https://client-api.news-factory.app/v1/workflow-runs/68b9b7c2f1a2b3c4d5e6f7b0/cancel" \
-H "Authorization: Bearer nfk_xxx..."
Pause Run
POST /v1/workflow-runs/{runId}/pause · operationId pauseWorkflowRun · scopes: workflows:run
Pause a running run before its next node starts. Resume it later or stop it.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
runId |
string | yes | The workflow run id |
Responses
200Workflow run paused
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data.runId |
string | The run |
data.status |
string | New run status |
message |
string | Success message |
{
"success": true,
"data": {
"runId": "68b9b7c2f1a2b3c4d5e6f7b0",
"status": "paused"
},
"message": "Workflow run paused"
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Workflow run not found503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X POST "https://client-api.news-factory.app/v1/workflow-runs/68b9b7c2f1a2b3c4d5e6f7b0/pause" \
-H "Authorization: Bearer nfk_xxx..."
Resume Run
POST /v1/workflow-runs/{runId}/resume · operationId resumeWorkflowRun · scopes: workflows:run
Resume a paused run from the node it paused on.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
runId |
string | yes | The workflow run id |
Responses
200Workflow run resumed
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true |
data.runId |
string | The run |
data.status |
string | New run status |
message |
string | Success message |
data.workflowId |
string | The workflow |
data.resumeNodeId |
string | Node the run continues from |
{
"success": true,
"data": {
"runId": "68b9b7c2f1a2b3c4d5e6f7b0",
"status": "running",
"workflowId": "68b9b7c2f1a2b3c4d5e6f7a0",
"resumeNodeId": "68b9b7c2f1a2b3c4d5e6f7a3"
},
"message": "Workflow run resumed"
}
401Unauthorized: missing, invalid, revoked or expired credentials403The API key lacks a scope this endpoint needs404Workflow run not found409Conflict503API keys cannot be verified or signed right now; retry in a minute
Example
curl -X POST "https://client-api.news-factory.app/v1/workflow-runs/68b9b7c2f1a2b3c4d5e6f7b0/resume" \
-H "Authorization: Bearer nfk_xxx..."