Article lifecycle
What happens between POST and finished content, and how to be told when it's ready.
POST /v1/articles returns 201 as soon as the job is queued — not when the article is written. Generation runs a multi-step pipeline in the background and takes minutes, not seconds.
Statuses
| Status | Meaning |
|---|---|
queued | Created, waiting for a worker |
batching | Submitted to the provider's batch queue — up to a 24-hour turnaround, at half price |
writing | Workflow steps are running |
polishing | Content is done; images and finishing touches are being generated |
completed | Finished and readable |
failed | Generation failed after all retries |
published | Delivered to an integration — WordPress, Ghost, Webflow, Contentful, or a webhook |
completed is the terminal state for generation. published only happens later, through a publishing integration or auto-publish.
While an article is in progress, current_step names the workflow step being executed — useful for a progress display.
Knowing when it's done
Webhook (preferred)
Pass webhook_url when creating the article. Serpon POSTs the finished article to that URL the moment it reaches completed.
{
"project_id": 1,
"title": "How to Choose a Mattress",
"article_type": "ultimate_guide",
"target_word_count": 2000,
"tone": "friendly",
"point_of_view": "second_person",
"language": "en",
"webhook_url": "https://example.com/hooks/serpon"
}
| Property | Value |
|---|---|
| Method | POST, application/json |
| Timeout | 30 seconds |
| Retries | 3 attempts, backing off 10s → 20s → 30s |
| Success | Any 2xx; anything else is retried, then logged as failed on the article |
The payload is the same shape as the publishing webhook — a data block holding the full article record, a content block with HTML and Markdown, and an seo block:
{
"success": true,
"data": { "id": 42, "external_id": "550e8400-…", "status": "completed" },
"content": {
"format": "json",
"html": "<h2>Choosing a mattress</h2>…",
"markdown": "## Choosing a mattress
…",
"meta_title": "How to Choose a Mattress (2026 Guide)",
"meta_description": "A practical guide to picking the right mattress…"
},
"seo": {
"target_keyword": "how to choose a mattress",
"keywords": ["memory foam", "firmness"],
"language": "en",
"country_targeting": "US",
"word_count": 1840,
"ai_detection_score": 12
}
}
Payloads are not signed — there is no HMAC or signature header, so the request alone cannot be verified. Treat the URL as the secret: make it unguessable, serve it over HTTPS, and add your own check such as a token in the query string.
Acknowledge with 2xx first and do slow work afterwards. Use data.external_id as an idempotency key: a retried delivery repeats the same article.
Only completed fires the webhook. A failed article sends nothing, so a job that waits solely on the webhook will wait forever — pair it with a timeout, or with polling.
Polling
Without a webhook, poll GET /v1/articles/{id} until status is completed or failed.
async function waitForArticle(id, { intervalMs = 30_000, timeoutMs = 1_800_000 } = {}) {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
const res = await fetch(`https://serpon.ai/v1/articles/${id}`, {
headers: { Authorization: `Bearer ${process.env.SERPON_TOKEN}`, Accept: 'application/json' },
})
const { data } = await res.json()
if (data.status === 'completed') return data
if (data.status === 'failed') throw new Error(`Article ${id} failed`)
await new Promise((r) => setTimeout(r, intervalMs))
}
throw new Error(`Article ${id} did not finish in time`)
}
Poll every 30–60 seconds. Every request spends the articles rate limit, and generation rarely finishes faster than that.
Content fields
Once complete, three fields carry text, and they are not interchangeable:
| Field | What it is |
|---|---|
content | The live article — the current version, including edits made after generation |
original_content | The frozen generation output, before any edits |
description | SEO meta description, when the workflow produced one |
For most integrations, content is the one to publish. Some workflows return it wrapped in a JSON envelope where the body lives at article.content; the webhook's content.html and content.markdown are always unwrapped, which is another reason to prefer the webhook when delivering to a CMS.
Batch mode
Setting use_batch_api: true submits the article through the provider's batch queue: roughly half the cost, with a turnaround of up to 24 hours. The article sits in batching rather than queued.
Batch mode only applies to single-step workflows on a batch-enabled model. Anything else is rejected at validation:
{
"message": "The given data was invalid.",
"errors": {
"use_batch_api": ["Batch mode is only available for single-step workflows using a Batch-enabled AI model."]
}
}
Use it for bulk backfills where latency does not matter. Use the normal lane for anything a person is waiting on.
Failures
A failed article has exhausted its retries. Common causes are an upstream model error, a workflow step that times out, and research finding no usable sources.
The word quota check at creation is a reservation test, not a debit — the allowance is spent against the words actually generated, so a run that fails early costs little. Failed articles can be regenerated from the dashboard.