Create one or more headlines for your SEObot website programmatically, with optional custom context per headline. Headlines created via this endpoint flow into the same article-generation pipeline as headlines added manually in the UI.
The endpoint is batch-friendly with partial success: valid headlines are inserted, problematic ones are reported per item. You don't have to fix the whole batch to make progress on the good ones.
Related API
Endpoint
POST https://app.seobotai.com/api/v1/headlines
Content-Type: application/json
Authentication
Pass your website API key as key in the request body. Each website has its own UUID key, auto-generated when the website was onboarded. You can find it on the CMS → Connect (Api) screen.
The key identifies the target website — you don't pass host separately.
Request body
{
"key": "your-website-api-key",
"headlines": [
{
"headline": "Best CRMs for Small Business in 2026",
"context": "Focus on tools under $50/user/month. Mention HubSpot, Pipedrive, Zoho.",
"slug": "best-crms-for-small-business-2026"
},
{
"headline": "How to Run a Lead Scoring Campaign"
}
]
}
Fields
Field | Type | Required | Description |
| string | yes | Per-website API key (UUID). |
| array | yes | 1 to 100 items per request. |
| string | yes | 1 to 100 characters, trimmed. The article title. |
| string | no | 0 to 5000 characters. Custom brief / instructions that guides article generation (audience, tone, must-mention products, sources, etc.). |
| string | no | URL slug. If omitted, a slug is derived deterministically from |
Limits
100 headlines per request
100 characters per headline
5000 characters per context
100 characters per slug
5000 headlines total per website (counted against non-deleted headlines)
Slug behavior
If you pass
slug, it's used as-is and never changed afterwards.If you omit
slug, we generate one from the headline immediately and return it. We may improve it shortly after (e.g., remove filler words, transliterate non-English) - but only before article generation starts. Once article generation begins, the slug is final and won't change.If you need a guaranteed stable URL from the moment of insert, pass
slugexplicitly.
Response
There are two response shapes, distinguished by the top-level success flag.
A. Request couldn't be processed at all (success: false)
Returned for auth/shape errors that prevent the request from being evaluated. No items are processed.
{
"success": false,
"code": "INVALID_KEY",
"error": "Wrong API key"
}
| Meaning | Extra fields | How to handle |
|
| — | Add |
|
| — | Check the API key on the Connect screen. Don't retry blindly. |
|
| — | Include at least one headline. |
| More than 100 items in one request. |
| Split your input into chunks of ≤ 100 and call sequentially. |
| Key resolved a host but the website record is missing (rare; stale key). | — | Contact support. |
| Unexpected server-side failure (e.g., transient downstream service error). | — | Safe to retry with backoff. If persistent, contact support — the call is logged on our side. |
B. Request was processed (success: true)
Returned whenever the request was valid enough to evaluate the items — even if zero were actually inserted (all duplicates / all invalid). Each input headline gets exactly one entry in results, in input order. summary gives quick aggregate counts.
{
"success": true,
"summary": { "total": 5, "inserted": 2, "skipped": 3 },
"results": [
{ "index": 0, "headline": "Best CRMs for Small Business in 2026", "status": "inserted", "id": "65f1c2a4e7b9d1a3f4e2c5b8" },
{ "index": 1, "headline": "best crms for small business in 2026", "status": "skipped", "code": "DUPLICATE_IN_BATCH", "error": "Earlier item in this request has the same headline", "conflictsWith": { "index": 0, "headline": "Best CRMs for Small Business in 2026" } },
{ "index": 2, "headline": "Best CRMs for Small Business in 2024", "status": "skipped", "code": "DUPLICATE_YEAR_VARIANT", "error": "A headline that only differs by year already exists", "conflictsWith": { "headline": "Best CRMs for Small Business in 2026" } },
{ "index": 3, "headline": "Lead Scoring Best Practices", "status": "inserted", "id": "65f1c2a4e7b9d1a3f4e2c5b9" },
{ "index": 4, "headline": "", "status": "skipped", "code": "EMPTY_HEADLINE", "error": "Headline is empty" }
]
}After insertion, headlines flow into the standard SEObot article-generation pipeline automatically. The custom context you provided is used during generation, so the resulting article reflects your instructions.
summary
Field | Type | Description |
| number | Same as |
| number | How many were actually written. |
| number | How many were rejected (duplicates, invalid, over limit). |
results[] — every entry
Field | Type | Description |
| number | Zero-based position in the request's |
| string | The normalized headline (trimmed, quotes stripped, whitespace collapsed). For |
|
| Per-item outcome. |
results[] — when status: "inserted"
Field | Type | Description |
| string | Unique identifier of the new headline. Use it as a stable reference to the article being generated. |
results[] — when status: "skipped"
Field | Type | Description |
| string | One of the skip codes below. Branch on this, not on |
| string | Human-readable explanation. May change; don't parse it. |
| object | Present for |
Skip codes
| Meaning |
| The headline was empty after trim / normalization. |
| Headline exceeded 100 characters after trim. The |
| Context exceeded 5000 characters after trim. The |
| Caller-provided |
| Another earlier item in the same request had the same headline or slug. See |
| A headline with the same URL slug already exists for this website (strictest collision). |
| A headline with the same text (case-insensitive) already exists. |
| A headline that differs only by year (e.g. "2024" vs "2026") already exists. |
| The website has hit the 5000-headline cap; this item didn't fit. Items that fit before the cap was reached are still inserted. |
| Database write failed at the very end. Rare. Safe to retry — failed items are not in the database. |
Caller pattern
const res = await fetch('https://app.seobotai.com/api/v1/headlines', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key, headlines }),
}).then(r => r.json());
if (!res.success) {
// Couldn't process at all — branch on top-level code
switch (res.code) {
case 'INVALID_KEY':
throw new Error('Bad API key — check Connect screen');
case 'TOO_MANY_HEADLINES':
/* chunk and retry */
break;
default:
throw new Error(`${res.code}: ${res.error}`);
}
return;
}
// Processed — examine per-item results
console.log(`Inserted ${res.summary.inserted} of ${res.summary.total}`);
for (const r of res.results) {
if (r.status === 'inserted') {
console.log(`✓ [${r.index}] ${r.headline} → ${r.id}`);
} else {
console.warn(`✗ [${r.index}] ${r.headline} — ${r.code}: ${r.error}`);
}
}
// Want to retry only the recoverable ones?
const retryable = res.results
.filter(r => r.status === 'skipped' && r.code === 'INSERT_FAILED')
.map(r => headlines[r.index]);
Behavior notes
Partial success. Inserted items are committed even if other items in the same batch are skipped. There's no rollback across items.
Idempotency. Re-sending the same headline returns a
DUPLICATE_*skip for that item; previously inserted items are unaffected. Safe to retry.Ordering.
resultsis sorted byindexin input order regardless of internal processing order.Whitespace and quotes. Each headline is trimmed; surrounding
"..."or'...'are stripped; internal whitespace is collapsed. The stored value (and theheadlinefield inresults) is the normalized form.No
hostin the request. It's derived from the API key.Quota. API-created headlines count against your monthly article quota exactly like headlines added in the UI.
Examples
Minimal - one headline, no context
curl -X POST https://app.seobotai.com/api/v1/headlines \
-H "Content-Type: application/json" \
-d '{ "key": "YOUR_API_KEY", "headlines": [ { "headline": "How to Train an LLM on Your Docs" } ] }'
Bulk - mixed valid/invalid
curl -X POST https://app.seobotai.com/api/v1/headlines \
-H "Content-Type: application/json" \
-d '{
"key": "YOUR_API_KEY",
"headlines": [
{ "headline": "Best CRMs for Small Business in 2026", "context": "Focus on under-$50/user/month tools." },
{ "headline": "Lead Scoring Best Practices", "context": "B2B SaaS angle." },
{ "headline": "Cold Email Subject Lines That Convert" }
]
}'
Response (illustrative):
{
"success": true,
"summary": { "total": 3, "inserted": 2, "skipped": 1 },
"results": [
{ "index": 0, "headline": "Best CRMs for Small Business in 2026", "status": "inserted", "id": "65f1c2..." },
{ "index": 1, "headline": "Lead Scoring Best Practices", "status": "skipped", "code": "DUPLICATE_TEXT", "error": "A headline with the same text (case-insensitive) already exists", "conflictsWith": { "headline": "Lead Scoring Best Practices" } },
{ "index": 2, "headline": "Cold Email Subject Lines That Convert", "status": "inserted", "id": "65f1c3..." }
]
}