REST API Reference
Base URL: https://api.editorinchief.io/api/v1
All endpoints require authentication. See Authentication for token formats.
Sites
A site represents a publication. It holds the editorial voice, approval mode, and publishing target. You must create a site before ingesting content.
List sites
GET /sites
Returns all sites owned by the authenticated user.
Response
{
"sites": [
{
"id": "site_abc123",
"name": "My Dev Blog",
"domain": "blog.example.com",
"status": "active",
"approval_mode": "per_article",
"created_at": 1713456789
}
]
}
Create site
POST /sites
Body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | ✓ | Human-readable publication name |
domain | string | ✓ | Domain without scheme, e.g. blog.example.com |
editorial_voice | object | ✓ | See Editorial voice |
approval_mode | string | per_article (default), digest, or auto | |
telegram_chat_id | string | Telegram chat ID for approval notifications | |
digest_time | string | UTC time for digest approvals, e.g. "09:00" | |
target_frequency_per_week | number | Target publication rate | |
publish_target | string | omnivocal (default) or webhook | |
omnivocal_api_key | string | * | Required when publish_target is omnivocal |
webhook_url | string | * | Required when publish_target is webhook |
webhook_secret | string | HMAC secret for webhook signature verification | |
financial_ack_at | number | * | Unix timestamp — required when editorial_voice.financial_content_regulated is true |
Returns 201 with { "site": { ... } }
Get site
GET /sites/:id
Update site
PATCH /sites/:id
Send only the fields to change. Supports all body fields from POST /sites.
Editorial voice
The editorial_voice object shapes every LLM prompt for the site. Unknown fields are rejected.
| Field | Type | Required | Notes |
|---|---|---|---|
mission | string | ✓ | One-sentence mission statement, max 500 chars |
tone_guardrails | string | Rules appended to prompts, max 500 chars | |
site_type | string | standard (default), satire, or financial | |
image_tone | string | Image style description passed to image generation, max 200 chars | |
financial_content_regulated | boolean | Set true for regulated financial content (requires financial_ack_at at creation) |
Example
{
"mission": "Plain-language coverage of TypeScript tooling for indie developers.",
"tone_guardrails": "Direct, no marketing language. Short paragraphs.",
"site_type": "standard"
}
Beats
A beat defines what content a site covers and how it generates articles. Each site has 1–N beats. Ingested content is routed to the best-matching beat.
List beats
GET /sites/:site_id/beats
Create beat
POST /sites/:site_id/beats
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | ✓ | Slug-style, e.g. "product-updates" |
content_types | string[] | ✓ | One or more of: short_social, status_message, long_form, github_content |
description | string | One-sentence description | |
coverage | string | selective, balanced (default), or broad | |
generation_guidance | string | Extra prompt guidance for this beat | |
synthesis_mode | string | auto (default), review, or off |
Get beat
GET /sites/:site_id/beats/:beat_id
Update beat
PATCH /sites/:site_id/beats/:beat_id
Delete beat
DELETE /sites/:site_id/beats/:beat_id
Seed beats (AI suggestion)
POST /sites/:id/seed-beats
Returns LLM-suggested beats based on the site’s domain and mission. Suggestions are not saved automatically.
Response
{
"suggestions": [
{
"name": "vc-deals",
"description": "Funding rounds and acquisition news.",
"content_types": ["short_social", "long_form"],
"coverage": "selective"
}
]
}
Sources
Sources define where EIC fetches content on a cron schedule.
List sources
GET /sites/:site_id/sources
Create source
POST /sites/:site_id/sources
| Field | Type | Required | Notes |
|---|---|---|---|
type | string | ✓ | rss, github_releases, github_commits, scraper, or api_push |
url | string | * | Required for rss and scraper |
repo | string | * | Required for github_releases / github_commits, format owner/repo |
name | string | Display name | |
poll_interval_minutes | number | Default varies by type |
Get source
GET /sites/:site_id/sources/:id
Update source
PATCH /sites/:site_id/sources/:id
Delete source
DELETE /sites/:site_id/sources/:id
Trigger fetch
POST /sites/:site_id/sources/:id/fetch-now
Immediately polls the source. Returns { "queued": true }.
Articles
List articles
GET /sites/:site_id/articles?stage=&beat_id=&persona_id=&limit=&cursor=
| Query param | Notes |
|---|---|
stage | Filter by stage: queued, generating, draft_ready, pending_approval, approved, submitting, published, rejected, submit_failed |
beat_id | Filter by beat |
persona_id | Filter by persona |
limit | Page size (default 50, max 100) |
cursor | Pagination cursor from previous response |
Get article
GET /sites/:site_id/articles/:id
Preview article
GET /sites/:site_id/articles/:id/preview
Returns the full generated body and per-channel variants.
Response
{
"article": {
"id": "art_xyz",
"generated_title": "TypeScript 5.8 ships with improved inference",
"generated_body": "...",
"stage": "pending_approval",
"content_type": "long_form"
},
"targets": [
{ "channel": "bluesky", "generated_body": "Thread text here…" },
{ "channel": "mastodon", "generated_body": "Toot text here…" }
]
}
Approve article
POST /sites/:site_id/articles/:id/approve
Moves to approved and queues submission to Omnivocal or webhook.
Reject article
POST /sites/:site_id/articles/:id/reject
Optional body: { "reason": "Off topic" }
Regenerate article
POST /sites/:site_id/articles/:id/regenerate
Discards the current draft and reruns generation.
Regenerate image
POST /sites/:site_id/articles/:id/regenerate-image
Reruns image generation only, keeping the text.
Update article
PATCH /sites/:site_id/articles/:id
Edit generated_title, generated_body, or stage directly.
Clusters
EIC groups related items into story clusters using vector similarity. High-significance clusters can be synthesised into a dedicated long-form article.
List clusters
GET /sites/:site_id/clusters?status=
Optional status filter: detected, queued_for_synthesis, synthesized, dismissed.
Get cluster
GET /sites/:site_id/clusters/:id
Synthesize cluster
POST /sites/:site_id/clusters/:id/synthesize
Queues for synthesis. Returns { "status": "queued_for_synthesis" }.
Dismiss cluster
POST /sites/:site_id/clusters/:id/dismiss
Personas
Personas are account-level writer identities. Assign to a site before generating articles.
List personas
GET /personas
Create persona
POST /personas
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | ✓ | |
slug | string | ✓ | URL-safe, unique per account |
byline | string | ✓ | Displayed under published articles |
voice_prompt | string | ✓ | System prompt fragment defining writing style |
image_tone | string | Override image style for this persona |
Get / Update / Delete persona
GET /personas/:id
PATCH /personas/:id
DELETE /personas/:id
Persona articles
GET /personas/:id/articles
Site persona assignments
GET /sites/:site_id/personas
POST /sites/:site_id/personas { "persona_id": "persona_abc" }
PATCH /sites/:site_id/personas/:persona_id
DELETE /sites/:site_id/personas/:persona_id
PATCH supports allowed_beats (array of beat IDs or null for all), off_limits_topics, and override_image_tone.
Inbox
The inbox shows raw content items not yet turned into articles. Low-scored items sit here until manually triggered.
List inbox items
GET /sites/:site_id/inbox
Trigger generation
POST /sites/:site_id/inbox/:item_id/generate
Bypasses the evaluator score threshold.
Ingest
Push content directly without a configured source — useful for CI/CD.
POST /ingest
| Field | Type | Required | Notes |
|---|---|---|---|
site_id | string | ✓ | |
content.url | string | * | Source URL |
content.text | string | * | Raw text body |
content.repo | string | * | GitHub repo owner/repo |
beat_hint | string | Beat name to prefer |
At least one of url, text, or repo is required.
Response
{ "item_id": "ci_abc", "stage": "queued" }
Returns { "status": "duplicate" } if the same content was already ingested.
API keys
List keys
GET /keys
Returns key metadata only — the raw key value is shown once at creation.
Create key
POST /keys
{ "name": "my-ci-key" }
Response
{
"key": "eic_a1b2c3...",
"id": "key_xyz",
"prefix": "eic_a1b2",
"name": "my-ci-key"
}
Store the key immediately — it cannot be retrieved again.
Revoke key
DELETE /keys/:id
Account
GET /me
Returns the authenticated user’s ID, email, and current plan.