Articles.
Creating an article enqueues an asynchronous job; reading one returns the complete generated package — body in two formats, citations, SEO metadata, structured data, quality scores, and derivatives.
Create an article#
/v1/articlesValidates the request, verifies the brand profile belongs to your org, debits credits, and returns 202 Accepted with the job. Generation runs the pipeline described in Jobs.
Request body#
| Field | Type | Description | |
|---|---|---|---|
| topic | string | required | What the article is about. 4–500 characters. Be specific — the planner builds the angle from this. |
| target_keyword | string | optional | Primary SEO keyword (≤200 chars). Falls back to the topic when omitted. |
| brand_profile_id | string | optional | Grounds the article in that profile's knowledge base, fact sheet, and voice profile. Returns 404 not_foundif the ID doesn't belong to your org. |
| mode | "standard" | "deep_research" | default: standard | deep_research runs more research questions on a stronger model stack. 10 vs 25 credits — see the cost table below. |
| language | string | default: "en" | Output language code (≤12 chars), e.g. ar, de. Non-English is culturally adapted, not literally translated, and adds 5 credits. |
| target_words | integer | optional | Accepted range 400–4000; the pipeline aims for 600–3000 and defaults to 1600. |
| tone | string | optional | Freeform tone instruction (≤300 chars). A brand profile voice usually makes this unnecessary. |
| audience | string | optional | Who this is for (≤300 chars). Sharpens the outline and research questions. |
| extra_instructions | string | optional | Anything else the planner should honor (≤2000 chars): sections to include, positions to take, products to mention. |
| image | boolean | default: false | Generate an editorial hero image (+3 credits). URL lands in hero_image_url. |
| derivatives | ("x_thread" | "linkedin" | "newsletter")[] | default: [] | Platform-native repurposing, generated after the article. Any non-empty selection is a flat +3 credits. |
Credit cost#
| Field | Type | Description |
|---|---|---|
| mode: standard | 10 credits | Base price for a standard article. |
| mode: deep_research | 25 credits | Base price for deep research mode (replaces the 10, not added to it). |
| image: true | +3 credits | Hero image generation. |
| derivatives: […] | +3 credits | Flat bundle price whether you request one derivative or all three. |
| language ≠ "en" | +5 credits | Culturally-adapted non-English output. |
The example below is deep_research + image + derivatives = 25 + 3 + 3 = 31 credits, confirmed by credits_charged in the response. Credits are debited when the job is created and automatically refunded if it fails. With insufficient balance the call fails up front with 402 insufficient_credits and nothing is charged.
Idempotency#
Pass an Idempotency-Key header to make creation safe to retry. If a job with the same key already exists in your org, the API returns that original job with 200 (instead of 202) and "replayed": true — no new job, no second charge.
curl -X POST https://scribe.whizztech.ai/v1/articles \
-H "Authorization: Bearer $SCRIBE_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: post-2026-07-06-01" \
-d '{
"topic": "Contextual retrieval for production RAG systems",
"target_keyword": "contextual retrieval",
"brand_profile_id": "bfa6d2a7-4c1e-4f7a-9a2b-91d3f0a6c8e2",
"mode": "deep_research",
"language": "en",
"target_words": 2200,
"tone": "technical, direct, first-person plural sparingly",
"audience": "ML engineers running RAG in production",
"image": true,
"derivatives": ["x_thread", "linkedin"]
}'{
"id": "4d1a8f36-9e02-47b8-b7c4-3a92d5e60f18",
"object": "job",
"type": "article",
"status": "queued",
"credits_charged": 31,
"replayed": false,
"links": { "self": "/v1/jobs/4d1a8f36-9e02-47b8-b7c4-3a92d5e60f18" }
}| Field | Type | Description |
|---|---|---|
| id | string | Job ID. Poll it at /v1/jobs/{id}. |
| object | "job" | Always job for this endpoint. |
| type | "article" | Job type. |
| status | string | queued on a fresh job; a replayed job returns its current status. |
| credits_charged | integer | Total credits debited for this job. |
| replayed | boolean | true when an Idempotency-Key matched an existing job. |
| links.self | string | Relative URL of the job resource. |
List articles#
/v1/articles| Query param | Type | Description | |
|---|---|---|---|
| limit | integer | default: 20 | Number of most-recent articles to return, capped at 50. |
{
"object": "list",
"data": [
{
"id": "9e72c4b0-51af-4c3d-8e6a-2d94b7f01c55",
"title": "SaaS Churn Drops When You Instrument These 5 Moments",
"status": "in_review",
"mode": "standard",
"language": "en",
"word_count": 1584,
"quality_score": 86,
"created_at": "2026-07-06T09:45:02.000Z"
}
]
}List items are summaries — fetch the full object by ID for content and metadata. Article status is one of draft, in_review, approved, published; the pipeline finishes articles in in_review, sign-off moves them to approved, and publishing marks them published.
Retrieve an article#
/v1/articles/{id}| Query param | Type | Description | |
|---|---|---|---|
| format | "markdown" | "html" | optional | markdown omits content_html; html omits content_markdown. Omit the param to get both. |
The article object#
| Field | Type | Description |
|---|---|---|
| id | string | Article ID (UUID). |
| object | "article" | Resource type. |
| status | string | draft | in_review | approved | published. |
| mode | string | standard | deep_research. |
| language | string | Output language code. |
| topic | string | The topic you submitted. |
| target_keyword | string | null | The keyword you submitted. |
| title | string | Final SEO-selected title (≤65 chars). |
| slug | string | null | kebab-case URL slug. |
| meta_title | string | null | SERP title tag, ≤60 chars. |
| meta_description | string | null | SERP description, 140–155 chars, includes the keyword. |
| content_markdown | string | Full article in Markdown, H1 first line, inline citation links. |
| content_html | string | The same content rendered to HTML. |
| citations | {claim, url, title}[] | Every external source linked in the article, mapped to the claim it supports. |
| title_variants | string[] | 3 alternative titles with different psychological hooks, for A/B testing. |
| faq | {q, a}[] | 3–4 question/answer pairs actually answered in the article (FAQPage schema source). |
| json_ld | object | schema.org @graph with BlogPosting and, when FAQ exists, FAQPage. Paste into a <script type="application/ld+json"> tag. |
| word_count | integer | Words in the final edit. |
| read_minutes | number | Estimated reading time. |
| flesch_score | number | null | Flesch Reading Ease. Computed for English only; null otherwise. |
| quality_score | integer | null | 0–100 LLM rubric. Strict scale: 80+ means publishable without edits. |
| quality_breakdown | object | {overall, clarity, hook, specificity, information_gain, notes}. |
| hero_image_url | string | null | Hero image URL when image: true was requested and generation succeeded. |
| derivatives | {type, content}[] | Requested repurposed formats — shapes below. |
| created_at | string | ISO 8601 timestamp. |
Derivative content shapes#
Each derivative's content is format-specific:
"derivatives": [
{ "type": "x_thread", "content": ["Tweet 1 — the hook…", "Tweet 2…", "…"] },
{ "type": "linkedin", "content": { "text": "Hook line…\n\nBody…" } },
{
"type": "newsletter",
"content": {
"subject_lines": ["Option A", "Option B", "Option C"],
"preview": "Preview text under 90 chars",
"body_markdown": "Personal-tone email body…"
}
}
]Update an article#
/v1/articles/{id}Edit the copy or move the article through its review lifecycle. Send at least one field; anything you omit is left untouched. Updating is free — no credits are charged, and the call counts only against your rate limit.
| Field | Type | Description | |
|---|---|---|---|
| title | string | optional | Replacement headline, ≤300 characters. |
| content_markdown | string | optional | Replacement body. content_html is re-rendered from it and word_count, read_minutes, and flesch_score are recomputed. |
| meta_title | string | optional | SERP title tag. |
| meta_description | string | optional | SERP description. |
| status | string | optional | One of draft, in_review, approved, published. |
curl -X PATCH https://scribe.whizztech.ai/v1/articles/9e72c4b0-51af-4c3d-8e6a-2d94b7f01c55 \
-H "Authorization: Bearer $SCRIBE_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "approved" }'The response is the full article object, exactly as GET /v1/articles/{id} returns it.