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": null, "job_id": "ajob_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/ajob_01jx... \
-H "Authorization: Bearer api_YOUR_KEY"
# => { ..., "status": "succeeded", "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 by its id (artl_...) or by job_id (ajob_...) (poll this) |
GET |
/v1/articles |
List articles, newest first (limit, starting_after, status, publication, q, ids, idempotency_key, source) |
PATCH |
/v1/articles/{id} |
Change title, summary, tags, cover, publish time or privacy → 200 |
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/publications/{id} |
One publication and the settings its articles inherit |
PATCH |
/v1/publications/{id} |
Change those settings |
GET |
/v1/voices |
Voices available to your account (shared with the TTS API) |
GET |
/v1/mixables |
Music beds — see Music beds |
POST |
/v1/mixables |
Upload a music bed of your own → 201 |
GET |
/v1/mixables/{id} |
One music bed |
DELETE |
/v1/mixables/{id} |
Delete one of your own beds → 204 |
GET |
/v1/playlists |
Playlists in this account — see Playlists |
POST |
/v1/playlists |
Create a playlist → 201 |
GET |
/v1/playlists/{id} |
One playlist with its resolved tracks and pins |
PATCH |
/v1/playlists/{id} |
Change the playlist’s rules |
DELETE |
/v1/playlists/{id} |
Delete the playlist → 204 |
POST |
/v1/playlists/{id}/articles |
Pin an article to a playlist → 201 |
PATCH |
/v1/playlists/{id}/articles/{article} |
Move a pinned article |
DELETE |
/v1/playlists/{id}/articles/{article} |
Unpin an article |
GET |
/v1/playlists/{id}/feed |
The playlist’s podcast feed — see Podcast feeds |
PATCH |
/v1/playlists/{id}/feed |
Set the podcast metadata |
POST |
/v1/playlists/{id}/feed/activate |
Publish the feed |
POST |
/v1/playlists/{id}/feed/deactivate |
Stop publishing it |
GET |
/v1/playlists/{id}/feed/platforms |
Where the show stands on each directory — see Directories |
POST |
/v1/playlists/{id}/feed/platforms/{platform}/submitted |
Record that the show was submitted there |
POST |
/v1/playlists/{id}/feed/platforms/{platform}/live |
Record that the directory listed it |
DELETE |
/v1/playlists/{id}/feed/platforms/{platform} |
Clear what was recorded for one directory → 204 |
POST |
/v1/playlists/{id}/feed/ping |
Ask Overcast to re-crawl the feed now |
GET |
/v1/pronunciations |
The pronunciation library — see Pronunciations |
PUT |
/v1/pronunciations |
Add or update a pronunciation rule |
DELETE |
/v1/pronunciations/{word} |
Remove a pronunciation rule |
GET |
/v1/ingestion-feeds |
RSS/Atom feeds narrated automatically — see Ingestion feeds |
POST |
/v1/ingestion-feeds |
Start ingesting a feed → 201 |
GET |
/v1/ingestion-feeds/{id} |
One ingestion feed |
PATCH |
/v1/ingestion-feeds/{id} |
Change its interval, caps, name or status |
DELETE |
/v1/ingestion-feeds/{id} |
Stop ingesting and delete it → 204 |
POST |
/v1/ingestion-feeds/{id}/poll |
Check the feed now |
GET |
/v1/ingestion-feeds/{id}/items |
Items the feed has seen and the articles they became |
POST |
/v1/mcp |
MCP server for AI agents — see MCP |
GET /v1/articles?idempotency_key=K finds a job again if you lose its id
before recording it (crash recovery, same as the TTS API); ?ids=artl_a,artl_b
(up to 50) fetches many articles you already know the ids of in one call.
?source= chooses jobs, Studio articles, or both — see
Any article, any origin.
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. May carry inline markup: [everlit-pause:1.5] for an explicit pause, [everlit-mixable:N] / [everlit-mixable-background:N] for a music bed, [everlit-subvoice-open:N]…[everlit-subvoice-close] for a passage in a second voice; other Everlit control sequences are 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). |
audio_url |
string | no | https URL of your own recording to publish instead of narrating — see Pre-recorded audio. Requires title. |
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, used when podcast_mode is on. |
language |
string | no | Language tag (en-US, es-MX) to narrate in, skipping detection. The publication’s voice and disclaimer for that language are used. |
podcast_mode |
boolean | no | Two-voice conversational narration. |
music |
boolean | no | Mix the intro/outro music beds into the audio. Default true. |
intro_music / outro_music |
string | no | The bed to play, overriding the publication’s: a mixable id, "shuffle" for a random track from the publication’s intro/outro music, or "" for none. |
intro_music_duration / outro_music_duration |
number | no | Seconds of bed before / after the narration. |
intro_music_pad / outro_music_pad |
number | no | Seconds the bed overlaps the narration. |
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). |
audio_polish |
boolean | no | Audio Polish pass — rewrite the text for the ear before narrating. Default: publication setting. |
audio_polish_level |
string | no | How far Audio Polish may rewrite: "0", "1", "2", "3". Default: publication setting, else "2". |
disclaimer / disclaimer_voice |
string | no | Override the spoken AI disclaimer text / voice for this article. disclaimer: "" narrates none. The strings "false", "null", "none" and "off" are rejected with 400 invalid_parameter so they are never read aloud. |
ui_disclaimer |
boolean | no | Show the AI disclosure line under the player. Default: publication setting. |
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. |
Every audio field above is the same name a publication uses for its own
default — see Publication settings. The names this
API launched with (mix, intro_mixable, outro_mixable, sonic_optimizer,
conversation_mode) are permanently accepted as aliases; sending both
spellings of one setting with different values is a 400. A validation error
names the field with the spelling you sent — send music and the error’s
param is music, send mix and it is mix.
Blank vs. omitted. This rule holds everywhere in this API, on articles and on publications:
- omitted, or
null— no opinion. The publication default applies, nothing stored is changed, and on aregeneratethe article keeps the settings it was last narrated with. -
""(empty string) — an explicit off. These are the only fields with an off state:Field ""meansdisclaimer(article)narrate no spoken disclaimer voices.{lang}.disclaimer(publication)narrate none for that language ui_disclaimers.{lang}(publication)show no disclosure line for that language intro_music/outro_music(both)play no bed language(publication)go back to detecting the language per article player.title_intro/player.title_icon(publication)show none Anywhere else
""is not an off switch: a voice, an enum or a boolean rejects it with400, and a blank number of seconds (oraudio_polish_level) is treated exactly likenull. Publicationpausesare the same —""there means use our default, because a pause has no off:0is a real, explicit value.For the music beds
""and omitted really are different: omittingintro_musiclets us pick a bed and omittingoutro_musicuses the publication’s, while""means silence.
The music beds take a third value. intro_music and outro_music each
accept a mixable id, the literal "shuffle" — a random track from the
publication’s intro/outro music, picked fresh for every narration and never
recorded on the article — or "" for no bed at all. A publication reads its
beds back in those same three forms, so the defaults object from a GET
PATCHes straight back whatever state it is in. An intro with no track set
reads as "shuffle", because that is what it plays; an outro with none reads
as "".
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.
Pre-recorded audio
Send audio_url — an https link to your own MP3, WAV or M4A — and nothing is
narrated: we fetch the file, transcribe it for read-along, mix the
publication’s intro/outro beds around it exactly as music, intro_music and
outro_music say, and publish it as an ordinary audio article with a player.
titleis required.textis optional — when you leave it out, the transcript of your recording becomes the article body.urlis still the canonical link and the identity of the article (see Existing articles).- The narration parameters do not apply and are rejected with
400 invalid_parameter:voice,guest_voice,language,podcast_mode,audio_polish,audio_polish_level,read_urls,read_alt_text,read_author,custom_byline,disclaimer,disclaimer_voice(and their aliases). Everything else — the music beds and their durations and pads,ui_disclaimer,authors,summary,category,tags,art_url,published_at,callback_url,metadata,publication— is accepted. - The file must be at most 500MB and served over
httpsfrom a public host. Every redirect is re-checked, so a link that ends up at a private address is refused with400 invalid_url. - On the finished article,
charactersandvoicearenull— nothing was synthesized, and no plan characters are spent (the article still counts against the monthly article quota).duration_secondsis the length of the mixed file, beds included. - The spoken AI disclaimer is never narrated over your recording, and the ID3
tags carry the AI disclosure as false. The disclosure line under the
player follows
ui_disclaimerwhen you send it, and is hidden when you do not — the same as an upload made in Studio. sourceisapi, as for any other article this API created.
curl https://api.everlit.audio/v1/articles \
-H "Authorization: Bearer $EVERLIT_API_KEY" -H "Content-Type: application/json" \
-d '{"publication":"pblc_abc123","title":"Episode 12: The bridge","url":"https://example.com/podcast/12","audio_url":"https://cdn.example.com/episodes/12.mp3","authors":["Ana Ruiz"]}'
# => 202 { "object": "article", "job_id": "ajob_01jx...", "status": "queued", "characters": null, ... }
Inline markup
text takes the same inline markup as the TTS API — the tags Studio and the
WordPress plugin write — so text copied from either narrates the same way:
[everlit-pause:N], [everlit-mixable:N], [everlit-mixable-background:N]
and [everlit-subvoice-open:N]…[everlit-subvoice-close]. The vocabulary,
the casing rules and the examples live in one place: Inline
markup in the TTS guide.
- Every id is checked against your account before the article is created: a
malformed tag or an unbalanced subvoice block is
400 invalid_markup, a bed or voice belonging to someone else is403 mixable_not_accessible/403 voice_not_accessible, and an id that does not exist is a404. The error’sparamistext. - Tags are not narrated and are never counted in
characters. - They apply to narrated text only. On an
audio_urlarticletextis the article body, not a script, so markup there is left exactly as you sent it. [Everlit-Sponsored:N]and any other Everlit control sequence are still stripped, fromtextand fromtitle,summary,custom_bylineanddisclaimer, which take no inline markup of their own.- Audio Polish leaves the tags where you put them.
The article resource
Returned by every endpoint and delivered to your callback — one shape:
{
"object": "article",
"id": "artl_9f3k...",
"job_id": "ajob_01jx...",
"status": "succeeded",
"source": "api",
"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,
"markup": null,
"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" },
"published_at": "2026-09-01T14:03:12Z",
"privacy": null,
"tags": [],
"summary": null,
"authors": [],
"cover": null
}
id(artl_...) is the published Everlit article — the durable thing your readers listen to. It isnulluntil the article exists, which for a new conversion means whenstatusbecomessucceeded.job_id(ajob_...) is the conversion job this API ran, returned byPOST /v1/articlesand usable as{id}on every route whileidis stillnull. It isnullon an article this API did not convert.markupcounts the inline markup in the text we narrated (pause_tags,pause_seconds,mixable_tags,subvoice_tags), and isnullwhen there was none.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.sourceisapifor a job created here andstudiofor everything else in the account (the widget, WordPress, RSS, Studio itself). Astudiorow has nojob_id, nocharacters, and itscreated_atis nothing more than itspublished_at;privacy,tags,summary,authorsandcovercome from the published article and are empty on a job that has not published one.
Any article, any origin
Your Studio holds more than this API created: pages the Auto Audio widget
narrated, WordPress posts, RSS items, uploads made in Studio. All of them are
readable, listable and deletable here by their artl_ article id.
GET /v1/articles/{id}andDELETE /v1/articles/{id}take an article id or ajob_id. Anartl_id that this API also has a job for is answered from the job (it knowscharacters,callback_url,error); otherwise you get the Studio article, withsource: "studio". Anything that is neither anartl_...id nor a job id is a400.GET /v1/articles?source=chooses what a page holds:all(the default) is every Studio article plus the jobs Studio cannot have — one that never published, and the failed and deleted ones — newest first;apiis only the jobs this API created;studiois only what Studio holds. Any other value is a400invalid_source.starting_aftertakes either form and means “older than this article”, by publication time.idsmay mix the two forms in one call; anartl_id this key cannot see is skipped rather than failing the page.- A publication-scoped key sees only articles in its own publications,
whatever created them. Another account’s article is a
404, never a403.
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 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.
A regenerate starts from the settings the article was last narrated with; anything this request sends explicitly overrides them, under the same blank vs. omitted rule.
Update article
PATCH /v1/articles/{id} changes what an article says about itself — its
title, summary, tags, cover, publish time and visibility — without
re-narrating a word of it. It takes an article id (artl_...) or a job_id
(ajob_...) and reaches any article in your Studio, whatever created it (see
Any article, any origin). Only the fields you send
change; omitting one, or sending null, leaves it alone.
| Parameter | Type | Description |
|---|---|---|
title |
string | Headline. Cannot be blank; at most 500 characters. |
summary |
string | Description shown on the player. "" clears it. |
authors |
string[] | The complete byline; replaces the article’s authors (at most 20). |
tags |
string[] | Replaces the article’s tags (at most 50). |
privacy |
string | "public" or "unlisted". |
published_at |
string | ISO 8601 publish time. |
art_url |
string | Cover image URL. |
metadata |
object | Up to 8 string values (512 bytes total). Only on an article this API created — a 400 otherwise. |
Unlisted and scheduled. privacy: "unlisted" keeps the player and the
embed working but hides the article from playlists, feeds, roundups and
listings. A published_at in the future schedules the article: unless you
also send privacy, it is set to unlisted and flipped public at that time.
A published_at in the past or present leaves privacy untouched.
The answer is the same article resource every other
endpoint returns. An article that is still converting (queued /
processing) has nothing published to edit yet and answers 409
job_not_complete; a deleted one answers 409 article_deleted.
curl -X PATCH https://api.everlit.audio/v1/articles/artl_9f3k... \
-H "Authorization: Bearer $EVERLIT_API_KEY" -H "Content-Type: application/json" \
-d '{"title":"City approves budget, 7-2","tags":["city hall","budget"],"privacy":"unlisted"}'
# => 200 { "object": "article", "id": "artl_9f3k...", "title": "City approves budget, 7-2", "privacy": "unlisted", ... }
Deleting articles
DELETE /v1/articles/{id} answers 204 and does one of two things. It takes
an article id (artl_...) or a job_id (ajob_...), so it also takes down an
article Studio, the widget, WordPress or RSS made — see
Any article, any origin.
- 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 same article (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
}
default_voice is the narrator English content gets; language_voices lists
every language the publication configures a voice for. Everything else a
publication hands its articles is in Publication
settings.
Publication settings
GET /v1/publications/{id} returns one publication with everything its
articles inherit, and PATCH /v1/publications/{id} changes it. These apply to
every article the publication produces — the API, the Auto Audio widget on
your site, RSS ingestion and Studio alike — so change them deliberately; to
affect one article only, pass the audio fields to POST /v1/articles.
{
"object": "publication",
"id": "pblc_abc123",
"name": "The Daily",
"url": "https://example.com",
"language_voices": ["en-US", "es-MX"],
"default_voice": "109",
"defaults": {
"language": "en-US",
"voices": {
"en-US": { "voice": "109", "guest_voice": "115", "disclaimer": "This audio was generated by AI.", "disclaimer_voice": "112" },
"es-MX": { "voice": "204", "guest_voice": null, "disclaimer": "Audio generado por IA.", "disclaimer_voice": null }
},
"author_voices": [{ "author": "Jane Doe", "voice": "118" }],
"ui_disclaimers": { "en-US": "Narrated by AI." },
"music": true,
"intro_music": "34",
"outro_music": "35",
"intro_music_duration": 15,
"outro_music_duration": 15,
"intro_music_pad": 5,
"outro_music_pad": 4,
"audio_polish": true,
"audio_polish_level": "2",
"podcast_mode": false,
"read_urls": false,
"read_alt_text": false,
"read_author": true,
"player": {
"size": "large", "cover_art": true, "title_intro": "Listen Now:", "title_icon": "headphones",
"theme": {
"colors": { "accent": "#1a2b3c", "background": "#ffffff" },
"fonts": { "title": { "src": "https://example.com/fonts/tiempos.woff2", "size_adjust": 100 } }
}
},
"studio": { "read_along": true },
"sharing": { "enabled": true, "link": "canonical" },
"email_capture": { "enabled": false, "label": null, "prompt": null },
"pauses": { "title": 0.5, "subtitle": [0.4, 0.6], "byline": null, "disclaimer": null, "header_to_body": null },
"extraction": {
"content_selector": ".article-body",
"title_selector": null,
"date_selector": null,
"exclude_selectors": [".share-widget", ".newsletter-signup"],
"tag_field_map": { "section": "category" },
"site_name_tag_prefix": null,
"tag_namespaces": ["author", "category", "language"]
},
"widget": {
"allowed_origins": ["example.com"],
"click_to_create": false,
"click_to_create_message": null,
"generate_on_first_view": false,
"rss_only": false
},
"paywall": {
"enabled": false,
"access_attribute": "everlit-access",
"cta_text": null,
"cta_url": null,
"button_label": null,
"button_color": null,
"button_color_enabled": false
}
}
}
PATCH takes any subset, follows the same
blank vs. omitted rule, and returns the updated publication
in the shape above. Unknown fields are a 400 unknown_parameter at any depth
(player.colour, voices.en-US.accent).
Voices
Voices, spoken disclaimers and the voice that reads them are configured per
language, because that is how narration is chosen: we detect the article’s
language (or take the language you send) and use that language’s entry.
| Field | Type | Description |
|---|---|---|
voices |
object | Keyed by language tag (en-US, es-MX, es, or the catch-all default). Each value is { voice, guest_voice, disclaimer, disclaimer_voice }. |
voices.{lang}.voice |
string | Narrator voice id for this language. Cannot be blank. |
voices.{lang}.guest_voice |
string | The second voice in podcast_mode for this language. Configured exactly like voice, and like it cannot be blank: leaving it unset means a voice is chosen when a dialogue is actually produced, which is not the same as “no guest”. |
voices.{lang}.disclaimer |
string | Spoken AI disclaimer. "" narrates none for this language. |
voices.{lang}.disclaimer_voice |
string | Voice that reads it. "" only while that language’s disclaimer is blank too. |
language |
string | The tag to narrate in without detecting, e.g. "en-US". Must be a key of voices; "" goes back to detecting per article. |
author_voices |
array | [{ "author": "Jane Doe", "voice": "118" }] — a byline’s own voice, which beats the language voice. Author names match ignoring case. |
ui_disclaimers |
object | Per language: the AI disclosure line shown under the player (the spoken one is voices.{lang}.disclaimer). "" shows none for that language; null drops the language. |
A voices or ui_disclaimers patch merges language by language: languages
you do not mention are untouched, and within a language, fields you do not
mention are untouched. Send "es-MX": null to remove that language entirely
(voice, disclaimer and disclaimer voice together). author_voices is the
exception — it is a list, so sending it replaces the whole list, and []
clears it.
Language tags are canonicalized (es-mx → es-MX), the same normalization
the Auto Audio widget applies, so both surfaces read each other’s settings.
Two notes on older publications. A publication configured before per-language
voices existed reports its single voice, guest voice and disclaimer under en-US — where
narration actually falls back to them, and where they migrate to. And a
language whose voice is a legacy rotation (several ids we pick from) reports
the first id; writing voice replaces the rotation with the one voice you
send.
Audio
| Field | Type | Description |
|---|---|---|
music |
boolean | Shorthand for turning both intro_music and outro_music off (false) or back on (true). It is derived, not stored: reading it is “at least one bed plays”, and writing it leaves the bed ids alone, so music: true puts the same beds back. Sending it in the same request as an intro_music / outro_music that disagrees is a 400. |
intro_music / outro_music |
string | The bed every article gets: a mixable id, "shuffle" for a random track from your intro/outro music, or "" for none. Setting an id clears a shuffle, and a shuffle keeps the id, so turning the shuffle off puts the same bed back. |
intro_music_duration / outro_music_duration |
number | Seconds of bed before / after the narration. |
intro_music_pad / outro_music_pad |
number | Seconds the bed overlaps the narration. |
audio_polish |
boolean | Rewrite the text for the ear before narrating. |
audio_polish_level |
string | How far it may rewrite: "0" (symbols and numbers only), "1", "2", "3" (free rewrite). Default "2". |
podcast_mode |
boolean | Two-voice conversational narration, using guest_voice. |
read_urls / read_alt_text |
boolean | Read URLs / image alt text aloud. |
read_author |
boolean | Read the byline aloud. |
pauses |
object | Silence after title, subtitle, byline, disclaimer, header_to_body. Each is seconds as a number, or [min, max] for a range we pick from per article. |
Music is always mixed at our end: the player is handed one finished, pre-mixed file, so the only music switch is which beds play, not whether we mix.
Music beds
GET /v1/mixables lists the intro/outro beds intro_music and outro_music
(here and on POST /v1/articles) may reference — Everlit’s library plus
anything assigned to your publisher — so you never have to guess an id and
read the 404 back:
{
"object": "list",
"data": [
{ "id": "42", "name": "Calm Intro", "kind": "intro", "tags": { "mood": "calm" },
"owned_by": "everlit", "preview_url": "https://cdn.everlit.audio/mixables/calm-intro.mp3" }
],
"has_more": false
}
| Field | Description |
|---|---|
id |
Pass this as intro_music / outro_music. |
name |
Display name (the filename without its extension). |
kind |
As stored on the bed (intro, outro, …). |
tags |
Free-form tags on the bed (mood, genre, …). |
owned_by |
everlit (global library) or publisher (assigned to your account only). |
preview_url |
A clip you can hand a person, or null. |
Query params: kind (filter, e.g. ?kind=intro), q (free-text on name or
tags), limit, starting_after. No TTS entitlement is required — it is
plan-based like the rest of the Articles API.
Your own beds. POST /v1/mixables adds a track to your account: we fetch
the audio from url and store it, and the bed it returns can be used as
intro_music / outro_music straight away. Everlit’s own library is never
changed — owned_by: "everlit" rows are read-only for every account.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Display name, 1–120 characters. Becomes the bed’s filename with the fetched file’s extension. |
url |
string | yes | https:// URL of the audio file. Fetched by our servers: mp3, wav, m4a, ogg or flac, at most 25 MB, no IP-literal or private hosts, at most 3 redirects. |
kind |
string | no | intro-outro (a music bed — the default), sfx (a sound effect) or ambient. |
tags |
object | no | tempo, subjects and attributes, each an array of up to 20 strings of at most 40 characters. Any other key is a 400. |
static_duration |
boolean | no | true when the track must play in full instead of being trimmed to the configured seconds. |
publications |
string[] | no | Publication ids (pblc_...) this bed is limited to. Omit (or []) to make it usable across the whole account. |
The response is the same row the list returns, plus a Location header. The
bed’s Studio dropdown label is set for you (tags.display_kind: Music for
intro-outro, Sound Effects for sfx, Ambient for ambient).
GET /v1/mixables/{id} reads one bed — yours or one of Everlit’s.
DELETE /v1/mixables/{id} deletes a bed you uploaded and clears it from
any publication still pointing at it (204). An Everlit library track is a
403 not_owned; a bed on another account is a 404, like any id you cannot
see.
curl -X POST https://api.everlit.audio/v1/mixables \
-H "Authorization: Bearer $EVERLIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Upbeat Intro",
"url": "https://cdn.example.com/beds/upbeat-intro.mp3",
"kind": "intro-outro",
"tags": { "tempo": ["upbeat"], "subjects": ["news"], "attributes": ["energetic"] }
}'
Player and page
| Field | Type | Description |
|---|---|---|
player.size |
string | large or small. |
player.cover_art |
boolean | Show the article’s cover art in the player. |
player.title_intro |
string | Line above the title, e.g. "Listen Now:". "" shows none. |
player.title_icon |
string | Icon beside the title ("headphones"). "" shows none. |
studio.read_along |
boolean | Highlight words during preview playback in Studio. |
sharing.enabled |
boolean | Show the share button on the player. |
sharing.link |
string | Which URL it shares: canonical, canonical_hosted or hosted. |
email_capture.enabled |
boolean | Ask listeners for an email address. |
email_capture.label |
string | Button label, at most 30 characters. |
email_capture.prompt |
string | Prompt text, at most 120 characters. |
Theme
player.theme is the player’s design — the same colours and fonts Studio’s
Player design screen paints the embed with.
| Field | Type | Description |
|---|---|---|
player.theme.colors |
object | Named colours, each a hex string like "#1a2b3c". The names: background, accent, primary, title_text, body_text, border, cover_background, control_background, control_icon, progress_bar, progress_bar_active, progress_handle, playlist_tracks_background, playlist_tracks_border. |
player.theme.fonts |
object | title and body, each { "src": "https://…/font.woff2", "size_adjust": 100 }. src must be an https URL ending in .woff2, .woff, .ttf or .otf; size_adjust is a CSS size-adjust percentage between 50 and 200 (default 100). |
The theme merges key by key, unlike the lists elsewhere in this API: a
colour or font you do not mention is left exactly as it was. Send "" for one
colour, or "" in place of a whole font object, to put that one back to its
derived default. Colours that are not #rrggbb are a 400, and a colour set
directly on the publication outside these fourteen names is preserved.
Page extraction
How the Auto Audio widget reads a page. Everything here is an override: leave it unset and articles are extracted automatically, which is right for nearly every site. Reach for it when a page’s furniture is being narrated, or the wrong element is picked as the body.
| Field | Type | Description |
|---|---|---|
extraction.content_selector |
string | CSS selector for the article body when automatic extraction picks the wrong element. Every match is read, in page order. "" goes back to automatic extraction. |
extraction.title_selector |
string | CSS selector for the headline, when the page has no usable title metadata. |
extraction.date_selector |
string | CSS selector for the publication date, same. |
extraction.exclude_selectors |
array of strings | Page furniture never to narrate — share widgets, newsletter sign-ups, related-story rails. At most 100 entries, each at most 500 characters. |
extraction.tag_field_map |
object | Page field name → tag prefix, e.g. {"section": "category"}: which of the page’s own metadata fields become article tags, and how each is prefixed. At most 50 entries. |
extraction.site_name_tag_prefix |
string | When set (e.g. "site"), every article gets a "<prefix>:<site name>" tag taken from the page. "" adds none. |
extraction.tag_namespaces |
array of strings | Tag namespaces the public playlist tag picker offers: any of author, category, language, content. Anything else is a 400. |
The three lists and the map replace what is stored rather than merging into
it, so send the whole list you want. [] (or {}) clears one: no exclusions,
no tag fields, no namespaces offered.
Widget behaviour
When and where the Auto Audio widget narrates.
| Field | Type | Description |
|---|---|---|
widget.allowed_origins |
array of strings | Hostnames the widget may narrate pages from, e.g. ["example.com", "www.example.com"]. Entries are hostnames only — a scheme or a path is a 400. [] allows every page the widget runs on. Replaces the whole list. |
widget.click_to_create |
boolean | Wait for a reader to press play before narrating a page that has no audio yet, instead of narrating it on the first view. |
widget.click_to_create_message |
string | Message shown while that first narration is being made. "" shows the default. |
widget.generate_on_first_view |
boolean | Narrate an older article on its first pageview instead of waiting for it to draw repeat traffic. For back catalogues. |
widget.rss_only |
boolean | Only narrate articles that arrive through an RSS feed; the widget narrates nothing on its own. |
Paywall
Gate the player behind your paywall. Readers without access hear nothing and see the call to action instead.
| Field | Type | Description |
|---|---|---|
paywall.enabled |
boolean | Turn the gate on. |
paywall.access_attribute |
string | The HTML attribute your page sets on a subscriber to say they have access. Defaults to everlit-access. |
paywall.cta_text |
string | The line shown to a reader without access, e.g. "Subscribe today to listen to this content". |
paywall.cta_url |
string | Where the button sends them — an http(s) URL. "" removes the link. |
paywall.button_label |
string | Label on that button, e.g. "Get Access". |
paywall.button_color |
string | Button colour as #rrggbb. Used only when button_color_enabled is true. |
paywall.button_color_enabled |
boolean | Use button_color instead of the player’s own colours. |
Every string field here follows the usual rule: omit it (or send null) to
leave it alone, and send "" to clear it.
curl -X PATCH https://api.everlit.audio/v1/publications/pblc_abc123 \
-H "Authorization: Bearer $EVERLIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"voices": { "es-MX": { "voice": "204", "disclaimer": "Audio generado por IA." } },
"audio_polish_level": "3",
"player": { "title_intro": "Listen:" }
}'
Playlists
A playlist is a curated group of your published articles: pinned tracks in
order, optionally topped up by a tag rule (“fill” — any newly published
article carrying one of the playlist’s tags is added, newest first), with a
player embed and its own RSS feed for podcast platforms. Studio owns the
playlist itself (it is where you build and manage one); this API is for
reading a playlist’s contents and pinning or unpinning tracks as new audio
publishes — most useful right after create_article succeeds.
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/playlists |
List this account’s playlists |
POST |
/v1/playlists |
Create a playlist |
GET |
/v1/playlists/{id} |
One playlist, with its resolved tracks and pins |
PATCH |
/v1/playlists/{id} |
Change its rules |
DELETE |
/v1/playlists/{id} |
Delete it |
POST |
/v1/playlists/{id}/articles |
Pin an article |
PATCH |
/v1/playlists/{id}/articles/{article} |
Move a pinned article |
DELETE |
/v1/playlists/{id}/articles/{article} |
Unpin an article |
curl -X POST https://api.everlit.audio/v1/playlists/plist_a1b2c3/articles \
-H "Authorization: Bearer $EVERLIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"article": "artl_9f3k2q9m7v4n6b1c5d8e0h"}'
# => 201
# {"object":"playlist_pin","playlist":"plist_a1b2c3","id":"plart_9k2m","article_id":"artl_9f3k...","position":4}
article (on both the pin and unpin routes) accepts either an ajob_...
job_id from create_article — resolved to the article it published,
so you never have to wait for a second lookup — or the artl_... article id
directly. A job with no published article yet answers 409 job_not_complete.
Unpinning only removes a pin: an article the tag rule filled in is not a pin
and cannot be removed this way (edit the rule in Studio instead).
The playlist resource:
{
"object": "playlist",
"id": "plist_a1b2c3",
"name": "Morning Briefing",
"description": "Top stories, read aloud",
"tags": ["news"],
"ignore_tags": [],
"match": "any",
"size": 25,
"order": "latest",
"within": "any",
"within_start": null,
"within_end": null,
"fill": true,
"publications": [],
"embed_url": "https://everlit.audio/embeds/plist_a1b2c3",
"embed_html": "<iframe src=\"https://everlit.audio/embeds/plist_a1b2c3\" ...></iframe>",
"feed_url": "https://creator.everlit.audio/feeds/lists/plist_a1b2c3",
"feed_distributed": false
}
publications, when non-empty, restricts the playlist to those publications’
articles and to keys scoped to (at least) one of them — an unrestricted
playlist ([]) is visible to every key on the account. feed_url is always
present — it is the RSS feed creator itself serves for the playlist, valid
whether or not you have submitted it anywhere; feed_distributed tells you
whether it has actually been submitted to podcast platforms (Apple, Spotify,
…) yet. GET /v1/playlists/{id} additionally returns tracks (the
resolved, ordered list of articles — pinned plus any the tag rule filled in,
each with article_id, title, published_at, duration_seconds,
publication, player_url) and pins (just the pins, with their id and
position, for the ones remove_from_playlist / DELETE .../articles/{article}
can remove).
Changing the rules
PATCH /v1/playlists/{id} changes the playlist itself. Only the fields you
send change; omitting one (or sending null) leaves it alone. Send at least
one field.
| Field | Type | Meaning |
|---|---|---|
name |
string | Playlist name |
description |
string | Shown on the player and in the feed |
tags |
array of strings | The fill rule: articles carrying these tags join automatically |
match |
"any" | "all" |
Whether an article needs any of the tags or all of them |
ignore_tags |
array of strings | Exclude articles carrying any of these tags |
size |
integer 1–100 | How many tracks the playlist holds |
order |
played | viewed | latest | oldest | random |
Which tracks survive the cap, and in what order |
within |
any | current_day | current_week | current_month | last_day | last_week | last_month | custom |
Only articles published in this window |
within_start, within_end |
string | Required when within is "custom" |
publications |
array of pblc_ ids |
Restrict the playlist to these publications |
fill |
boolean | false: pinned articles only, no tag rule |
curl -X PATCH https://api.everlit.audio/v1/playlists/plist_a1b2c3 \
-H "Authorization: Bearer $EVERLIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tags": ["news", "politics"], "ignore_tags": ["sponsored"], "size": 15, "order": "latest"}'
An unknown field is a 400 unknown_parameter; a publication your key cannot
see is a 404 publication_not_found.
Reordering and deleting
PATCH /v1/playlists/{id}/articles/{article} moves a pinned article. Send
exactly one of:
before— the article (anartl_...id or anajob_...job_id) this one should sit in front of, ornullto move it to the end.position— a 1-based slot among the playlist’s pins. A position past the last pin moves it to the end.
Sending both, or neither, is a 400. Only pins can be moved — an article the
tag rule filled in is ordered by the rule, and moving one answers
404 article_not_found. The response is the pin in its new place:
{"object":"playlist_pin","playlist":"plist_a1b2c3","id":"plart_9k2m","article_id":"artl_9f3k...","position":1}.
DELETE /v1/playlists/{id} deletes the playlist itself and answers 204: its
player and page come down, but the articles in it are untouched and keep their
own players. If the playlist had a podcast feed, the feed is
deactivated rather than destroyed — it stops publishing new episodes, and its
metadata is still there if you rebuild the show later.
Podcast feeds
Every playlist is already served as an RSS feed at its feed_url. What turns
that URL into a podcast Apple, Spotify or Overcast will accept is the show’s
metadata — title, description, owner email, category, cover art, language —
plus switching distribution on. That is what these routes do.
The flow: PATCH .../feed to fill the metadata → POST .../feed/activate →
submit the feed_url to the directories. Submission itself is done by a person
in Studio → Feeds (the directories require a human to accept their terms);
everything up to that point is here.
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/playlists/{id}/feed |
The feed, its metadata and its directory status |
PATCH |
/v1/playlists/{id}/feed |
Set the podcast metadata |
POST |
/v1/playlists/{id}/feed/activate |
Validate and start publishing |
POST |
/v1/playlists/{id}/feed/deactivate |
Stop publishing (metadata kept) |
GET |
/v1/playlists/{id}/feed/platforms |
Where the show stands on each directory |
POST |
/v1/playlists/{id}/feed/platforms/{platform}/submitted |
Record a submission the user made |
POST |
/v1/playlists/{id}/feed/platforms/{platform}/live |
Record the directory listing the show |
DELETE |
/v1/playlists/{id}/feed/platforms/{platform} |
Clear one directory’s record |
POST |
/v1/playlists/{id}/feed/ping |
Ping Overcast to re-crawl |
curl -X PATCH https://api.everlit.audio/v1/playlists/plist_a1b2c3/feed \
-H "Authorization: Bearer $EVERLIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "The Morning Briefing",
"description": "Top stories from the newsroom, read aloud every weekday.",
"owner_email": "podcasts@example.com",
"category": "News",
"artwork_url": "https://cdn.example.com/briefing-3000.jpg",
"language": "en",
"keywords": ["news", "local"]
}'
curl -X POST https://api.everlit.audio/v1/playlists/plist_a1b2c3/feed/activate \
-H "Authorization: Bearer $EVERLIT_API_KEY"
Required before a feed can go live: title, description, owner_email,
category, artwork_url and language. Activation fills what it can from the
playlist itself (its name becomes the title, its description the description,
its tags the keywords, and the owner email defaults to a managed Everlit
address that receives the directories’ verification codes), so in practice only
artwork_url usually has to be set by hand — square JPEG or PNG, 1400–3000px.
A feed never lacks a description: when none was written, one is derived as “A show from publication featuring title” (the playlist’s publication when it is restricted to one, otherwise the account name).
activate answers 200 with active: true once the feed is live. When
metadata is still missing it is a 400 feed_incomplete whose error.message
and error.missing_fields name the fields (usually just artwork_url) and
whose error.feed is the feed as it stands; set them with PATCH .../feed
and call activate again.
Every writable field, all optional: title (≤255), description (≤4000),
subtitle (≤255), summary (≤4000), artwork_url (http(s)), website_url
(http(s)), author (≤255), owner_name (≤255), owner_email (an email
address), category, subcategory, language (≤10, e.g. "en"), explicit
(boolean), keywords (array of strings), copyright (≤255), type
("episodic" or "serial"), complete (boolean — the show is finished),
locked (boolean — block other platforms from importing the feed),
funding_url (http(s)) and funding_text (≤255). null leaves a field alone;
"" clears it.
The feed resource:
{
"object": "feed",
"playlist": "plist_a1b2c3",
"feed_url": "https://creator.everlit.audio/feeds/lists/plist_a1b2c3",
"active": true,
"distribution_enabled": true,
"validation_status": "valid",
"validation_errors": [],
"title": "The Morning Briefing",
"description": "Top stories from the newsroom, read aloud every weekday.",
"subtitle": null,
"summary": null,
"artwork_url": "https://cdn.example.com/briefing-3000.jpg",
"website_url": null,
"author": "Morning Briefing",
"owner_name": "Example News",
"owner_email": "podcasts@example.com",
"category": "News",
"subcategory": null,
"language": "en",
"explicit": false,
"keywords": ["news", "local"],
"copyright": null,
"type": "episodic",
"complete": false,
"locked": true,
"funding_url": null,
"funding_text": null,
"podcast_guid": "ead4c236-bf58-58c6-a2c6-a6b28d128cb6",
"last_validated_at": "2026-09-11T09:00:00Z",
"distributions": [
{ "platform": "apple", "status": "live", "platform_url": "https://podcasts.apple.com/...", "updated_at": "2026-09-11T09:05:00Z" }
]
}
active is the one to check: it means the feed both validates and has
distribution on. distribution_enabled is the switch alone, validation_status
(pending / valid / incomplete) the metadata alone. podcast_guid is the
show’s permanent identity in the podcast namespace — it never changes once the
feed exists, which is what lets a show move hosts without losing its
subscribers. distributions lists the directories the show has been submitted
to and where each one stands.
A playlist with no feed set up yet still answers GET .../feed: feed_url is
there (it is always served), active is false and every metadata field is
null.
Directories
A feed is only a URL until a directory carries it. Every directory except Overcast requires a person to sign in and submit the feed — they all make someone accept their terms — so these routes record what happened rather than do it. The flow:
POST .../feed/activate, so there is a feed worth submitting.GET .../feed/platformsand hand the user thesubmission_urlfor the directory they want.- The user submits the
feed_urlthere. POST .../feed/platforms/{platform}/submitted— withnotesif there is anything worth remembering.- When the directory approves the show (hours to days),
POST .../feed/platforms/{platform}/livewithplatform_url, the show’s page on that directory.
The six platforms are apple, spotify, pocketcasts, amazon, youtube
and overcast. status is one of not_started (nothing recorded yet),
pending, submitted, live, failed or removed.
Two of them have no submission_url. Apple is submitted in
Apple Podcasts Connect — the Studio wizard
walks a user through signing in there with an Apple ID and pasting the feed
URL. Overcast takes no submission at all: it discovers feeds on its own and
accepts a ping. POST .../feed/ping tells Overcast the feed changed so it
re-crawls now; every other directory polls on its own schedule, and there is
nothing to ping.
# The user submitted the feed at podcasters.spotify.com
curl -X POST https://api.everlit.audio/v1/playlists/plist_a1b2c3/feed/platforms/spotify/submitted \
-H "Authorization: Bearer $EVERLIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"notes": "Submitted from the newsroom account"}'
# Spotify approved it
curl -X POST https://api.everlit.audio/v1/playlists/plist_a1b2c3/feed/platforms/spotify/live \
-H "Authorization: Bearer $EVERLIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"platform_url": "https://open.spotify.com/show/4kL2..."}'
Both take an optional platform_podcast_id (the directory’s own id for the
show); submitted also takes notes (≤1000 characters). platform_url is
required on live for every directory a person submits to — that is, all
but Overcast. Both need the feed to be active first, or they answer
409 feed_not_active; submitted on a platform already live is a
409 already_live (reset it first). An unknown platform is a
404 platform_not_found.
DELETE .../feed/platforms/{platform} forgets Everlit’s record — the platform
reads not_started again — and 404 platform_not_started when there was
nothing to forget. It withdraws nothing at the directory itself.
The feed_platform resource:
{
"object": "feed_platform",
"playlist": "plist_a1b2c3",
"platform": "spotify",
"name": "Spotify",
"status": "live",
"submission_url": "https://podcasters.spotify.com/",
"manual_submission": true,
"supports_ping": false,
"platform_url": "https://open.spotify.com/show/4kL2...",
"platform_podcast_id": "4kL2...",
"submitted_at": "2026-09-11T09:05:00Z",
"live_at": "2026-09-13T14:20:00Z",
"last_checked_at": "2026-09-13T14:20:00Z",
"last_error": null,
"notes": "Submitted from the newsroom account"
}
GET .../feed/platforms answers { "object": "list", "data": [...],
"has_more": false } with one row per platform, in that order, whether or not
anything has been recorded for it. The distributions array on the feed itself
carries the same fields (minus object and playlist, plus updated_at) but
only for the platforms that have a record.
The ping answers:
{
"object": "feed_ping",
"playlist": "plist_a1b2c3",
"platform": "overcast",
"pinged_at": "2026-09-13T14:20:00Z",
"ok": true,
"message": "Overcast will re-crawl the feed shortly."
}
ok: false with a message means Overcast refused or timed out; it is safe to
try again. Pinging a feed that is not active is a 409 feed_not_active.
Ingestion feeds
An ingestion feed is a standing instruction rather than a one-off call: point a publication at an RSS or Atom feed and every item the feed publishes from then on is narrated automatically, with that publication’s defaults — voice, music, disclaimer, player. Set it up once and the newsroom’s own CMS feed becomes the input to the audio.
Everything about how an item is narrated comes from the publication, not the feed. The feed itself only decides which items are picked up: how often it is checked, how many a single check may take, and how far back to go.
Each item narrated is an article and counts against your plan’s monthly
article quota, exactly as if you had called POST /v1/articles for it. A busy
feed with no max_items can spend a month’s quota in one poll; max_items and
oldest_date are what keep the first poll from narrating the whole archive.
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/ingestion-feeds |
Feeds this key can see (optionally ?publication=pblc_...) |
POST |
/v1/ingestion-feeds |
Start ingesting a feed → 201 |
GET |
/v1/ingestion-feeds/{id} |
One feed |
PATCH |
/v1/ingestion-feeds/{id} |
Change the interval, caps, name, or pause/resume it |
DELETE |
/v1/ingestion-feeds/{id} |
Stop ingesting and delete the feed → 204 |
POST |
/v1/ingestion-feeds/{id}/poll |
Check the feed now |
GET |
/v1/ingestion-feeds/{id}/items |
What the feed has seen and what became of it |
curl -X POST https://api.everlit.audio/v1/ingestion-feeds \
-H "Authorization: Bearer $EVERLIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"publication": "pblc_a1b2c3",
"url": "https://example.com/feed.xml",
"name": "Front page",
"poll_interval_minutes": 15,
"max_items": 5,
"oldest_date": "2026-09-01T00:00:00Z"
}'
publication and url are required; everything else is optional.
poll_interval_minutes is an integer from 5 to 1440 (default 15), max_items
an integer from 1 to 500 (null = no cap), oldest_date an ISO 8601 timestamp
below which items are skipped (null = the whole feed), name a label of up to
120 characters, and status either "active" (the default) or "paused".
poll_now (default true) polls once immediately as well as on the interval,
so you see whether the feed parses without waiting a quarter of an hour.
A feed URL is ingested once across Everlit: a url already on a feed answers
409 ingestion_feed_exists, naming the existing feed’s id when it is one of
yours. The url of an existing feed cannot be changed — a different address is a
different feed, so delete this one and create another.
Deleting a feed removes it and the record of the items it has seen. Articles already narrated from it are not deleted — they are ordinary articles with their own players from the moment they are published.
The feed resource:
{
"object": "ingestion_feed",
"id": "rsif_9k2mq4x7dhz",
"publication": "pblc_a1b2c3",
"url": "https://example.com/feed.xml",
"name": "Front page",
"status": "active",
"poll_interval_minutes": 15,
"max_items": 5,
"oldest_date": "2026-09-01T00:00:00Z",
"last_polled_at": "2026-09-11T09:15:00Z",
"last_build_date": "2026-09-11T09:12:00Z",
"last_error": null,
"items": { "total": 42, "completed": 40, "processing": 1, "error": 1 },
"created_at": "2026-09-01T12:00:00Z",
"updated_at": "2026-09-11T09:15:00Z"
}
status is active while the feed is being polled, paused while you have
stopped it, and error when the last poll failed — last_error says why, and
setting status back to "active" clears it. A paused feed is polled by
nobody: POST .../poll on one answers 409 ingestion_feed_paused.
Polling now
POST /v1/ingestion-feeds/{id}/poll queues an immediate check and answers at
once — the items are narrated in the background, so read
GET .../items afterwards to see what came of it:
{ "object": "ingestion_poll", "feed": "rsif_9k2mq4x7dhz", "queued": true, "queued_at": "2026-09-11T09:20:00Z" }
Manual polls are throttled to one a minute per feed; inside that minute the call
is a 429 poll_too_soon with a Retry-After header giving the seconds left.
The interval polls are unaffected.
Items
GET /v1/ingestion-feeds/{id}/items lists what the feed has seen, newest
first. ?limit is 1 to 100 (default 50), ?status filters to
pending / processing / completed / error, and ?starting_after takes an
item id to page after; has_more says whether another page exists.
{
"object": "ingestion_item",
"id": "rsii_4t8bn1qwlvz",
"feed": "rsif_9k2mq4x7dhz",
"guid": "https://example.com/news/city-budget",
"url": "https://example.com/news/city-budget",
"title": "City approves budget",
"status": "completed",
"article": "artl_x7y8z9",
"published_at": "2026-09-11T09:05:00Z",
"last_error": null,
"created_at": "2026-09-11T09:15:00Z",
"updated_at": "2026-09-11T09:16:30Z"
}
article is the article the item became — an artl_ id you can pass to
GET /v1/articles/{id}, PATCH, or POST /v1/playlists/{id}/articles — and is
null until the narration finishes. An item that failed carries status:
"error" and the reason in last_error; the feed keeps going.
Pronunciations
The pronunciation library fixes how a name or word is spoken, for every
future narration — not just one article. A rule is either account-wide
(publication omitted or null, applied to every publication) or scoped to
one publication (its own rules apply on top of the account-wide ones). This
changes only narrations made after the rule is set; an article already
narrated keeps its original audio until you regenerate: true it.
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/pronunciations |
List this account’s rules (optionally ?publication=pblc_...) |
PUT |
/v1/pronunciations |
Add or update a rule (upsert on word + scope) |
DELETE |
/v1/pronunciations/{word} |
Remove a rule (optionally ?publication=pblc_...) |
curl -X PUT https://api.everlit.audio/v1/pronunciations \
-H "Authorization: Bearer $EVERLIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"word": "Nguyen", "say": "win"}'
# => 200
# {"object":"pronunciation","word":"Nguyen","say":"win","exact_match":false,"publication":null,"updated_at":"2026-09-09T00:00:00Z"}
word (up to 120 characters) is the text as it appears in your content;
say (up to 300 characters) is how it should sound — spell it phonetically,
the way you’d want it read aloud, not an IPA transcription. exact_match
(default false) matches the word only as a whole word rather than also
inside longer words containing it. A PUT with the same word and
publication updates the existing rule in place rather than creating a
second one; a word set both account-wide and for a publication keeps both —
the publication’s rule is what that publication’s narrations use.
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. - Concurrent conversions are capped at the number of publications on
your account: one running conversion per publication (
max_concurrent_articlesunderplaninGET /v1/account;nullmeans no cap). Over it,POST /v1/articlesanswers429 concurrency_limit_exceededwithRetry-After; wait for a running job to finish, then retry. Requests that converge onto an existing article never take a slot. This cap is a plan feature only: pay-as-you-go credit or a card adds extra lanes for speech (POST /v1/speech), never for article conversions. - Requests: 60 creates and 300 reads per minute per key, with
RateLimit-Limit,RateLimit-Remaining, andRateLimit-Resetheaders on every response andRetry-Afteron a429.PATCH /v1/publications/:idis a write: it draws on the create bucket, not the read one. - 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 |
get_publication |
GET /v1/publications/{id} |
update_publication |
PATCH /v1/publications/{id} |
create_article |
POST /v1/articles (accepts an idempotency_key argument) |
get_article |
GET /v1/articles/{id} (accepts idempotency_key instead of id) |
list_articles |
GET /v1/articles (accepts ids and idempotency_key) |
update_article |
PATCH /v1/articles/{id} |
delete_article |
DELETE /v1/articles/{id} |
list_voices |
GET /v1/voices |
list_mixables |
GET /v1/mixables |
update_playlist |
PATCH /v1/playlists/{id} |
delete_playlist |
DELETE /v1/playlists/{id} |
move_in_playlist |
PATCH /v1/playlists/{id}/articles/{article} |
get_feed |
GET /v1/playlists/{id}/feed |
update_feed |
PATCH /v1/playlists/{id}/feed |
activate_feed |
POST /v1/playlists/{id}/feed/activate |
deactivate_feed |
POST /v1/playlists/{id}/feed/deactivate |
list_feed_platforms |
GET /v1/playlists/{id}/feed/platforms |
mark_feed_submitted |
POST /v1/playlists/{id}/feed/platforms/{platform}/submitted |
mark_feed_live |
POST /v1/playlists/{id}/feed/platforms/{platform}/live |
reset_feed_platform |
DELETE /v1/playlists/{id}/feed/platforms/{platform} |
ping_feed |
POST /v1/playlists/{id}/feed/ping |
list_ingestion_feeds |
GET /v1/ingestion-feeds |
create_ingestion_feed |
POST /v1/ingestion-feeds |
get_ingestion_feed |
GET /v1/ingestion-feeds/{id} |
update_ingestion_feed |
PATCH /v1/ingestion-feeds/{id} |
delete_ingestion_feed |
DELETE /v1/ingestion-feeds/{id} |
poll_ingestion_feed |
POST /v1/ingestion-feeds/{id}/poll |
list_ingestion_items |
GET /v1/ingestion-feeds/{id}/items |
create_mixable |
POST /v1/mixables |
delete_mixable |
DELETE /v1/mixables/{id} |
Endpoint: https://api.everlit.audio/v1/mcp — Streamable HTTP transport,
stateless, JSON responses (see
Sessions and updates). Authenticate either by connecting
with OAuth (log in to Studio, no key to copy) or with the same
Authorization: Bearer api_... header the REST endpoints take — see
Connect with OAuth.
{
"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 an audio article from a url, or from title + text, in a publication; returns the queued job with job_id. Poll get_article until “succeeded”, then use player_url.
| Argument | Type | Required | Description |
|---|---|---|---|
publication |
string | yes | The publication id (pblc_…) from list_publications. |
url |
string or null | no | Public http(s) URL to fetch and narrate; required unless title and text are given. |
title |
string or null | no | Article headline (with text, skips fetching the url). |
text |
string or null | no | Plain-text article body, up to 200,000 characters; same inline tags as create_speech. |
audio_url |
string or null | no | https URL of your own MP3/WAV to publish instead of narrating; title required. |
authors |
array or null | no | Author names, read aloud as the byline. |
summary |
string or null | no | Short description shown on the player. |
category |
string or null | no | Section/category name. |
tags |
array or null | no | Extra tags. |
art_url |
string or null | no | Cover image URL. |
published_at |
string or null | no | ISO 8601 publication time; defaults to now. |
voice |
string or integer or null | no | Narrator voice id from list_voices. |
guest_voice |
string or integer or null | no | Second voice, used when podcast_mode is on. |
language |
string or null | no | Language tag to narrate in (e.g. “en-US”), skipping detection. |
podcast_mode |
boolean or null | no | Two-voice conversational narration. |
music |
boolean or null | no | Mix the publication’s intro/outro beds in; default true. |
intro_music |
string or null | no | Intro bed: a music bed id, “shuffle” for a random one, or “” for none. |
outro_music |
string or null | no | Outro bed: a music bed id, “shuffle” for a random one, or “” for none. |
intro_music_duration |
number or null | no | Seconds of intro bed before the narration. |
outro_music_duration |
number or null | no | Seconds of outro bed after the narration. |
intro_music_pad |
number or null | no | Seconds the intro bed overlaps the narration. |
outro_music_pad |
number or null | no | Seconds the outro bed overlaps the narration. |
audio_polish |
boolean or null | no | Rewrite the text for the ear before narrating. |
audio_polish_level |
string or integer or null | no | How far Audio Polish may rewrite: “0” (symbols only) to “3”. Default “2”. |
read_urls |
boolean or null | no | Read URLs aloud. |
read_alt_text |
boolean or null | no | Read image alt text aloud. |
read_author |
boolean or null | no | Read the byline aloud. |
custom_byline |
string or null | no | Exact byline text to read instead of the generated one. |
disclaimer |
string or null | no | Spoken AI disclaimer. “” narrates none; null uses the publication default. |
disclaimer_voice |
string or integer or null | no | Voice that reads the spoken disclaimer. |
ui_disclaimer |
boolean or null | no | Show the AI disclosure line under the player. |
regenerate |
boolean or null | no | Re-narrate even if an article for this url exists, keeping its last settings unless overridden here. |
callback_url |
string or null | no | https URL to POST the finished article to. |
metadata |
object or null | no | Up to 8 string values, 512 bytes total. |
idempotency_key |
string or null | no | Any unique string; a resubmit returns the original article job. |
mix |
boolean or null | no | Alias of music. |
intro_mixable |
string or null | no | Alias of intro_music. |
outro_mixable |
string or null | no | Alias of outro_music. |
sonic_optimizer |
boolean or null | no | Alias of audio_polish. |
conversation_mode |
boolean or null | no | Alias of podcast_mode. |
get_article
Fetch an article by its artl_ id, or by the job_id create_article returned while the id is still null. Returns its status, and player_url once succeeded.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | no | The article id (artl_…); any article in this Studio, whatever created it. |
job_id |
string | no | The conversion job id (ajob_…); use it while id is still null. |
idempotency_key |
string | no | Instead of id: the idempotency_key the article was created with. |
list_articles
List this account’s articles, newest first - API jobs plus everything in Studio (widget, WordPress, RSS, uploads). Page with starting_after.
| Argument | Type | Required | Description |
|---|---|---|---|
limit |
integer | no | Page size (default 20). |
starting_after |
string | no | Return articles older than this id (artl_… or a job_id). |
status |
enum: queued, processing, succeeded, failed, deleted | no | Filter by status. |
publication |
string | no | Filter by publication id. |
source |
enum: all, api, studio | no | all (default), api (jobs from this API) or studio (Studio only). |
q |
string | no | Match on id, job id, url, title, error code or metadata. |
ids |
array | no | Only these article or job ids, up to 50. |
update_article
Change an article’s title, summary, authors, tags, cover image, publish time or visibility without re-narrating. privacy “unlisted” hides it from playlists and feeds.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The article id (artl_…) or job_id (ajob_…). |
title |
string or null | no | Article headline. |
summary |
string or null | no | Short description shown on the player. |
authors |
array or null | no | Replaces the article’s byline. |
tags |
array or null | no | Replaces the article’s tags. |
privacy |
enum: public, unlisted, | no | “unlisted” hides it from playlists, feeds and lists; the player keeps working. |
published_at |
string or null | no | ISO 8601 publish time; a future time schedules the article. |
art_url |
string or null | no | Cover image URL. |
metadata |
object or null | no | Up to 8 string values, 512 bytes total; API-created articles only. |
delete_article
Delete an article: cancels it if still queued/processing, otherwise takes the player and embed down. The job record stays with status “deleted”.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The article id (artl_…) or a job_id (ajob_…). |
list_publications
Publications this key can publish into, with each one’s site URL and default voice. Use the id as create_article’s publication.
No arguments.
get_publication
One publication and the defaults every article in it inherits: voice and disclaimer per language, per-author voices, music beds, Audio Polish and the player.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The publication id (pblc_…) from list_publications. |
update_publication
Change a publication’s defaults; only the fields you send change, and voices / ui_disclaimers merge language by language. To change one article only, use create_article.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The publication id (pblc_…). |
language |
string or null | no | Language tag to narrate in, skipping detection; one of the voices keys. “” turns it off. |
music |
boolean or null | no | Play the beds; shorthand for turning intro_music and outro_music off or on. |
intro_music |
string or null | no | Intro bed: a track id, “shuffle” for a random one, or “” for none. |
outro_music |
string or null | no | Outro bed: a track id, “shuffle” for a random one, or “” for none. |
intro_music_duration |
number or null | no | Seconds of intro bed before the narration. |
outro_music_duration |
number or null | no | Seconds of outro bed after the narration. |
intro_music_pad |
number or null | no | Seconds the intro bed overlaps the narration. |
outro_music_pad |
number or null | no | Seconds the outro bed overlaps the narration. |
audio_polish |
boolean or null | no | Rewrite the text for the ear before narrating. |
audio_polish_level |
string or integer or null | no | How far Audio Polish may rewrite. One of 0, 1, 2, 3. |
podcast_mode |
boolean or null | no | Two-voice conversational narration. |
read_urls |
boolean or null | no | Read URLs aloud. |
read_alt_text |
boolean or null | no | Read image alt text aloud. |
read_author |
boolean or null | no | Read the byline aloud. |
voices |
object or null | no | Per language tag (e.g. “en-US”): narrator, guest voice, spoken disclaimer. null removes a language. |
author_voices |
array or null | no | Voice per author name; beats the per-language voice. [] clears the list. |
ui_disclaimers |
object or null | no | Per language tag: the AI disclosure line under the player. “” shows none; null drops it. |
pauses |
object or null | no | Seconds of silence after each meta field: a number or a [min, max] pair. “” restores the default. |
player |
object or null | no | How the embedded player looks. |
studio |
object or null | no | Studio-only preferences; no effect on published audio. |
sharing |
object or null | no | The player share button, and which URL it shares. |
email_capture |
object or null | no | Asking listeners for an email address. |
extraction |
object or null | no | How the Auto Audio widget reads a page; only needed when extraction gets it wrong. |
widget |
object or null | no | How the widget behaves: which hosts it runs on, and when it narrates a page. |
paywall |
object or null | no | Paywall gating, and the call to action readers see. |
list_mixables
Intro/outro music beds this account may use. Use the id as intro_music / outro_music on create_article or update_publication.
| Argument | Type | Required | Description |
|---|---|---|---|
kind |
string | no | Filter by kind as reported on each row. |
q |
string | no | Free-text match on name or tags. |
limit |
integer | no | Page size (default 100). |
starting_after |
string | no | Return beds with an id greater than this one. |
create_mixable
Add a music bed of your own from an https url (25 MB max; mp3/wav/m4a/ogg/flac), usable at once as intro_music / outro_music.
| Argument | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Display name of the bed, up to 120 characters. |
url |
string | yes | https URL of the audio file to fetch (mp3, wav, m4a, ogg or flac; 25 MB max). |
kind |
enum: intro-outro, sfx, ambient | no | Track kind; “intro-outro” is the default. |
tags |
object | no | Optional descriptive tags, each an array of up to 20 short strings. |
static_duration |
boolean | no | Play the track in full instead of trimming it to the configured seconds. |
publications |
array | no | Publication ids this bed is limited to; omit for the whole account. |
delete_mixable
Delete one of your own music beds and clear it from any publication using it. Everlit library tracks cannot be deleted.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The music bed id from list_mixables. |
list_playlists
Playlists in this account: name, tag rule, publications, player embed and RSS feed_url. Use the id with get_playlist / add_to_playlist.
No arguments.
get_playlist
One playlist with its pins and resolved tracks (the pins plus any filled in by its tag rule).
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The playlist id (plist_…). |
create_playlist
Create a playlist. Pin articles with articles, and/or give tags so new articles carrying them fill it, newest first.
| Argument | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Playlist name. |
description |
string or null | no | Shown on the player and in the feed. |
publications |
array or null | no | Restrict to these publication ids (pblc_…). |
tags |
array or null | no | Fill rule: articles carrying any of these tags. |
articles |
array or null | no | Articles to pin: article ids (artl_…) or job ids (ajob_…). |
add_to_playlist
Pin a published article to a playlist by artl_ id, or by the job_id create_article returned. It must have finished publishing.
| Argument | Type | Required | Description |
|---|---|---|---|
playlist |
string | yes | The playlist id (plist_…). |
article |
string | yes | article id (artl_…) or job_id (ajob_…). |
remove_from_playlist
Unpin an article from a playlist. Articles the tag rule filled in are not pins and cannot be removed this way.
| Argument | Type | Required | Description |
|---|---|---|---|
playlist |
string | yes | The playlist id (plist_…). |
article |
string | yes | article id (artl_…) or job_id (ajob_…). |
update_playlist
Change a playlist’s rules: tags/match/ignore_tags fill it, within limits it by publish date, size caps it, order sorts it, fill false leaves only pins.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The playlist id (plist_…). |
name |
string or null | no | Playlist name. |
description |
string or null | no | Shown on the player and in the feed. |
tags |
array or null | no | Fill rule: articles carrying these tags. |
match |
enum: any, all, | no | Match any of the tags (default) or all of them. |
ignore_tags |
array or null | no | Exclude articles carrying any of these tags. |
size |
integer or null | no | How many tracks the playlist holds, 1-100. |
order |
enum: played, viewed, latest, oldest, random, | no | Track order: most played, most viewed, newest, oldest, or random. |
within |
enum: any, current_day, current_week, current_month, last_day, last_week, last_month, custom, | no | Only articles published in this window. |
within_start |
string or null | no | Start of the window when within is “custom”. |
within_end |
string or null | no | End of the window when within is “custom”. |
publications |
array or null | no | Restrict to these publication ids (pblc_…). |
fill |
boolean or null | no | false: pinned articles only, no tag rule. |
delete_playlist
Delete a playlist: its player, page and podcast feed come down. The articles in it are NOT deleted.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The playlist id (plist_…). |
move_in_playlist
Move a pinned article: give before, the article it should sit in front of (null moves it last), or position. Only pins can be moved.
| Argument | Type | Required | Description |
|---|---|---|---|
playlist |
string | yes | The playlist id (plist_…). |
article |
string | yes | The article to move: article id (artl_…) or job_id (ajob_…). |
before |
string or null | no | Put it in front of this article (artl_…/ajob_…); null moves it last. |
position |
integer or null | no | 1-based position among the pins. |
get_feed
The podcast feed on a playlist: its feed_url, whether it is active, the metadata, what validation is missing, and the directories submitted to.
| Argument | Type | Required | Description |
|---|---|---|---|
playlist |
string | yes | The playlist id (plist_…). |
update_feed
Set the podcast metadata on a playlist’s feed; “” clears a field, null leaves it alone. activate_feed needs title, description, owner_email, category, artwork_url, language.
| Argument | Type | Required | Description |
|---|---|---|---|
playlist |
string | yes | The playlist id (plist_…). |
title |
string or null | no | Show title. |
description |
string or null | no | Show description, as directories display it. |
subtitle |
string or null | no | One-line subtitle. |
summary |
string or null | no | Longer summary. |
artwork_url |
string or null | no | https URL of the cover art (square, 1400-3000px). |
website_url |
string or null | no | The show’s web page. |
author |
string or null | no | Author shown in directories. |
owner_name |
string or null | no | Feed owner name. |
owner_email |
string or null | no | Feed owner email — directories send verification here. |
category |
string or null | no | Apple podcast category, e.g. “News”. |
subcategory |
string or null | no | Apple podcast subcategory. |
language |
string or null | no | Language code, e.g. “en”. |
explicit |
boolean or null | no | Explicit content flag. |
keywords |
array or null | no | Keywords for discovery. |
copyright |
string or null | no | Copyright line. |
type |
enum: episodic, serial, | no | episodic (newest first) or serial. |
complete |
boolean or null | no | true: the show is finished, no more episodes. |
locked |
boolean or null | no | true: block other platforms from importing the feed. |
funding_url |
string or null | no | Support/donation link. |
funding_text |
string or null | no | Label for the funding link. |
activate_feed
Switch the playlist’s podcast feed on, filling blanks from the playlist. 400 feed_incomplete names the fields still missing (usually artwork_url): set them with update_feed, then retry.
| Argument | Type | Required | Description |
|---|---|---|---|
playlist |
string | yes | The playlist id (plist_…). |
deactivate_feed
Stop publishing the playlist’s podcast feed; the metadata is kept, so activate_feed turns it back on.
| Argument | Type | Required | Description |
|---|---|---|---|
playlist |
string | yes | The playlist id (plist_…). |
list_feed_platforms
Where the podcast feed stands on each directory: status, the submission_url to hand the user, and platform_url once live.
| Argument | Type | Required | Description |
|---|---|---|---|
playlist |
string | yes | The playlist id (plist_…). |
mark_feed_submitted
Record that the show was submitted to a directory. Submits nothing itself: a person signs in at the submission_url from list_feed_platforms.
| Argument | Type | Required | Description |
|---|---|---|---|
playlist |
string | yes | The playlist id (plist_…). |
platform |
enum: apple, spotify, pocketcasts, amazon, youtube, overcast | yes | The directory. |
notes |
string or null | no | What the user reported, e.g. the account they submitted from. |
platform_url |
string or null | no | The show’s page on the directory, if it is already known. |
platform_podcast_id |
string or null | no | The directory’s own id for the show, if it gave one. |
mark_feed_live
Record that a directory approved the show and it is now listed; platform_url is where listeners get sent.
| Argument | Type | Required | Description |
|---|---|---|---|
playlist |
string | yes | The playlist id (plist_…). |
platform |
enum: apple, spotify, pocketcasts, amazon, youtube, overcast | yes | The directory. |
platform_url |
string or null | no | The show’s page on the directory. |
platform_podcast_id |
string or null | no | The directory’s own id for the show. |
reset_feed_platform
Forget what was recorded for one directory: it goes back to not_started, losing its notes and platform_url. Nothing is withdrawn at the directory.
| Argument | Type | Required | Description |
|---|---|---|---|
playlist |
string | yes | The playlist id (plist_…). |
platform |
enum: apple, spotify, pocketcasts, amazon, youtube, overcast | yes | The directory. |
ping_feed
Tell Overcast the feed changed so it refreshes now; other directories poll on their own schedule.
| Argument | Type | Required | Description |
|---|---|---|---|
playlist |
string | yes | The playlist id (plist_…). |
list_ingestion_feeds
The RSS/Atom feeds this account narrates automatically: the publication each feeds, its poll interval and status, last poll, and item counts.
| Argument | Type | Required | Description |
|---|---|---|---|
publication |
string or null | no | Only feeds on this publication (pblc_…). |
create_ingestion_feed
Narrate every new item of an RSS/Atom feed into a publication, polled every poll_interval_minutes. Each narration uses an article quota.
| Argument | Type | Required | Description |
|---|---|---|---|
publication |
string | yes | The publication the items are published into (pblc_…). |
url |
string | yes | The RSS or Atom feed address (http(s)). |
name |
string or null | no | A label for the feed (<= 120 characters). |
poll_interval_minutes |
integer or null | no | How often to check, 5 to 1440 minutes. Default 15. |
max_items |
integer or null | no | Most items to narrate in one poll, 1 to 500. Null = no cap. |
oldest_date |
string or null | no | ISO 8601: skip items published before this. Null = the whole feed. |
status |
enum: active, paused | no | Start polling (“active”, the default) or hold it (“paused”). |
poll_now |
boolean or null | no | Poll immediately as well as on the interval. Default true. |
get_ingestion_feed
One ingestion feed: its publication, poll interval, status, last poll, last error and item counts.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The ingestion feed id (rsif_…). |
update_ingestion_feed
Change a feed’s name, poll interval, item cap or date floor, or pause and resume it. The url cannot be changed - delete the feed and create another.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The ingestion feed id (rsif_…). |
name |
string or null | no | A label for the feed (<= 120 characters). |
poll_interval_minutes |
integer or null | no | How often to check, 5 to 1440 minutes. |
max_items |
integer or null | no | Most items to narrate in one poll, 1 to 500; null removes the cap. |
oldest_date |
string or null | no | ISO 8601 floor on item publish dates; null removes it. |
status |
enum: active, paused | no | Resume (“active”) or hold (“paused”) the feed. |
delete_ingestion_feed
Stop ingesting a feed and delete it with the record of items it has seen. Articles already narrated are NOT deleted.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The ingestion feed id (rsif_…). |
poll_ingestion_feed
Check a feed now rather than waiting for its interval, one manual poll a minute. New items are narrated in the background; read list_ingestion_items.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The ingestion feed id (rsif_…). |
list_ingestion_items
What a feed has seen, newest first: each item’s title, link, status and the article it became, or the error that stopped it.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The ingestion feed id (rsif_…). |
status |
enum: pending, processing, completed, error | no | Only items in this state. |
limit |
integer or null | no | 1 to 100; default 50. |
starting_after |
string or null | no | Item id (rsii_…) to page after. |
list_pronunciations
The account’s pronunciation library: how each word is read aloud, account-wide (publication null) or for one publication.
| Argument | Type | Required | Description |
|---|---|---|---|
publication |
string | no | Only rules that apply to this publication (its own plus account-wide). |
set_pronunciation
Add or update how a word is read aloud in every future narration; spell say the way it sounds. Scope it with publication, else account-wide.
| Argument | Type | Required | Description |
|---|---|---|---|
word |
string | yes | The text as written (up to 120 characters). |
say |
string | yes | How to read it, spelled phonetically (up to 300 characters). |
exact_match |
boolean or null | no | Match the word only as a whole word (default false: also inside longer words). |
publication |
string or null | no | Publication id (pblc_…) to scope the rule to; omit for account-wide. |
delete_pronunciation
Remove a pronunciation rule (the exact word and scope it was set with).
| Argument | Type | Required | Description |
|---|---|---|---|
word |
string | yes | The word as it was set. |
publication |
string or null | no | The publication the rule was scoped to; omit for an account-wide rule. |
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 |
| 400 | invalid_playlist |
Studio rejected the playlist (see message) |
| 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 |
| 403 | terms_not_accepted |
The publisher has not accepted the current Everlit Terms of Service. next_step is accept_terms; resolution_url is the acceptance page. One member accepts once; access resumes immediately. |
| 404 | publication_not_found |
Not on your account (or outside this key’s publication scope) |
| 404 | playlist_not_found / pronunciation_not_found |
No playlist/pronunciation at that id or word |
| 404 | article_not_found / voice_not_found / mixable_not_found / unknown_endpoint |
Nothing at that address |
| 409 | job_not_complete |
A job_id named as article has not published yet |
| 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) |
| 429 | concurrency_limit_exceeded |
As many conversions running as your account has publications; wait for one to finish (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.8.0 — 2026-09-11
- Inline markup in article
text:POST /v1/articlesandcreate_articleaccept the same[everlit-pause:N],[everlit-mixable:N],[everlit-mixable-background:N]and[everlit-subvoice-open:N]…[everlit-subvoice-close]tags as the TTS API. Every id is checked against the account, tags are never counted incharacters, the article resource reports them asmarkup, and anaudio_urlarticle’stextis left untouched. See Inline markup. - Podcast directories:
GET /v1/playlists/{id}/feed/platformslists where the show stands on Apple, Spotify, Pocket Casts, Amazon, YouTube and Overcast (with thesubmission_urlto hand a person);POST .../platforms/{platform}/submittedand.../liverecord a submission and a listing;DELETE .../platforms/{platform}resets one;POST .../feed/pingasks Overcast to re-crawl. The feed resource’sdistributionsrows carry the same fields. MCP:list_feed_platforms,mark_feed_submitted,mark_feed_live,reset_feed_platform,ping_feed. See Directories. - Ingestion feeds:
GET/POST /v1/ingestion-feeds,GET/PATCH/DELETE /v1/ingestion-feeds/{id},POST .../pollandGET .../items— point a publication at an RSS or Atom feed and every item it publishes is narrated with that publication’s defaults.poll_interval_minutes(5–1440),max_itemsandoldest_datedecide what is picked up; each narration counts against the plan’s article quota. MCP:list_ingestion_feeds,create_ingestion_feed,get_ingestion_feed,update_ingestion_feed,delete_ingestion_feed,poll_ingestion_feed,list_ingestion_items. See Ingestion feeds.
1.7.0 — 2026-09-11
PATCH /v1/articles/{id}and theupdate_articleMCP tool: change an article’s title, summary, tags, cover image, publish time or privacy without re-narrating it — for every article in the account’s Studio.privacy: "unlisted"keeps the player working but hides the article from playlists, feeds and lists; a futurepublished_atschedules it (unlisted until then) unless the same request setsprivacy. See Update article.audio_urlonPOST /v1/articles/create_article: publish your own recording instead of narrating one. The file is fetched, transcribed for read-along, mixed with the publication’s intro/outro beds (or the ones you send) and published as an ordinary audio article.titleis required,textoptional (the transcript fills it in); narration parameters are rejected. Such an article reportscharactersandvoiceasnull, narrates no spoken AI disclaimer and carries the AI disclosure as false in its ID3 tags.- Playlists:
PATCH /v1/playlists/{id}(name, description,tags,match,ignore_tags,size,order,within+within_start/within_end,publications,fill),DELETE /v1/playlists/{id}, andPATCH /v1/playlists/{id}/articles/{article}to reorder a pin (beforeorposition). The playlist resource reportsignore_tags,within,within_start,within_endandfill. MCP:update_playlist,delete_playlist,move_in_playlist. - Podcast feeds:
GET/PATCH /v1/playlists/{id}/feedandPOST /v1/playlists/{id}/feed/activate/.../deactivate— set the show’s metadata, activate it (400 feed_incompletenames anything still missing; a description is always derived when none was written), submitfeed_urlto the directories. MCP:get_feed,update_feed,activate_feed,deactivate_feed. See Podcast feeds. - Publication settings now cover everything the Studio form does: an
extractionblock (content_selector,title_selector,date_selector,exclude_selectors,tag_field_map,site_name_tag_prefix,tag_namespaces), awidgetblock (allowed_origins,click_to_create,click_to_create_message,generate_on_first_view,rss_only), apaywallblock, andplayer.theme(fourteen named colours and the title/body web fonts, merged key by key;""restores a derived default). - Your own music beds:
POST /v1/mixables/create_mixableuploads a bed, sound effect or ambient track from anhttpsURL (25 MB max) with optionaltags,static_durationandpublications;GETandDELETE /v1/mixables/{id}/delete_mixableread or remove one you uploaded (deleting also clears it from any publication still pointing at it). Everlit’s library stays read-only (403 not_owned).
1.6.0 — 2026-09-09
GET /v1/articles/{id},DELETE /v1/articles/{id}and theget_article/delete_articleMCP tools now take an article id (artl_...) as well as a job id, so every article in the account’s Studio — the Auto Audio widget, WordPress, RSS, Studio uploads — is readable and deletable here. See Any article, any origin.GET /v1/articleslists those alongside API jobs, newest first, and takessource=all|api|studio(allis the default).starting_afterandidsaccept both id forms.- The article resource gained
source,published_at,privacy,tags,summary,authorsandcover. idis now the article id (artl_...) on every article; the conversion job moved tojob_id(ajob_...).article_idwas removed. Jobs created before this release were renamed toajob_...ids.get_articletakes ajob_idargument as an alternative toid.- MCP server
version1.9.0 → 1.12.0.
1.5.0 — 2026-09-09
- New
GET/POST /v1/playlists,GET /v1/playlists/{id},POST/DELETE /v1/playlists/{id}/articles, and thelist_playlists,get_playlist,create_playlist,add_to_playlist,remove_from_playlistMCP tools. See Playlists. - New
GET/PUT /v1/pronunciations,DELETE /v1/pronunciations/{word}, and thelist_pronunciations,set_pronunciation,delete_pronunciationMCP tools: fix how a name is read for every future narration, account-wide or per publication. See Pronunciations. - A
get_article(REST or MCP) that finds the job still pending no longer counts against the read rate limit when it waitedpoll_after_seconds(30) since the previous poll of that job, so honest polling can never lock a key out; a faster re-poll still counts. - MCP server
version1.8.0 → 1.9.0.
1.4.0 — 2026-09-08
- New
GET /v1/mixables/list_mixables: the intro/outro music bedsintro_music/outro_musicmay reference. See Music beds. get_articleacceptsidempotency_keyinstead ofid;list_articlesandGET /v1/articlesacceptids(up to 50) andidempotency_key, for crash recovery and checking on many jobs at once.- Errors carry
next_stepandresolution_url; see the TTS API changelog for the full list of codes — the same envelope is shared across both APIs. - New
GET /v1/account/get_accounton the shared MCP server: publisher, key, entitlements, billing, limits and plan — call it first on a new connection. See the TTS API guide. - MCP resources (
everlit://docs/tts-api,everlit://docs/articles-api,everlit://languages) and prompts (narrate_text,publish_from_url). - MCP server
version1.7.0 → 1.8.0.
1.3.2 — 2026-09-05
- Music beds can shuffle:
intro_music/outro_musicaccept"shuffle"onPOST /v1/articlesandPATCH /v1/publications/{id}(and through thecreate_article/update_publicationMCP tools) — a random track from the publication’s intro/outro music, picked fresh for every narration and never stored on the article. - A publication reports each bed in the same three forms it takes — an id,
"shuffle",""— so adefaultsobject alwaysPATCHes back unchanged. An intro with no track set now reads as"shuffle"rather thannull(that is what it has always played), an outro with none reads as"", andmusicisfalseonly when neither bed plays.
1.3.1 — 2026-09-04
regenerate: truenow really does keep the settings the article was last narrated with: the stored per-article disclaimer flags are carried into the re-narration, and an explicitdisclaimer(a string, or"") in the same request still wins.- MCP: every optional argument of
create_articleandupdate_publicationacceptsnull(it means “no opinion”, exactly as over HTTP), voice ids andaudio_polish_levelaccept an integer as well as a string, and the legacy aliasesmix,intro_mixable,outro_mixable,sonic_optimizerandconversation_modeare in thecreate_articleschema again. - A language configured with a blank spoken disclaimer or a blank
ui_disclaimersentry now narrates and shows none for that language, instead of falling through to another language’s line.
1.3.0 — 2026-09-04
GET /v1/publications/{id}andPATCH /v1/publications/{id}: read and change everything a publication hands its articles — voices, disclaimers and the AI disclosure line per language, per-author voices, music, Audio Polish, pauses, the player, sharing and email capture. With theget_publication/update_publicationMCP tools.- One vocabulary whether a setting is a publication default or a per-article override:
music,intro_music,outro_music,audio_polish,audio_polish_level,podcast_mode, the intro/outro duration and pad fields,languageandui_disclaimeronPOST /v1/articles.mix,intro_mixable,outro_mixable,sonic_optimizerandconversation_modekeep working as aliases. languageon an article, and a publication default language, skip detection.intro_music: ""/outro_music: ""play no bed at all — previously an unset intro let one be chosen and an unset outro fell back to the publication’s.GET /v1/publicationsnow reports each publication’s realdefault_voiceandlanguage_voices.
1.2.0 — 2026-09-04
disclaimer: ""onPOST /v1/articlessilences the spoken AI disclaimer for that article; omittingdisclaimeror sendingnulluses the publication default and leaves anything stored on the article alone. The strings"false","null","none"and"off"are rejected with400 invalid_parameterrather than narrated.
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://everlit.openstatus.dev