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.

  1. 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.
  2. 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"
  1. 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

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.

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

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.

  1. createContentSource (POST /v1/content-sources) with { "title": "<headline>", "link": "<article URL>", "sourceType": "website" }. A 409 CONFLICT means it exists: use details.existingContentSourceId.
  2. scrapeContentSource (POST /v1/content-sources/{id}/scrape) to fetch and parse the page.
  3. repurposeContentSource (POST /v1/content-sources/{id}/repurpose) with { "languageCode": "en-US" }. It answers data.storyId of the new draft.
  4. getStoryById (GET /v1/stories/{id}) to read the result.

Check and improve a story before publishing

Scopes: stories:read, stories:write, ai:generate.

  1. qualityCheck (POST /v1/stories/{storyId}/content/quality-check) and seoAudit (POST /v1/stories/{storyId}/content/seo-audit).
  2. For each finding or fix worth applying: applyQualityFix or applySeoFix with the finding or fix object from the report. On 422 retry with "mode": "rewrite".
  3. updateStory (PATCH /v1/stories/{id}) with { "status": "published" } when the story is ready. News Factory records the moment in publishedAt.

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

  1. listFeeds (GET /v1/feeds) for the feeds the Topic Workspace follows; searchFeedCatalog and subscribeFeed to follow more (feeds:write).
  2. listFeedArticles (GET /v1/feeds/articles?minConfidence=70) for articles already judged relevant to the Topic Workspace, highest confidence first with order=matchConfidence DESC.
  3. Write a story from an article's url with the first recipe.

Run a workflow and wait for it

Scopes: workflows:read, workflows:run.

  1. listWorkflows (GET /v1/workflows) and pick one (isActive=true).
  2. runWorkflow (POST /v1/workflows/{id}/run) answers 202 with data.runId. A 409 means a run is already active: use details.runId.
  3. Poll getWorkflowRun (GET /v1/workflow-runs/{runId}) every 10 to 30 seconds until status is completed, failed or cancelled. listNodeRuns shows per-step progress. pauseWorkflowRun, resumeWorkflowRun and cancelWorkflowRun (Stop) control it.

Good manners

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}.

// 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
  }
]
// 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

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

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"
    }
  }
}

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

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
  }
}

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

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"
}

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

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"
  }
}

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

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
  }
}

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

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"
      }
    ]
  }
}

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

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"
}

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

Field Type Description
success boolean Always true
message string Success message
{
  "success": true,
  "message": "Story deleted successfully"
}

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

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"
      }
    ]
  }
}

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

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
  }
}

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

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"
}

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

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"
}

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

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"
}

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

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"
}

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

Field Type Description
success boolean Always true
message string Success message
{
  "success": true,
  "message": "Thumbnail removed"
}

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

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"
}

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

Field Type Description
success boolean Always true
message string Success message
{
  "success": true,
  "message": "Image removed from story"
}

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

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"
}

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

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"
}

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

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"
}

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

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"
}

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

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
}

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

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"
  }
}

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

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."
}

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

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"
  }
}

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

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>"
}

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

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
}

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

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"
  }
}

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

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"
}

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

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
  }
}

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

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"
}

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

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"
      }
    ]
  }
}

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

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"
}

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

Field Type Description
success boolean Always true
message string Success message
{
  "success": true,
  "message": "Content source deleted successfully"
}

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

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"
}

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

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"
}

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

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
  }
}

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

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"
}

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

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
  }
}

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

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"
  }
}

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

Field Type Description
success boolean Always true
message string Success message
{
  "success": true,
  "message": "Unsubscribed from feed"
}

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

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"
}

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

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
  }
}

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

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"
  }
}

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

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"
  }
}

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

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
  }
}

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

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"
      }
    ]
  }
}

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

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"
}

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

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"
  }
}

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

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"
  }
}

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

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"
  }
}

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

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"
}

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

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"
}

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

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"
}

Example

curl -X POST "https://client-api.news-factory.app/v1/workflow-runs/68b9b7c2f1a2b3c4d5e6f7b0/resume" \
  -H "Authorization: Bearer nfk_xxx..."