A URL in. A published audio article out.
Send a page URL or your own title and text; Everlit extracts, narrates in your publication’s voice with byline, disclaimer and music, and publishes to a public player you can link or embed. The full Studio pipeline, from code or from an AI agent over MCP.
Quickstart
# 0. Which publication? (one-time — the id is stable)
curl https://api.everlit.audio/v1/publications \
-H "Authorization: Bearer api_YOUR_KEY"
# => { "object": "list", "data": [ { "id": "pblc_abc123", "name": "The Daily", ... } ] }
# 1. Create an article from a URL
curl -X POST https://api.everlit.audio/v1/articles \
-H "Authorization: Bearer api_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"publication":"pblc_abc123","url":"https://example.com/news/city-budget"}'
# => 202
# {
# "object": "article", "id": "art_01jx...", "status": "queued",
# "url": "https://example.com/news/city-budget", "title": "City approves budget", ...
# }
# 2. Poll until status is "succeeded" (typically 1-5 minutes)
curl https://api.everlit.audio/v1/articles/art_01jx... \
-H "Authorization: Bearer api_YOUR_KEY"
# => { ..., "status": "succeeded", "article_id": "artl_9f3k...",
# "player_url": "https://everlit.audio/embeds/artl_9f3k...",
# "embed_html": "<iframe src=\"https://everlit.audio/embeds/artl_9f3k...\" ...></iframe>",
# "duration_seconds": 254.3 }
Or send the content yourself — no fetch, deterministic input:
curl -X POST https://api.everlit.audio/v1/articles \
-H "Authorization: Bearer api_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"publication": "pblc_abc123",
"url": "https://example.com/news/city-budget",
"title": "City approves budget",
"text": "The city council voted 7-2 on Tuesday to approve...",
"authors": ["Jane Doe"],
"published_at": "2026-09-01T10:30:00Z"
}'
Client examples
Python
import time, requests
API = "https://api.everlit.audio/v1"
H = {"Authorization": "Bearer api_YOUR_KEY"}
pub = requests.get(f"{API}/publications", headers=H).json()["data"][0]["id"]
job = requests.post(f"{API}/articles", headers=H,
json={"publication": pub, "url": "https://example.com/news/city-budget"}).json()
while job["status"] in ("queued", "processing"):
time.sleep(15)
job = requests.get(f"{API}/articles/{job['id']}", headers=H).json()
if job["status"] == "succeeded":
print(job["player_url"])
print(job["embed_html"])
else:
print("failed:", job["error"])
Node
const API = "https://api.everlit.audio/v1";
const H = { Authorization: "Bearer api_YOUR_KEY", "Content-Type": "application/json" };
let job = await (await fetch(`${API}/articles`, {
method: "POST", headers: H,
body: JSON.stringify({ publication: "pblc_abc123", url: "https://example.com/news/city-budget" }),
})).json();
while (["queued", "processing"].includes(job.status)) {
await new Promise(r => setTimeout(r, 15000));
job = await (await fetch(`${API}/articles/${job.id}`, { headers: H })).json();
}
console.log(job.status === "succeeded" ? job.player_url : job.error);
Authentication
Every request needs an api_ key as a bearer token:
Authorization: Bearer api_...
Keys are minted in Studio under API → Keys and are scoped to your publisher account. They are server-side credentials — never embed one in a browser or mobile app (for reader-facing pages, use the Auto Audio widget instead, which needs no key).
Any paid Studio plan can mint an api_ key. A key may additionally be
restricted to specific publications; those keys only see and publish into
their publications. Unlike the TTS API, the Articles API needs no
pay-as-you-go card: articles count against your plan’s monthly article
quota, exactly as widget- and Studio-created articles do. (The same key
calls the TTS API only once pay-as-you-go billing is set up; until then TTS
requests answer 403 tts_api_not_enabled.)
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/articles |
Create an article → 202 (or 200 when it already exists) |
GET |
/v1/articles/{id} |
Fetch an article job (poll this) |
GET |
/v1/articles |
List article jobs, newest first (limit, starting_after, status, publication, q) |
DELETE |
/v1/articles/{id} |
Delete the article, or cancel a running conversion → 204 |
GET |
/v1/publications |
The publications this key can publish into |
GET |
/v1/voices |
Voices available to your account (shared with the TTS API) |
POST |
/v1/mcp |
MCP server for AI agents — see MCP |
Create article
POST /v1/articles
Send url (we fetch and extract the page) or title + text (we use
your content verbatim and never fetch). When both are sent, url is only the
article’s canonical identifier: it dedupes repeat requests and is what the
player links back to.
| Parameter | Type | Required | Description |
|---|---|---|---|
publication |
string | yes | Publication id (pblc_...) from GET /v1/publications. Supplies the voice, music, disclaimer and player defaults. |
url |
string | * | Public http(s) URL of the article. |
title |
string | * | Headline. With text, skips fetching. |
text |
string | * | Article body, plain text, up to 200,000 characters. Markup is stripped. |
authors |
string[] | no | Author names; read aloud as the byline and added as author: tags. |
summary |
string | no | Description shown on the player. |
category |
string | no | Section name; becomes a category: tag. |
tags |
string[] | no | Extra tags (e.g. tickers). |
art_url |
string | no | Cover image URL. Extracted from the page when omitted. |
published_at |
string | no | ISO 8601. Defaults to the page’s date, else now. A future time schedules the article: it stays unlisted until then. |
custom_byline |
string | no | Exact byline text to read instead of the generated “A and B”. |
voice |
string | no | Narrator voice id (GET /v1/voices). Default: the publication’s voice for the detected language. |
guest_voice |
string | no | Second voice for conversation mode. |
conversation_mode |
boolean | no | Two-voice conversational narration. |
mix |
boolean | no | Apply the publication’s intro/outro music beds. Default true. |
intro_mixable / outro_mixable |
string | no | Mixable ids overriding the publication’s beds. |
read_urls / read_alt_text |
boolean | no | Read URLs / image alt text aloud. Default: publication setting. |
read_author |
boolean | no | Read the byline aloud. Default: publication setting (usually true). |
sonic_optimizer |
boolean | no | Audio Polish pass. Default: publication setting. |
disclaimer / disclaimer_voice |
string | no | Override the publication’s AI disclaimer text / voice. |
regenerate |
boolean | no | Re-narrate even if an article for this url already exists (see Existing articles). |
callback_url |
string | no | https:// URL to POST the finished article to. |
metadata |
object | no | Up to 8 string values (512 bytes total), echoed back on the job. |
Unknown parameters are rejected with 400 unknown_parameter — the API is
strict so that future additions are never breaking.
When url is given without text, the page is fetched during the request
(a few seconds). A page we cannot fetch or that has no article body returns
422 content_unavailable / 422 no_content — send title and text instead.
The article resource
Returned by every endpoint and delivered to your callback — one shape:
{
"object": "article",
"id": "art_01jx...",
"status": "succeeded",
"created_at": "2026-09-01T14:00:00Z",
"completed_at": "2026-09-01T14:03:12Z",
"publication": "pblc_abc123",
"url": "https://example.com/news/city-budget",
"title": "City approves budget",
"language": "en",
"voice": "109",
"characters": 4812,
"article_id": "artl_9f3k...",
"player_url": "https://everlit.audio/embeds/artl_9f3k...",
"embed_html": "<iframe title=\"Everlit Audio Player\" src=\"https://everlit.audio/embeds/artl_9f3k...\" width=\"100%\" height=\"136px\" ...></iframe>",
"duration_seconds": 254.3,
"regenerate": false,
"deleted_at": null,
"callback_url": null,
"error": null,
"metadata": { "ref": "cms-4512" }
}
id(art_...) is this conversion job.article_id(artl_...) is the published Everlit article — the durable thing your readers listen to, present oncestatusissucceeded.player_urlis the public player page;embed_htmlis the same player as an iframe for your site. Both stay valid for the life of the article.statusis one ofqueued,processing,succeeded,failed,deleted. Poll every 15–30 seconds; a typical article completes in 1–5 minutes (longer for very long pieces).error.codeexplains a failure.
Existing articles
url is the identity of an article within a publication. If a completed
article already exists for it — created by the API, the widget on your
site, RSS ingestion, or Studio — POST /v1/articles returns it immediately
with 200, status: "succeeded", and its player_url, without narrating
again or spending quota. If a conversion for that URL is already running,
you get that running job back (200, status: "queued" or "processing").
Pass regenerate: true to re-narrate: the article keeps its article_id
and player URL, and the new audio replaces the old when it finishes. Use it
when the page’s text changed or the voice settings did. Text-only requests
(no url) always create a new article.
Deleting articles
DELETE /v1/articles/{id} answers 204 and does one of two things:
- A finished job: the published article is taken down —
player_urlandembed_htmlstop working, the article disappears from Studio and from any playlists it was in. Other jobs that returned the samearticle_id(see Existing articles) are marked deleted with it. - A running job (
queuedorprocessing): the conversion is cancelled and no article is published. Cancelling aregenerate: truejob keeps the existing article and its old audio.
The job record survives with status: "deleted" and a deleted_at
timestamp, so GET /v1/articles/{id} and the list still show it (filter
with status=deleted). Deleting is idempotent and never refunds the
month’s article quota. Deleting an article does not stop the Auto Audio
widget from narrating that page again the next time readers open it — remove
the widget from the page, or delete the article from Studio and set the
page to skip, if that is what you want.
Publications
GET /v1/publications
{
"object": "list",
"data": [
{
"object": "publication",
"id": "pblc_abc123",
"name": "The Daily",
"url": "https://example.com",
"default_voice": "109",
"language_voices": ["english", "spanish"]
}
],
"has_more": false
}
A publication carries everything an article inherits: default narrator per language, guest voice, intro/outro beds, disclaimer text and voice, reading toggles, and the player’s look. Configure them in Studio under Publications → Auto Audio; the API only overrides what you pass.
Quotas and rate limits
- Articles per month follow your plan, shared with the widget and Studio.
Over the cap,
POST /v1/articlesanswers403 article_limit_reached. Requests that converge onto an existing article never spend quota. - Requests: 60 creates and 300 reads per minute per key, with
RateLimit-Limit,RateLimit-Remaining, andRateLimit-Resetheaders on every response andRetry-Afteron a429. - Capacity: when the conversion queue is saturated you get
503 capacity_unavailablewithRetry-After— our capacity, not your behaviour; retry after the delay. - Per URL: one conversion at a time. A second request while one is in
flight returns the running job; a concurrent widget-triggered conversion
answers
409 conversion_in_progress.
Idempotency
Pass an Idempotency-Key header (any unique string, e.g. a UUID) on
POST /v1/articles to make retries safe: resubmitting the same key with the
same body returns the original job (200 + Idempotency-Replayed: true)
without creating another. The same key with a different body is rejected
with 409 idempotency_key_reuse. Keys are scoped to your account. For
URL-based requests the URL itself already dedupes (see
Existing articles); the key matters most for text-only
input.
Webhooks
Pass callback_url and we POST the full article resource to it when the
job reaches a terminal state, with header X-Everlit-Event:
article.succeeded or article.failed. Deliveries retry with backoff for
~30 minutes on non-2xx responses. Only https:// URLs are accepted.
Deliveries are signed exactly like TTS API webhooks — X-Everlit-Signature:
t=<unix>,v1=<hmac> over "<t>.<body>" with your webhook secret from
Studio → API → Usage. See the TTS API webhook section
for verification code; one verifier handles both event families.
MCP
The Articles API is part of Everlit’s MCP server, so Claude, Cursor, Claude Code, and any other MCP-capable agent can publish audio articles directly. The server also hosts the TTS API tools; one connection, one key.
| Tool | Backs |
|---|---|
list_publications |
GET /v1/publications |
create_article |
POST /v1/articles (accepts an idempotency_key argument) |
get_article |
GET /v1/articles/{id} |
list_articles |
GET /v1/articles |
list_voices |
GET /v1/voices |
Endpoint: https://api.everlit.audio/v1/mcp — Streamable HTTP transport,
stateless (no session ids), JSON responses. Authenticate every request with
the same Authorization: Bearer api_... header; there is no OAuth flow.
{
"mcpServers": {
"everlit": {
"type": "http",
"url": "https://api.everlit.audio/v1/mcp",
"headers": { "Authorization": "Bearer api_YOUR_KEY" }
}
}
}
claude mcp add --transport http everlit https://api.everlit.audio/v1/mcp \
--header "Authorization: Bearer api_YOUR_KEY"
A typical agent session: list_publications → create_article with the
page URL the user gave → get_article until succeeded → hand back
player_url. There is no blocking “wait” tool: while a job is queued or
processing, create_article and get_article results carry
poll_after_seconds (30), and the server instructions tell agents to wait
that long between calls. When an article for the URL already exists the tool
returns it with converged: true and no conversion runs.
Tool calls are admitted and limited exactly like the REST endpoints — they
run the same code. A failed call returns an MCP tool error (isError: true)
whose structuredContent.error is the same envelope documented under
Errors, plus http_status and, on 429/503, retry_after
seconds.
Tools
Every tool takes a JSON object of arguments and returns the same JSON the
matching REST endpoint would, as structuredContent (and pretty-printed as
text). create_article and get_article add poll_after_seconds while the
job is still running.
create_article
Publish a full Everlit audio article from a web page (url) or from text (title + text): narration in the publication’s configured voice, intro/outro music, byline, disclaimer, and a public player. Returns the article job (status “queued”); poll get_article (honoring poll_after_seconds) until status is “succeeded”, then use player_url / embed_html. If an article for the url already exists it is returned immediately with status “succeeded” — pass regenerate: true only to re-narrate. Counts against the monthly plan article quota.
| Argument | Type | Required | Description |
|---|---|---|---|
publication |
string | yes | The publication id (pblc_…) from list_publications. |
url |
string | no | Public http(s) URL of the article to narrate. We fetch and extract it. Required unless title and text are given. |
title |
string | no | Article headline (with text, skips fetching the url). |
text |
string | no | Article body to narrate, plain text (up to 200,000 characters). |
authors |
array | no | Author names, read aloud as the byline. |
summary |
string | no | Short description shown on the player. |
category |
string | no | Section/category name. |
tags |
array | no | Extra tags. |
art_url |
string | no | Cover image URL. |
published_at |
string | no | ISO 8601 publication time; defaults to now. |
voice |
string or integer | no | Narrator voice id from list_voices. Defaults to the publication’s voice for the detected language. |
conversation_mode |
boolean | no | Two-voice conversational narration. |
mix |
boolean | no | Apply the publication’s intro/outro music beds (default true). |
regenerate |
boolean | no | Re-narrate even if an article for this url already exists. |
callback_url |
string | no | Optional https:// URL to POST the finished article to. |
metadata |
object | no | Up to 8 string values (512 bytes total), echoed back on the article. |
idempotency_key |
string | no | Any unique string; resubmitting the same key and arguments returns the original article job. |
get_article
Fetch an article job by id. status is one of queued, processing, succeeded, failed, deleted. While queued/processing the result includes poll_after_seconds: wait that long before checking again. When succeeded, player_url is the public player page and embed_html an iframe for a website.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The article job id (art_…). |
list_articles
List this account’s article jobs, newest first. Paginate with starting_after = the last id of the previous page while has_more is true.
| Argument | Type | Required | Description |
|---|---|---|---|
limit |
integer | no | Page size (default 20). |
starting_after |
string | no | Return jobs created before this job id. |
status |
enum: queued, processing, succeeded, failed, deleted | no | Filter by status. |
publication |
string | no | Filter by publication id. |
q |
string | no | Free-text match on id, url, title, article id, error code, or metadata. |
list_publications
Publications this key can publish articles into, with each one’s site URL and default voice. Use the id as create_article’s publication.
No arguments.
delete_article
Delete an article: cancels it if still queued/processing, otherwise takes the published player and embed down. The job record stays with status “deleted”. Quota already used is not refunded.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The article job id (art_…). |
Errors
Errors use one envelope, with a stable machine-readable code — branch on
code, never on message text:
{
"error": {
"type": "invalid_request_error",
"code": "content_unavailable",
"message": "The article could not be fetched from that URL. Check that it is public, or send title and text instead.",
"param": "url",
"request_id": "ab12cd34"
}
}
Include request_id in support tickets.
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_json |
Body is not a JSON object |
| 400 | missing_parameter |
publication absent, or neither url nor title+text given |
| 400 | unknown_parameter |
Unrecognized parameter (check spelling) |
| 400 | invalid_parameter |
Wrong type or value (arrays, booleans, published_at, voice/mixable access) |
| 400 | invalid_url |
url / art_url is not an http(s) URL |
| 400 | invalid_encoding / text_too_long |
text is not UTF-8 / over 200,000 characters |
| 400 | invalid_callback_url |
Must be https with a hostname |
| 401 | missing_api_key / invalid_api_key / api_key_revoked / api_key_expired |
Credential problems |
| 403 | wrong_key_type |
Use an api_ key (not pk_/tkn_) |
| 403 | article_limit_reached |
Monthly plan quota used up |
| 403 | voice_not_accessible / mixable_not_accessible |
Belongs to another account |
| 404 | publication_not_found |
Not on your account (or outside this key’s publication scope) |
| 404 | article_not_found / voice_not_found / mixable_not_found / unknown_endpoint |
Nothing at that address |
| 409 | conversion_in_progress |
The URL is being converted by another request; retry after Retry-After |
| 409 | idempotency_key_reuse |
Same key, different body |
| 422 | content_unavailable |
The page could not be fetched |
| 422 | no_content / no_speakable_text |
Nothing narratable on the page / in text |
| 429 | rate_limit_exceeded / read_rate_limit_exceeded |
Slow down (see Retry-After) |
| 500 | internal_error |
Our fault |
| 503 | capacity_unavailable |
Temporarily saturated; retry |
A job that fails after being accepted reports it on the resource instead:
status: "failed" with error.code one of no_content, generation_failed,
mixing_failed, storage_error, publish_failed, article_failed,
timeout, internal_error. Failed jobs never spend quota.
Changelog
1.1.0 — 2026-09-02
DELETE /v1/articles/{id}and thedelete_articleMCP tool: soft-delete a published article or cancel a running conversion. Newdeletedstatus anddeleted_atfield on the article resource.
1.0.0 — 2026-09-01
- Initial release:
POST /v1/articles,GET /v1/articles/{id},GET /v1/articles,GET /v1/publications, webhooks, idempotency, and thecreate_article/get_article/list_articles/list_publicationsMCP tools on the shared Everlit MCP server.
Support
- Email: support@everlit.audio
- OpenAPI spec: https://everlit.audio/docs/tts-openapi.yml (shared with the TTS API)
- Status: https://status.everlit.audio