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

FieldTypeRequiredNotes
namestringHuman-readable publication name
domainstringDomain without scheme, e.g. blog.example.com
editorial_voiceobjectSee Editorial voice
approval_modestringper_article (default), digest, or auto
telegram_chat_idstringTelegram chat ID for approval notifications
digest_timestringUTC time for digest approvals, e.g. "09:00"
target_frequency_per_weeknumberTarget publication rate
publish_targetstringomnivocal (default) or webhook
omnivocal_api_keystring*Required when publish_target is omnivocal
webhook_urlstring*Required when publish_target is webhook
webhook_secretstringHMAC secret for webhook signature verification
financial_ack_atnumber*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.

FieldTypeRequiredNotes
missionstringOne-sentence mission statement, max 500 chars
tone_guardrailsstringRules appended to prompts, max 500 chars
site_typestringstandard (default), satire, or financial
image_tonestringImage style description passed to image generation, max 200 chars
financial_content_regulatedbooleanSet 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
FieldTypeRequiredNotes
namestringSlug-style, e.g. "product-updates"
content_typesstring[]One or more of: short_social, status_message, long_form, github_content
descriptionstringOne-sentence description
coveragestringselective, balanced (default), or broad
generation_guidancestringExtra prompt guidance for this beat
synthesis_modestringauto (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
FieldTypeRequiredNotes
typestringrss, github_releases, github_commits, scraper, or api_push
urlstring*Required for rss and scraper
repostring*Required for github_releases / github_commits, format owner/repo
namestringDisplay name
poll_interval_minutesnumberDefault 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 paramNotes
stageFilter by stage: queued, generating, draft_ready, pending_approval, approved, submitting, published, rejected, submit_failed
beat_idFilter by beat
persona_idFilter by persona
limitPage size (default 50, max 100)
cursorPagination 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
FieldTypeRequiredNotes
namestring
slugstringURL-safe, unique per account
bylinestringDisplayed under published articles
voice_promptstringSystem prompt fragment defining writing style
image_tonestringOverride 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
FieldTypeRequiredNotes
site_idstring
content.urlstring*Source URL
content.textstring*Raw text body
content.repostring*GitHub repo owner/repo
beat_hintstringBeat 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.