Text in. Studio-quality audio out.
Send text, get back a production-ready MP3 narrated by any voice in your catalog — including your own cloned voices — with word-level timestamps included. Full-file synthesis with automatic quality verification; most jobs finish in under a minute.
Quickstart
# 1. Create a speech job
curl -s -X POST "https://api.everlit.audio/v1/speech" \
-H "Authorization: Bearer api_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Hello from Everlit.", "voice": "669"}'
# => 202
# {
# "id": "spch_01jx...", "status": "queued",
# "audio_url": "https://api.everlit.audio/v1/speech/spch_01jx.../audio", ...
# }
# 2. Poll until status is "succeeded" (typically 15-60s), then download.
# audio_url is known from the moment you submit and never changes.
curl -sL -o hello.mp3 \
-H "Authorization: Bearer api_YOUR_KEY" \
"https://api.everlit.audio/v1/speech/spch_01jx.../audio"
# Need a link to send someone? The finished job also carries download_url:
# signed, no API key, plays in a browser, valid 24h — see Shareable links.
Prefer not to poll? Pass a callback_url and we POST you the finished job —
see Webhooks.
Client examples
End to end in three languages: create a job, poll every few seconds, download
the MP3. All three use only the standard HTTP client; there is no SDK to
install. Replace 669 with a voice id from GET /v1/voices.
import os, time, requests
BASE = "https://api.everlit.audio"
H = {"Authorization": f"Bearer {os.environ['EVERLIT_API_KEY']}"}
job = requests.post(f"{BASE}/v1/speech", headers=H,
json={"text": "Hello from Everlit.", "voice": "669"}).json()
while job["status"] in ("queued", "processing"):
time.sleep(3)
job = requests.get(f"{BASE}/v1/speech/{job['id']}", headers=H).json()
if job["status"] != "succeeded":
raise SystemExit(f"job failed: {job['error']}")
# audio_url answers 302 -> storage; requests follows it and drops the bearer.
with open("hello.mp3", "wb") as f:
f.write(requests.get(job["audio_url"], headers=H).content)
// Node 18+ (global fetch)
const BASE = "https://api.everlit.audio";
const H = { Authorization: `Bearer ${process.env.EVERLIT_API_KEY}` };
let res = await fetch(`${BASE}/v1/speech`, {
method: "POST",
headers: { ...H, "Content-Type": "application/json" },
body: JSON.stringify({ text: "Hello from Everlit.", voice: "669" }),
});
let job = await res.json();
while (job.status === "queued" || job.status === "processing") {
await new Promise((r) => setTimeout(r, 3000));
job = await (await fetch(`${BASE}/v1/speech/${job.id}`, { headers: H })).json();
}
if (job.status !== "succeeded") throw new Error(`job failed: ${JSON.stringify(job.error)}`);
// fetch follows the 302 and drops the bearer on the cross-origin hop.
const audio = await fetch(job.audio_url, { headers: H });
await require("fs/promises").writeFile("hello.mp3", Buffer.from(await audio.arrayBuffer()));
require "net/http"
require "json"
BASE = URI("https://api.everlit.audio")
AUTH = { "Authorization" => "Bearer #{ENV.fetch('EVERLIT_API_KEY')}" }
http = Net::HTTP.new(BASE.host, 443).tap { |h| h.use_ssl = true }
res = http.post("/v1/speech", { text: "Hello from Everlit.", voice: "669" }.to_json,
AUTH.merge("Content-Type" => "application/json"))
job = JSON.parse(res.body)
while %w[queued processing].include?(job["status"])
sleep 3
job = JSON.parse(http.get("/v1/speech/#{job['id']}", AUTH).body)
end
abort "job failed: #{job['error']}" unless job["status"] == "succeeded"
# Net::HTTP does not follow redirects: take the 302 by hand, and send the
# storage request WITHOUT the bearer (the URL is self-authenticating).
redirect = http.get(URI(job["audio_url"]).path, AUTH)
File.binwrite("hello.mp3", Net::HTTP.get(URI(redirect["location"])))
The MCP server needs none of this — an agent calls create_speech, then
get_speech until status is succeeded. See MCP.
Authentication
Every request needs an api_ key as a bearer token:
Authorization: Bearer api_...
Keys are minted in Studio under API → Keys. They are server-side
credentials — never embed one in a browser or mobile app. Speech is included
with any paid Studio plan (no per-character charge, no monthly cap;
concurrency is capped at the number of publications on your account). Without
a plan the API is pay-as-you-go: if you see tts_api_not_enabled, upgrade or
add a card under Billing → TTS API (see Billing).
The full key is shown exactly once, when it is minted. We store only a SHA-256 digest and the first 12 characters, so it cannot be retrieved later — copy it into your secret store at creation time. If a key is lost, revoke it and mint a new one; the key list identifies each one by its prefix and label.
Getting access
Everlit access is granted per organization. If you have an account, mint a key in Studio or connect an MCP client with OAuth (see MCP). If you have none — or you are an agent whose user has none — ask for one without a key:
curl -s -X POST https://api.everlit.audio/v1/access-requests \
-H "Content-Type: application/json" \
-d '{"email": "editor@example.com", "use_case": "Narrate a daily newsletter, ~2M characters a month",
"organization": "Example News", "product": "tts"}'
# => 201
# {
# "object": "access_request", "id": "acr_k3j9x2m8q1w5", "status": "pending",
# "status_url": "https://api.everlit.audio/v1/access-requests/acr_k3j9x2m8q1w5",
# "connect_url": "https://api.everlit.audio/v1/mcp",
# "next_step": "wait", "poll_after_seconds": 3600,
# "message": "Request received. Everlit reviews every request, usually within a business day, ..."
# }
email and use_case are required; name, organization,
expected_monthly_characters and client_name help the review. product is
tts (speech from text, pay-as-you-go), articles (published audio articles,
plan-based) or both (default). Submitting the same email again updates the
existing request rather than opening a second one.
GET /v1/access-requests/{id} (also key-free) reports status — pending,
approved or denied — with next_step: wait (poll no more than hourly;
you are also emailed), authorize_oauth (access is on: connect at
connect_url and log in, or mint a key in Studio; the email has both links) or
contact_support. The document never carries the requester’s email.
Both routes are limited per IP (5 requests and 30 reads a minute) and answer
429 beyond that. The same request can be made from an MCP client that has no
credentials yet: tools/list on the server offers exactly two tools,
request_access and get_access_request, with the same arguments and result;
every other tool, and initialize itself, still answers 401 until you log in
or add a key (see MCP).
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/speech |
Create a synthesis job → 202 |
POST |
/v1/audio/speech |
OpenAI-compatible synchronous speech |
POST |
/v1/speech/estimate |
Price a request and predict its duration — no job created |
GET |
/v1/speech/{id} |
Fetch a job (poll this) |
GET |
/v1/speech |
List jobs, newest first (limit, starting_after, status, q, ids, idempotency_key) |
GET |
/v1/speech/{id}/audio |
Download the audio (302 redirect) |
GET |
/v1/speech/{id}/download |
Download the audio with a signed link — no API key (shareable links) |
GET |
/v1/speech/{id}/timings |
Word-level timestamps (JSON) |
GET |
/v1/speech/{id}/captions |
Subtitles built from those timings (srt, vtt, json) |
DELETE |
/v1/speech/{id} |
Delete the audio, or cancel a running job → 204 |
GET |
/v1/voices |
Voices available to your account (limit, starting_after, language, q) |
POST |
/v1/voices |
Clone a voice from a sample recording → 201 (see Cloning a voice) |
GET |
/v1/voices/{id} |
Fetch one voice — including a clone’s review status |
DELETE |
/v1/voices/{id} |
Delete a voice you cloned, freeing a slot → 204 |
GET |
/v1/voices/{id}/preview |
Key-free demo clip of a catalog voice (302 redirect) |
GET |
/v1/mixables |
Music beds (intro/outro) your account may use |
GET |
/v1/pronunciations |
The account’s pronunciation library — rules applied to every future job (Articles guide) |
PUT |
/v1/pronunciations |
Add or update a pronunciation rule |
DELETE |
/v1/pronunciations/{word} |
Remove a pronunciation rule |
GET |
/v1/languages |
Supported languages, billing multipliers, transcript availability |
GET |
/v1/account |
Who this key is, entitlements, billing, limits and plan — call this first |
POST |
/v1/access-requests |
Ask for an Everlit account — no key (getting access) |
GET |
/v1/access-requests/{id} |
Status of an access request — no key |
GET |
/v1/usage |
Usage aggregates |
POST |
/v1/mcp |
MCP server (Model Context Protocol) for AI agents — see MCP |
Account
GET /v1/account is the first call to make with a new key: it needs no TTS
entitlement, so it always answers and tells you what state the key is in.
{
"object": "account",
"publisher": { "id": "pbls_abc123", "name": "Daily Gazette" },
"api_key": { "id": 42, "name": "Production", "oauth": false, "expires_at": null, "publications": [] },
"entitlements": { "tts_api": true, "tts_reason": "payg", "articles_api": true },
"billing": {
"mode": "payg",
"included": false,
"included_concurrent_jobs": null,
"extra_capacity": null,
"price_cents_per_million": 800,
"balance_cents": 1250.0,
"estimated_characters_remaining": 1562500,
"billing_url": "https://studio.everlit.audio/pbls_abc123/billing/api"
},
"limits": { "requests_per_minute": 60, "reads_per_minute": 300, "max_concurrent_jobs": 4, "max_characters_per_request": 20000, "max_voice_clones": 10 },
"usage": { "month_to_date_characters": 812340, "voice_clones": 2 },
"plan": { "tier": "payg", "publications": null, "articles_per_month": null, "max_concurrent_articles": 3 },
"server": {
"version": "1.8.0",
"docs_url": "https://everlit.audio/docs/tts-api",
"status_url": "https://everlit.openstatus.dev",
"request_access_url": "https://everlit.audio/api#request-key"
}
}
billing.mode is one of plan (included with your paid Studio plan, never
charged per character), payg (a card or prepaid balance), comped (a staff
flag, never charged) or none (no TTS entitlement at all); billing.included
is true for plan and comped.
billing.estimated_characters_remaining is your balance divided by the
current price — a rough budget for Latin-script text; non-Latin scripts bill
at a higher multiplier (see Billing), so it runs out sooner for
them. When entitlements.tts_api is false, every POST /v1/speech answers
403 tts_api_not_enabled; billing.billing_url is where to upgrade your
plan, add a card or ask about a comp. limits are the same object GET
/v1/usage returns — the rates
and caps this key is actually held to, including max_voice_clones, how many
cloned voices the account may hold at once (default 10);
usage.voice_clones is how many it holds today. plan are quotas that live
on your Everlit plan rather than the TTS API (articles_per_month and the plan’s
publications cap).
billing.included_concurrent_jobs / billing.extra_capacity are set only
for plan accounts (see Included with a Studio plan).
Create speech
POST /v1/speech
| Parameter | Type | Required | Description |
|---|---|---|---|
text |
string | yes | Text to narrate, up to your plan’s per-request cap. HTML is stripped. 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. |
voice |
string | yes | A voice id from GET /v1/voices. |
language |
string | no | ISO 639-1 hint (e.g. "en", "es") — see GET /v1/languages. Defaults to auto-detection. Never lowers the billing rate (it only disambiguates Japanese vs Chinese readings of Han characters). |
format |
string | no | "mp3" (default, 44.1 kHz), "wav" (16-bit 44.1 kHz) or "opus" (Ogg container, 48 kHz). |
bitrate |
integer | no | kbps. mp3: 64, 96, 128 (default), 192, 256, 320; opus: 32, 48, 64 (default), 96, 128. Rejected for wav, which is uncompressed. |
speed |
number | no | Playback speed 0.5–2.0 (default 1.0), applied after synthesis; word timings are scaled to match. |
paragraph_pause |
number | no | Seconds of silence (0–5) at every blank-line paragraph break. Omit for our natural 0.3–0.5 s. |
pronunciations |
object | no | Up to 50 {"written": "spoken"} pairs applied to this request only. |
callback_url |
string | no | https:// URL to POST the finished job to. |
metadata |
object | no | Up to 8 string values (512 bytes total), echoed back on the job. |
Unknown parameters are rejected with 400 unknown_parameter — the API is
strict so that future additions are never breaking.
The job resource
Returned by every endpoint and delivered to your callback — one shape:
{
"object": "speech",
"id": "spch_01jx...",
"status": "succeeded",
"created_at": "2026-08-07T14:00:00Z",
"completed_at": "2026-08-07T14:00:41Z",
"expires_at": "2026-09-06T14:00:00Z",
"voice": "669",
"voice_name": "Lana Clark",
"language": "en",
"format": "mp3",
"audio_url": "https://api.everlit.audio/v1/speech/spch_01jx.../audio",
"download_url": "https://api.everlit.audio/v1/speech/spch_01jx.../download?exp=1757462400&sig=9f0c...",
"timings_available": true,
"usage": { "characters": 812, "billable_characters": 812, "audio_seconds": 58.3 },
"audio": { "content_type": "audio/mpeg", "bytes": 934218, "duration_seconds": 58.3 },
"error": null,
"options": { "speed": 1.0, "bitrate": 128, "paragraph_pause": null },
"metadata": { "ref": "article-4512" }
}
voice_name is the display name of voice at the time of the read, for
logs and UIs; voice is the id to store.
status is one of queued, processing, succeeded, failed, expired,
deleted. Poll every 3–5 seconds; a typical job completes in 15–60 seconds,
long texts in a few minutes.
Pacing, pauses and pronunciation
Four controls shape how a narration is delivered; all of them are per request and none of them change what you are billed.
speed (0.5–2.0) is applied after synthesis, as a tempo change that
leaves pitch alone — the voice does not turn into a chipmunk at 1.5. Word
timings are scaled by the same factor, so /timings and /captions still
line up with the audio you get back. estimated_duration_seconds from
POST /v1/speech/estimate accounts for it too.
paragraph_pause (0–5 seconds) sets the silence at every blank-line
paragraph break. Leave it out and we use a natural 0.3–0.5 s; set 0 to run
paragraphs together, or 1.5 for a documentary-style beat between sections.
[everlit-pause:N] tags put a pause exactly where you want one, anywhere
in the text: The verdict is in. [everlit-pause:2] Not guilty. N is
seconds, 0.1 to 10. The tag is not spoken, but its characters are counted in
usage.characters (not in billable_characters). See
Inline markup for the rest of the family.
pronunciations fixes a name or an acronym for this request only:
{
"text": "Everlit publishes with Kagi.",
"voice": "669",
"pronunciations": { "Everlit": "ever lit", "Kagi": "kah gee" }
}
Matching is whole-word and case-insensitive; up to 50 pairs, keys up to 64
characters. Spell the value the way it sounds — this is a respelling, not
IPA. Nothing is stored: the map applies to the one job and is not echoed back
on the resource (only options.pronunciations_count is).
For a fix that should apply to every future narration on the account, use
the pronunciation library instead — PUT /v1/pronunciations (or the
set_pronunciation MCP tool). Those rules are permanent, can be scoped to one
publication, and are applied to every job we run for you afterwards; they do
not change audio that already exists. Use the per-request map for a one-off,
the library for a name you say all the time.
Inline markup
Everlit’s own bracket markup — the tags Studio and the WordPress plugin write
into article text — works in text too, so copy from either and the pauses,
music and voice changes come with it. Tags are matched case-insensitively;
the documented form is lowercase. Every id is checked against your account
before the job is created, so a tag can never reach a voice or a bed that is
not yours.
| Tag | What it does |
|---|---|
[everlit-pause:N] |
A pause of N seconds, 0.1–10. |
[everlit-mixable:N] |
Plays music bed N in full at this point, between the words around it — a sting, a bumper, a transition. |
[everlit-mixable-background:N] |
Plays bed N under the text that follows, ducked beneath the voice, until the next background tag or the end. |
[everlit-subvoice-open:N] … [everlit-subvoice-close] |
Narrates the enclosed passage in voice N instead of the job’s voice — a quote, a second speaker, a dialogue line. |
N for a mixable is an id from GET /v1/mixables (your own beds and
Everlit’s library); for a subvoice it is any voice id you could pass as
voice, cloned voices included. Subvoice blocks cannot nest and must balance.
{
"voice": "669",
"text": "[everlit-mixable:1201] Welcome to the Monday briefing. [everlit-mixable-background:1188] First, the markets.\n\n[everlit-subvoice-open:704]Rates were left unchanged, the chair said.[everlit-subvoice-close] Analysts had expected as much. [everlit-pause:1] Next, the weather."
}
What a job with markup reports:
usage.characterscounts the tag text;billable_charactersdoes not.usage.audio_secondsandaudio.duration_secondsinclude the music. A foreground bed adds its full length; a background bed adds nothing.POST /v1/speech/estimatepredicts the spoken text plus explicit pauses from the primary voice’s pace; it does not add mixable length, and a subvoice passage is estimated at the primary voice’s pace.timings_availablefollows the voices involved: a subvoice on a provider without transcripts makes the whole job’s timings unavailable.- The job’s voice pace is not measured from jobs that carry mixables or subvoices (see Estimating cost and duration).
Errors: an id that is not an integer, a nested subvoice, or an unmatched
open/close is 400 invalid_markup. A bed that exists but is not yours is
403 mixable_not_accessible, an unknown one 404 mixable_not_found. A
subvoice id gets the same checks as voice (voice_not_found,
voice_not_accessible, voice_removed), all with param: "text".
[Everlit-Sponsored:N] and any tag not listed above are stripped.
Downloading audio
audio_url is deterministic and stable: it is returned the moment you
create the job, never changes, and works for the full 30-day retention
window. Authenticate with the same bearer key; the endpoint replies with a
302 redirect to short-lived storage, so curl -L and every HTTP client
follow it transparently. There are no expiring links to manage or cache. audio.content_type and the
download filename follow the job’s format: audio/mpeg (.mp3),
audio/wav (.wav) or audio/ogg (.ogg, for opus).
While the job is still running the endpoint returns 409 job_not_complete;
after retention or deletion it returns 410 (audio_expired /
audio_deleted).
Retention: audio and timings are kept for 30 days from creation, then
deleted automatically. The job record itself survives (status expired) so
you can always reconcile usage against your bill. Download promptly or call
DELETE /v1/speech/{id} when you’re done — deletion removes the audio but
never the usage record.
Cancelling: DELETE on a job that is still queued or processing
cancels it and no audio is produced. A job cancelled while queued is not
billed and its characters are released from your usage; a job cancelled
while processing is billed in full — synthesis had already started.
Either way the job answers 204, is idempotent, and afterwards reads as
status: "deleted".
Loudness: program loudness is typically around −16 LUFS but varies
slightly by voice; apply your own loudnorm downstream if you need broadcast
compliance. Music placed with inline markup is mixed and
ducked under the voice the way it is in an audio article.
Shareable links
download_url is the same audio behind a signed link that needs no API
key — the URL a person can click, or an agent can hand to one. It appears on
the job as soon as there is audio to fetch (null before that, and after
deletion or retention), and each one is valid for 24 hours from the moment
it was returned:
GET /v1/speech/{id}/download?exp=1757462400&sig=9f0c...
The signature is bound to that job id and that expiry, so the link cannot be
edited into a link for another job or a longer life. It 302-redirects to the
audio with Content-Type: audio/mpeg and an inline disposition, so a browser
plays it in a tab.
Links are minted fresh on every read, so an expired one is one call away
from fixed: GET /v1/speech/{id} (or the get_speech MCP tool) hands back a
new 24-hour link. Don’t store download_url as an identifier — store the job
id, or audio_url, which never changes. An invalid or expired link answers
403 invalid_download_link.
Treat a download_url like a password to that one recording for a day:
anyone holding it can play the audio. The proof travels in the query string,
which means a link is also written into browser history and into the request
logs of anything it passes through, so pass one around the way you would pass
around a 24-hour bearer token for that single file. If you need something you
can publish permanently, host your own copy of the bytes.
Word timings
GET /v1/speech/{id}/timings — included with almost every job at no extra
cost (timings_available on the resource tells you):
{
"object": "speech.timings",
"id": "spch_01jx...",
"duration_seconds": 58.3,
"words": [
{ "word": "Hello", "start": 0.0, "end": 0.31 },
{ "word": "from", "start": 0.38, "end": 0.55 }
]
}
Times are seconds from the start of the audio file. Use them for karaoke-style read-along highlighting, subtitles, or jumping playback to a sentence.
The word list is your submitted text, verbatim — words are taken from the text you sent (whitespace-tokenized, punctuation preserved), never from a speech recognizer’s guess at it. Spoken expansions still map cleanly: “$5” is one entry whose span covers the spoken “five dollars”.
Estimating cost and duration
POST /v1/speech/estimate answers the two questions you get asked before you
spend anything: what will this cost, and how long will the audio be. It takes
the same body as POST /v1/speech (only text, voice, language and
speed matter) and runs the same validation, so a text or voice that would be
rejected there is rejected here:
curl -sS https://api.everlit.audio/v1/speech/estimate \
-H "Authorization: Bearer $EVERLIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"Good morning. Here is the news.","voice":"669"}'
{
"object": "speech_estimate",
"voice": "669",
"language": null,
"characters": 31,
"billable_characters": 31,
"price_cents": 0.0248,
"price": "$0.0002",
"billing_mode": "payg",
"estimated_duration_seconds": 1.8,
"explicit_pause_seconds": 0.0,
"duration_basis": "voice_pacing",
"speed": 1.0,
"paragraph_pause": null
}
billing_mode mirrors GET /v1/account’s billing.mode — plan and
comped always price at 0; payg prices from the published rate.
duration_basis says where the number came from. voice_pacing means we have
a measured speaking pace for that voice — the median characters per second
across its recent successful jobs, or, for a voice with no history yet, a short
neutral calibration passage we synthesize once. catalog_default means we have
no measurement and used the catalog average, so treat the duration as a rough
figure. The pace is on the voice itself as pacing.characters_per_second (see
Voices).
The voice’s pace already includes the natural sentence and paragraph pauses
every narration gets. Pauses you ask for on top are added separately and
reported as explicit_pause_seconds: every [everlit-pause:N] tag in the text, plus
paragraph_pause (pass it to the estimate exactly as you will pass it to
POST /v1/speech) at each blank-line paragraph break. Both are divided by
speed, like the rest of the file. Jobs that carry explicit pauses or a
speed other than 1.0 are left out when a voice’s pace is measured, so the
measurement stays a clean sample of how that voice reads prose.
The estimate creates no job: nothing is queued, nothing is billed, and no
quota is reserved. It is metered as a read (the reads_per_minute bucket), so
it is cheap to call before every job.
Captions
GET /v1/speech/{id}/captions turns the job’s word timings into subtitles.
It needs timings_available: true on the job — otherwise it answers
404 timings_not_available, exactly like /timings.
| Parameter | Type | Default | Description |
|---|---|---|---|
format |
string | srt |
srt (SubRip), vtt (WebVTT), or json (cue objects). |
max_chars |
integer | 42 |
Characters per line, 10–120. |
max_lines |
integer | 2 |
Lines per cue, 1–3. |
max_seconds |
number | 5 |
Longest a cue may stay on screen, 1–15. |
curl -sS "https://api.everlit.audio/v1/speech/$ID/captions?format=vtt" \
-H "Authorization: Bearer $EVERLIT_API_KEY" -o narration.vtt
srt is served as application/x-subrip and vtt as text/vtt, both with an
inline Content-Disposition naming the file after the job id. json returns
the cues themselves:
{
"object": "captions",
"id": "spch_01jx...",
"format": "json",
"cues": [
{ "index": 1, "start": 0.0, "end": 1.42, "text": "Good morning.\nHere is the news." }
]
}
How cues are cut: words are accumulated until one of four things happens —
the text would no longer fit in max_lines lines of max_chars characters
(42 × 2 by default, the broadcast convention), the cue would run longer than
max_seconds, the speaker pauses for more than a second, or a sentence ends
and the cue has already been on screen long enough to read (0.8 s). Cue text is
wrapped at word boundaries — a word is never split — and every cue is on screen
for at least 0.1 s. Times are seconds from the start of the audio and match the
audio exactly, speed included.
OpenAI-compatible endpoint
POST /v1/audio/speech is a synchronous, OpenAI-compatible wrapper over
POST /v1/speech: point any tool that has an “OpenAI base URL” setting
(Obsidian’s aloud-tts, Open WebUI, LiteLLM, podcastfy, Remotion templates) at
Everlit by changing the base URL and the key, with no code changes. It
creates the same speech job as POST /v1/speech, blocks the connection for
it to finish — up to about 110 seconds — and streams the audio bytes straight
back instead of handing you a job id to poll. It shares the 4,096-character
input limit common to OpenAI’s endpoint; for longer text use POST
/v1/speech and poll instead.
curl https://api.everlit.audio/v1/audio/speech \
-H "Authorization: Bearer api_..." \
-H "Content-Type: application/json" \
-d '{
"model": "everlit",
"voice": "669",
"input": "Hello from Everlit.",
"response_format": "mp3"
}' \
--output speech.mp3
from openai import OpenAI
client = OpenAI(base_url="https://api.everlit.audio/v1", api_key="api_...")
response = client.audio.speech.create(
model="everlit",
voice="669",
input="Hello from Everlit.",
response_format="mp3",
)
response.write_to_file("speech.mp3")
| Field | Type | Description |
|---|---|---|
input |
string | Text to narrate, up to 4,096 characters. |
voice |
string | An Everlit voice id (see GET /v1/voices) — not an OpenAI voice name. |
response_format |
string | mp3, wav, or opus. Defaults to mp3. |
speed |
number | 0.5–2.0. Defaults to 1.0. |
model, instructions |
string | Accepted for OpenAI SDK compatibility, but ignored. |
The response is the audio file, with Content-Type set from
response_format and two extra headers: X-Everlit-Speech-Id (the
underlying spch_... job id) and X-Everlit-Audio-Url (that job’s stable
audio_url, fetchable later the same way GET /v1/speech/{id}/audio is).
Errors from this endpoint use OpenAI’s envelope (error.message, error.type,
error.code). input over 4,096 characters is 400 input_too_long; a job
that fails is 500 synthesis_failed.
If synthesis is still running when the wait times out, the response is 504
with error.code timeout and the X-Everlit-Speech-Id header set — fetch
the audio from audio_url once the job succeeds, or poll GET
/v1/speech/{id}.
Billing, idempotency (the Idempotency-Key header) and rate limits are all
identical to POST /v1/speech — this endpoint is the same job underneath.
Voices
GET /v1/voices returns every voice your key can use — Everlit’s catalog
plus your own cloned voices ("owned_by": "publisher"). Voice ids are
stable strings; pass them as voice when creating speech.
Every voice has a native_language: the language it was built in, which is
also its accent and the language it speaks best. Most voices are
is_multilingual: true — they narrate every language on
GET /v1/languages, in their native accent — so a Spanish text read by an
English-native voice sounds like an English speaker reading Spanish. Pick a
voice whose native_language matches your text when that matters.
is_multilingual: false voices speak only their native_language.
languages lists every code the voice can narrate, native language first.
Filter with ?language=es to get only voices that can narrate Spanish,
native Spanish speakers first; or free-text with ?q=british+female
(matches name, style, accent, gender or language). Each row also carries
accent, gender and preview_url: a key-free demo clip, null for your
own cloned voices (only catalog voices are previewable).
Each row may also carry pacing, the voice’s measured speaking pace:
"pacing": { "characters_per_second": 16.8, "measured_at": "2026-09-02T11:04:00Z", "source": "jobs" }
Every row also carries status and consent:
"status": "active",
"consent": { "subject_name": "Dana Reeves", "relationship": "employee", "attested_at": "2026-09-09T14:02:11Z" }
status is active for any Everlit catalog voice and for a cloned voice that
can narrate. A clone Everlit has removed is removed, and the row then also
carries removed with the reason (see Cloning a voice).
consent is the attestation recorded when the voice was cloned, and is null
for catalog voices.
characters_per_second counts billable (script-weighted) characters, so one
number serves every language the voice speaks. source is jobs when it was
measured from that voice’s recent successful jobs, calibration when it came
from a short synthesized passage instead. pacing is null for a voice we
have not measured yet, and it is what
/v1/speech/estimate predicts duration
from.
Voice previews
GET /v1/voices/{id}/preview?language=xx needs no API key — hand the URL
to a person so they can audition a voice before you use it. It 302-redirects
to a short demo MP3 (the first request for a voice/language pair synthesizes
and caches it; later ones are instant). language defaults to the voice’s
own. Per-IP rate limited, so a page that embeds many previews should still
throttle itself; only catalog voices are previewable — a cloned voice 404s.
Cloning a voice
POST /v1/voices clones a voice from a sample recording and adds it to your
account, alongside Everlit’s catalog: your own voice, a colleague’s, or a
narrator you have licensed. The new voice is used exactly like a catalog one —
pass its id as voice on POST /v1/speech, or as the narrator of an audio
article — and it is private to your account.
Send the sample either as a multipart upload (field file) or as a
sample_url we fetch:
curl -X POST https://api.everlit.audio/v1/voices \
-H "Authorization: Bearer $EVERLIT_API_KEY" \
-F "name=Dana (newsroom)" \
-F "file=@dana-sample.mp3" \
-F "subject_name=Dana Reeves" \
-F "relationship=employee" \
-F "consent=true" \
-F "statement=Dana signed a voice release on 2026-01-04."
curl -X POST https://api.everlit.audio/v1/voices \
-H "Authorization: Bearer $EVERLIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Dana (newsroom)",
"sample_url": "https://cdn.example.com/samples/dana.mp3",
"language": "en",
"subject_name": "Dana Reeves",
"relationship": "employee",
"consent": true,
"statement": "Dana signed a voice release on 2026-01-04."
}'
import requests
voice = requests.post(
"https://api.everlit.audio/v1/voices",
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": ("dana-sample.mp3", open("dana-sample.mp3", "rb"), "audio/mpeg")},
data={
"name": "Dana (newsroom)",
"subject_name": "Dana Reeves",
"relationship": "employee",
"consent": "true",
},
timeout=120,
).json()
print(voice["id"], voice["status"])
The answer is 201 with a Location header and the voice row:
{
"id": "1487",
"name": "Dana (newsroom)",
"style": "Cloned",
"language": "en-US",
"languages": ["en"],
"is_multilingual": false,
"accent": null,
"gender": null,
"owned_by": "publisher",
"preview_url": null,
"pacing": null,
"status": "active",
"consent": {
"subject_name": "Dana Reeves",
"relationship": "employee",
"attested_at": "2026-09-09T14:02:11Z"
}
}
The sample
- Formats:
mp3,wav,m4a,aac,ogg,flac,webm. - Size: 25 MB or less. A
sample_urlmust behttps://with a hostname (no IP literals), and must serve an audio content type. - Length: at least 15 seconds. 20–60 seconds is ideal.
- One speaker, talking normally, with no music, no overlapping voices and as little room noise as possible. We automatically trim the recording to the best 4–8 second window inside it, so a longer clean take is better than a short one you have edited yourself — but everything after the first minute or so buys nothing.
language(ISO 639-1) is optional; send it when the sample is not English.
A sample we cannot use comes back as 400 invalid_sample (too short, wrong
format, unreadable) or 400 sample_fetch_failed (the URL did not give us the
audio). A failure inside the cloning pipeline is 502
voice_processing_failed, and nothing is stored — retry.
Consent
Cloning a voice requires the consent of the person it belongs to, and the API records that you have it:
consent— must betrue. It is an attestation: the person whose voice this is has agreed to have it cloned and used to generate speech on Everlit, and you accept responsibility for that use. Anything else is400 consent_required.subject_name— the name of that person (≤120 characters).relationship— your relationship to them:self(your own voice),employee,contractor,licensed(a narrator whose voice you have licensed) orother.statement— optional free text (≤2000 characters) saying how consent was obtained. A release date and where the paperwork lives is the useful thing to put here.
Everlit stores those fields with the API key and publisher that sent them, the timestamp, the request IP and a SHA-256 hash of the sample, and returns the first three on every read of the voice. Cloning someone’s voice without their permission is a violation of the terms of service, and the record is what we act on when a voice is disputed.
Removal
There is no review hold: a clone narrates the moment POST /v1/voices
returns. Everlit can remove one afterwards for policy reasons — an
impersonation, a complaint from the voice’s owner, abusive or deceptive
output. Its status becomes removed, the row carries a removed object,
and synthesis with it answers 403 voice_removed quoting the same reason:
curl https://api.everlit.audio/v1/voices/1487 \
-H "Authorization: Bearer $EVERLIT_API_KEY"
# => { "id": "1487", "status": "removed",
# "removed": { "at": "2026-09-09T18:20:00Z", "reason": "rights_complaint",
# "message": "Complaint from the voice's owner or rights holder",
# "note": null } }
A removed clone does not count against limits.max_voice_clones, so a
replacement can be cloned straight away; DELETE /v1/voices/{id} clears the
row. Email support@everlit.audio if you believe a removal is a mistake.
How many, and deleting one
limits.max_voice_clones on GET /v1/account is how many cloned
voices your account may hold at once (default 10) and usage.voice_clones how
many it holds now. Cloning past the cap is
403 voice_clone_limit_reached, which carries limit and used; email
support@everlit.audio to have it raised.
DELETE /v1/voices/{id} answers 204 and frees a slot. It deletes the
reference audio and the consent record with the voice, and the voice stops
appearing in GET /v1/voices and can no longer narrate. Audio already
generated with it is untouched, and finished jobs keep their files. Only
voices your own account cloned can be deleted — a catalog voice or another
account’s clone is 404 voice_not_found.
A cloned voice’s speaking pace is measured automatically from its first
narrations, so pacing fills in shortly after the first jobs and
/v1/speech/estimate starts predicting
durations for it. Until then estimates fall back to the catalog average.
Languages
GET /v1/languages lists every supported language with two properties:
billing_multiplier— see Billing below.transcripts— whether jobs in this language produce word-level timestamps (/timings). Currently 25 European languages; everywhere else jobs succeed withtimings_available: false. Transcript coverage expands over time. The catalog itself covers every official EU language.
Launch catalog (69 languages):
- ×1 — English, Spanish, Portuguese, French, German, Italian, Dutch, Polish, Russian, Ukrainian, Czech, Slovak, Romanian, Greek, Hungarian, Finnish, Swedish, Danish, Bulgarian, Croatian, Estonian, Latvian, Lithuanian, Maltese, Slovenian, Irish, Norwegian, Catalan, Galician, Turkish, Vietnamese, Indonesian, Serbian, Bosnian, Belarusian, Kazakh, Mongolian, Uzbek, Georgian, Icelandic, Welsh, Basque, Esperanto, Swahili, Kinyarwanda, Ganda, Kabyle
- ×1.5 — Arabic, Hebrew, Persian, Urdu, Uighur, Kurdish (Sorani), Bengali, Hindi, Tamil, Telugu, Kannada, Marathi, Gujarati, Nepali, Malayalam, Punjabi, Odia
- ×2 — Korean, Thai
- ×2.5 — Japanese
- ×3 — Chinese, Cantonese
Billing
You are billed on usage.billable_characters: a script-weighted Unicode
character count of text exactly as you submitted it, before any HTML
stripping or text normalization. The one exception is
inline markup: the characters of an [everlit-…] tag are
never billed, since they are instructions rather than narration.
Why weights? Our costs scale with audio length, and scripts differ in how
much speech one character buys: English yields roughly 1,050 characters per
spoken minute, Chinese roughly 300. So that every language costs about the
same per finished hour of audio, each character bills at its script’s
rate: Latin/Cyrillic/Greek ×1, Arabic, Hebrew, and Indic scripts ×1.5,
Hangul and Thai ×2, kana ×2.5, Han ×3 (×2.5 when the text is Japanese — kana
present or language: "ja"). Mixed-script text is weighted per character;
billable_characters = ceil(Σ per-character rates). The computation uses
standard Unicode script ranges, so your bill remains reproducible from your
own request logs; the language hint can never reduce it. Multipliers are
published on GET /v1/languages and only ever change in your favor for
already-supported languages.
Failed jobs are not billed. Repeating identical text is billed each time — internal caching is our cost optimization, never a billing variable (and never a way to get free synthesis).
Included with a Studio plan
Every paid Studio plan (Independent, Network, Media, Enterprise) includes
speech: no per-character charge and no monthly character cap. Your plan’s
request rates and per-request character cap apply, and concurrency is capped
at the number of publications on your account — one running job per
publication, reported as max_concurrent_jobs on GET /v1/account and GET
/v1/usage (ask support@everlit.audio to raise it). billing.mode is plan,
POST /v1/speech/estimate reports price_cents: 0, and any pay-as-you-go
credit you hold is left untouched.
Extra capacity (optional). A plan account can also add a card or credit
under Billing → TTS API. That adds the pay-as-you-go lanes (4 by default)
on top of your included ones, so limits.max_concurrent_jobs becomes
included + extra. Jobs that start while an included lane is free stay free; a
job that has to take an extra lane is billed at the pay-as-you-go rate from
your balance, and if the balance cannot cover it the request is refused with
402 insufficient_balance (with included_concurrent_jobs in the error)
rather than queued. billing.included_concurrent_jobs and
billing.extra_capacity on GET /v1/account tell you which lanes you have;
a job’s own charge_cents says whether it was billed. Extra lanes apply to speech only; article conversions stay capped at your publication count.
Pay-as-you-go
The API is prepaid and independent of your Everlit plan. In Studio under
Billing → TTS API you add a card and buy credit; each successful job is
priced from usage.billable_characters at the published per-million-character
rate (fractional cents — nothing is rounded up) and deducted from your balance
when it finishes. A request whose price
exceeds your balance is rejected up front with 402 insufficient_balance —
nothing is queued and nothing is charged. Turn on automatic top-up (a
top-up amount and a balance threshold) and your card is charged whenever the
balance drops below the threshold, so production traffic never sees a 402.
If you leave auto top-up off, we email your team when the balance falls below
your warning line (settable on the same page, $2 by default).
Operational limits (safety rails, not credits) for pay-as-you-go accounts: 60 creates/min, 300 reads/min, 4 concurrent jobs, 20,000 characters per request. Need more headroom? Ask support@everlit.audio — limits are set per account. The per-request cap applies to raw text length.
Your effective limits and current usage are returned by GET /v1/usage:
{
"object": "usage",
"from": "2026-09-01T00:00:00Z",
"to": "2026-09-08T12:00:00Z",
"month_to_date_characters": 812340,
"monthly_character_limit": null,
"billing_mode": "payg",
"balance_cents": 1250.0,
"limits": { "requests_per_minute": 60, "reads_per_minute": 300, "max_concurrent_jobs": 4, "max_characters_per_request": 20000 },
"daily": [ { "day": "2026-09-08", "api_key_id": 42, "requests": 12, "succeeded": 11, "characters": 8120, "audio_seconds": 601.2 } ]
}
daily has one row per key per day: api_key_id matches api_key.id on
GET /v1/account, so an account with several keys can split its usage by key.
limits carries only the four rates and caps the TTS API actually enforces —
requests_per_minute, reads_per_minute, max_concurrent_jobs,
max_characters_per_request — under those exact names; plan-wide quotas
(publications, articles_per_month, …) live on GET /v1/account’s plan
instead, since they aren’t things this endpoint governs. billing_mode and
balance_cents mirror GET /v1/account’s billing.mode /
billing.balance_cents (balance_cents is null unless billing_mode is
payg). Track consumption here or in Studio under API → Usage; for
identity, entitlements and plan quotas use GET /v1/account instead.
Rate limits
Two independent per-key buckets: creates (POST /v1/speech,
rate_limit_exceeded) and reads (every other request, including each MCP
message, read_rate_limit_exceeded), so a polling loop can never block a
create. Every response carries RateLimit-Limit, RateLimit-Remaining, and
RateLimit-Reset (seconds) for the bucket it drew on. On 429 you also get
Retry-After — respect it, then retry with exponential backoff. Poll
GET /v1/speech/{id} every few seconds, not in a tight loop. A 503 capacity_unavailable means
the problem is on our side; retry after the Retry-After value.
A GET /v1/speech/{id} (or the get_speech MCP tool) that finds the job
still queued or processing is free if it waited: a poll made at least
poll_after_seconds (20) after your previous poll of that job is not counted
against the read limit, so an agent that follows the polling hint can never
lock itself out by checking on a job. A poll made sooner counts like any other
read. Polling in a tight loop is therefore still bounded by the read limit —
which is the point: wait the interval, and polling costs you nothing.
Idempotency
Pass an Idempotency-Key header (any unique string, e.g. a UUID) on
POST /v1/speech to make retries safe: resubmitting the same key with the
same body returns the original job (200 + Idempotency-Replayed: true
header) without charging you again. The same key with a different body is
rejected with 409 idempotency_key_reuse. Keys are scoped to your account
and live for 30 days.
Crash recovery: if you lose the job id before recording it (a process
died between the 202 and writing it down), GET /v1/speech?idempotency_key=K
finds the same job again — no need to resubmit and risk a 409.
GET /v1/speech?ids=spch_a,spch_b (up to 50, comma-separated) fetches many
jobs you already know the ids of in one call, e.g. to check on a batch of
running jobs without paginating the full list.
Webhooks
Pass callback_url and we POST the full job resource to it — the same shape
documented above, including a freshly signed download_url — when the job
reaches a terminal state, with header X-Everlit-Event: speech.succeeded or
speech.failed. Deliveries retry with backoff for ~30 minutes on non-2xx
responses. Only https:// URLs are accepted.
Verify authenticity with X-Everlit-Signature and your webhook secret
(Studio → API → Usage):
X-Everlit-Signature: t=1754575200,v1=5257a869e7...
import hmac, hashlib
def verify(payload_body, sig_header, secret):
parts = dict(p.split("=", 1) for p in sig_header.split(","))
expected = hmac.new(secret.encode(), f"{parts['t']}.{payload_body}".encode(),
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
Reject deliveries whose timestamp t is more than 5 minutes old to prevent
replays.
MCP
The API is also an MCP server, so Claude, Cursor, Claude Code, and any other MCP-capable agent can narrate text directly. It exposes the same operations as the REST endpoints as tools (alongside the Articles API tools — publishing full audio articles — on the same server):
| Tool | Backs |
|---|---|
create_speech |
POST /v1/speech (accepts an idempotency_key argument) |
get_speech |
GET /v1/speech/{id} (mints a fresh download_url; accepts idempotency_key instead of id) |
list_speech |
GET /v1/speech (accepts ids and idempotency_key) |
get_speech_timings |
GET /v1/speech/{id}/timings |
delete_speech |
DELETE /v1/speech/{id} |
list_voices |
GET /v1/voices |
create_voice |
POST /v1/voices (a sample_url; file uploads are REST-only) |
get_voice |
GET /v1/voices/{id} — poll a clone’s review status |
delete_voice |
DELETE /v1/voices/{id} |
list_languages |
GET /v1/languages |
get_usage |
GET /v1/usage |
get_account |
GET /v1/account — call first on a new connection |
list_mixables |
GET /v1/mixables |
The same server also hosts playlist tools (pinning published articles into a
playlist with an RSS feed) and pronunciation tools (fixing how a name is
read, account-wide or per publication) — see the
Articles guide for list_playlists,
create_playlist, add_to_playlist, list_pronunciations,
set_pronunciation and the rest.
Resources and prompts: the server also exposes MCP resources —
everlit://docs/tts-api and everlit://docs/articles-api (this guide and its
companion, as Markdown) and everlit://languages (the language catalog, as
JSON) — and two prompts, narrate_text(text, voice?) and
publish_from_url(url, publication?). A client that supports them can attach
either guide as context without spending a tool call, or start a workflow
straight from a slash menu instead of describing it in a message.
Endpoint: https://api.everlit.audio/v1/mcp — Streamable HTTP transport,
stateless, JSON responses. Nothing is stored between calls; the session id the
handshake hands back is only a version tag (see
Sessions and updates).
There are two ways to authenticate: Connect with OAuth (recommended for interactive clients — you log in to Studio and never handle a key) or a static API key (server-to-server, CI, headless agents).
Connect with OAuth
The server implements the MCP authorization spec (2025-06-18): OAuth 2.1
authorization code + PKCE, with dynamic client registration, so a compliant
client needs nothing but the URL. It discovers everything from
https://api.everlit.audio/.well-known/oauth-protected-resource/v1/mcp, which
points at the authorization server, https://studio.everlit.audio.
Claude.ai — Settings → Connectors → Add custom connector, URL
https://api.everlit.audio/v1/mcp, then Connect. You are sent to Everlit
Studio to log in (magic link, Google, or your company SSO), you pick which
publisher the connector should act as, and you approve. That is it.
Claude Code
claude mcp add --transport http everlit https://api.everlit.audio/v1/mcp
# then, inside Claude Code:
/mcp # select "everlit" -> Authenticate, and finish in the browser
The same flow works in Cursor, VS Code, the MCP Inspector, and anything else that follows the spec. Access is granted for one publisher, chosen at login; to switch publishers, revoke the connection and connect again.
Behind the scenes the access token is an ordinary Everlit API key with a
24-hour life, refreshed automatically by your client, so an OAuth connection is
metered, rate-limited and billed exactly like a static key. Connections appear
in Studio under API → Connected apps, where you can revoke one at any time
(revocation takes effect within a minute). Scopes: the server advertises a
single scope, everlit, covering everything the tools below can do — there is
no partial access in this version.
Static API key
Send the same Authorization: Bearer api_... header the REST endpoints take.
Use this for server-to-server and CI, where no one is present to click Approve.
{
"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 request with no key, or with an expired or revoked one, gets 401 with a
WWW-Authenticate: Bearer challenge carrying resource_metadata= — that
header is what makes an OAuth-capable client offer to log you in instead of
simply failing.
There is no blocking “wait” tool: while a job is queued or processing,
create_speech and get_speech results carry poll_after_seconds (20), and
the server instructions tell agents to wait that long between get_speech
calls. get_speech is not billed, but every MCP message counts against the
read rate limit (a 429 at the HTTP level, see Rate limits).
Tool calls are metered, limited, and billed 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. Audio itself is never returned inline. The finished job carries
download_url — a signed, key-free link, valid 24 hours — which is what an
agent should hand a person: it opens in a browser with nothing to paste. Agents
that hold the key programmatically can use audio_url instead, as described
under Downloading audio. Hosts like Claude.ai never
expose the connector’s bearer to the model, so download_url is the only link
the model can usefully give you there; call get_speech again for a fresh one
after 24 hours.
Server metadata
initialize answers with the server’s identity in serverInfo: name
(everlit), title (Everlit), version, a one-line description, the
websiteUrl https://everlit.audio, and icons — the Everlit wordmark,
https://everlit.audio/site/images/everlit-logo.png (PNG, 973x161).
Which of those a client actually receives depends on the protocol version it
negotiates: description and icons were added in 2025-11-25, title and
websiteUrl in 2025-06-18. A client that handshakes at an older version
gets the properties its version defines and nothing more, so older clients are
unaffected.
The icon is a wide wordmark, not a square mark. Claude.ai does not currently
render serverInfo.icons for custom connectors and shows a lettered avatar
instead; other MCP clients use it.
Every tool also carries a title and MCP annotations. readOnlyHint: true
marks the tools that only read (get_*, list_*), and destructiveHint: true
marks the ones that overwrite or remove data (delete_speech,
delete_article, update_publication) — clients use these to decide which
calls to confirm with you, so expect a prompt on the destructive ones.
Sessions and updates
We ship changes to this server — new tools, wider arguments, clearer instructions — without asking anyone to reconnect. How a change reaches your client depends on which handshake it uses, and neither one needs you to do anything.
Claude.ai and other sessionless clients re-discover the server every time a conversation loads: the tool list, the instructions and the server card come back fresh on each discovery call, so they are never more than a conversation old. Nothing to do.
Claude Code, Cursor, the Inspector and anything else that opens a session
learn the tools once, at initialize, and would otherwise keep using that
snapshot for as long as the client runs. So the Mcp-Session-Id we hand back
carries the server version that minted it:
Mcp-Session-Id: 1.7.0_2f6c1e9d4b8a07c35e1f9a2d6b4c8e70
The server stays stateless — that id is a tag, not a key to anything stored — but when we deploy a version with different tools or instructions, the next call carrying a tag from the old version is answered with
HTTP/1.1 404 Not Found
{"jsonrpc":"2.0","id":7,"error":{"code":-32600,"message":"Session not found"}}
which is what the Streamable HTTP spec says a server returns for a session it
has terminated. Your client then does what the spec tells it to: it opens a new
session with a fresh InitializeRequest, and picks up the current tools and
instructions in the process. Whether it also re-sends the call that was
rejected is up to the client — the spec does not require it, and some libraries
surface the expiry to the calling code instead. So a release costs one rejected
call per client still holding a session from the previous version: it
re-handshakes, and the next call succeeds. Claude.ai and other sessionless
clients are unaffected — they hold no session to expire.
The version in serverInfo is the same number, so you can always see which
build you are talking to.
Two promises about what those releases contain:
- Additive only. New tools and new optional arguments; existing tool names, their required arguments and their result fields do not change meaning under you.
-
Nothing disappears without warning. A tool we intend to remove is first marked deprecated: its description starts with
DEPRECATED: use <tool>., and every successful result carries a notice naming the replacement, instructuredContent:{ "id": "spch_01jx...", "status": "succeeded", "notices": [ { "code": "deprecated_tool", "message": "This tool is deprecated as of Everlit MCP server 1.8.0; use create_speech instead. It still works today and will be removed in a later version." } ] }and as a trailing
NOTICE (deprecated_tool): ...line in the result text, so a model that only sees the text still sees it. The deprecated tool keeps working until a later release removes it;noticesis absent on a call that has nothing to report.
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_speech and get_speech add poll_after_seconds while the job
is still running.
create_speech
Narrate text with a voice; returns the speech job, status “queued”. Poll get_speech until “succeeded”, then give the user download_url - key-free, valid 24 hours.
| Argument | Type | Required | Description |
|---|---|---|---|
text |
string | yes | Text to narrate; HTML is stripped. Inline tags: [everlit-pause:N], [everlit-mixable:N], [everlit-subvoice-open:N]. |
voice |
string or integer | yes | A voice id from list_voices. |
language |
string | no | ISO 639-1 hint (see list_languages); auto-detected when omitted. |
format |
enum: mp3, wav, opus | no | Output format: mp3 (default), wav or opus. |
bitrate |
integer | no | kbps. mp3: 64, 96, 128 (default), 192, 256, 320; opus: 32, 48, 64 (default), 96, 128; not for wav. |
speed |
number | no | Playback speed 0.5-2.0; default 1.0. |
paragraph_pause |
number | no | Silence in seconds (0-5) at every blank-line paragraph break. |
pronunciations |
object | no | Up to 50 word -> respelling pairs for this request; whole-word, case-insensitive. |
callback_url |
string | no | https URL to POST the finished job to. |
metadata |
object | no | Up to 8 string values, 512 bytes total. |
idempotency_key |
string | no | Any unique string; a resubmit returns the original job, uncharged. |
estimate_speech
Price a request and predict its audio length before creating it: billable characters, price and estimated_duration_seconds. Creates no job.
| Argument | Type | Required | Description |
|---|---|---|---|
text |
string | yes | Text to narrate; HTML is stripped. Inline tags: [everlit-pause:N], [everlit-mixable:N], [everlit-subvoice-open:N]. |
voice |
string or integer | yes | A voice id from list_voices. |
language |
string | no | ISO 639-1 hint (see list_languages); auto-detected when omitted. |
speed |
number | no | Playback speed 0.5-2.0; default 1.0. |
paragraph_pause |
number | no | Seconds (0-5) of silence at each paragraph break, added to the estimate. |
get_speech
Fetch a speech job by id; status is queued, processing, succeeded, failed, expired or deleted. Once succeeded it carries a fresh download_url, valid 24 hours.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | no | The job id (spch_…). |
idempotency_key |
string | no | Instead of id: the idempotency_key the job was created with. |
list_speech
List this account’s speech jobs, newest first. Page with starting_after = the last id of the previous page while has_more is true.
| Argument | Type | Required | Description |
|---|---|---|---|
limit |
integer | no | Page size (default 20). |
starting_after |
string | no | Return jobs created before this job id. |
status |
enum: queued, processing, succeeded, failed, expired, deleted | no | Filter by status. |
q |
string | no | Match on id, language, voice name, error code or metadata. |
ids |
array | no | Only these job ids, up to 50. |
get_speech_timings
Word-level timestamps for a succeeded job, for captions and read-along highlighting. Needs timings_available.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The job id (spch_…). |
get_speech_captions
Subtitle cues from the job’s word timings (needs timings_available). srt/vtt return the file text; json returns cue objects {index, start, end, text}.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The job id (spch_…). |
format |
enum: srt, vtt, json | no | Caption format; default srt. |
max_chars |
number | no | Characters per line, 10-120 (default 42). |
max_lines |
number | no | Lines per cue, 1-3 (default 2). |
max_seconds |
number | no | Longest a cue may stay on screen, 1-15 seconds (default 5). |
delete_speech
Permanently delete a job’s audio and timings, or cancel one still queued/processing. Cancelled while queued: not billed; processing: billed in full.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The job id (spch_…). |
list_voices
Voices for this account, with native_language, is_multilingual, accent, gender, pacing and preview_url (a key-free demo). Use the id as create_speech’s voice.
| Argument | Type | Required | Description |
|---|---|---|---|
language |
string | no | Only voices that narrate this language (e.g. “es-MX”); native speakers first. |
q |
string | no | Match on name, style, accent or gender. |
limit |
integer | no | Page size (default 100). |
starting_after |
string | no | Return voices with an id greater than this one. |
create_voice
Clone a voice from an https sample of one speaker (15 s+), with the person’s name, your relationship and consent: true. Counts against limits.max_voice_clones.
| Argument | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Name shown in list_voices, 80 characters or fewer. |
sample_url |
string | yes | https link to the sample (mp3/wav/m4a/aac/ogg/flac/webm, 25 MB max, 15 s+ of one speaker). |
subject_name |
string | yes | Name of the person whose voice this is. |
relationship |
enum: self, employee, contractor, licensed, other | yes | Your relationship to that person; “self” for your own voice. |
consent |
boolean | yes | Must be true: the person consented to the clone and its use on Everlit. |
language |
string | no | ISO 639-1 code of the language spoken in the sample (optional). |
statement |
string | no | Note on how consent was obtained, kept with the record. |
get_voice
Fetch one voice by id; status is active or removed, and a removed clone carries removed and can no longer narrate.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The voice id, as returned by list_voices. |
delete_voice
Permanently delete a voice this account cloned, with its reference audio and consent record, freeing a clone slot. Catalog voices cannot be deleted.
| Argument | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The cloned voice id. |
list_languages
Supported languages with their billing multipliers and whether word timings are available.
No arguments.
get_usage
Character usage: month to date, billing mode and balance, the enforced limits, and a daily breakdown. For entitlements use get_account.
| Argument | Type | Required | Description |
|---|---|---|---|
from |
string | no | ISO 8601 start of the window. |
to |
string | no | ISO 8601 end of the window. |
get_account
The account behind this connection: publisher, entitlements (tts_api gates the speech tools), billing mode, balance, limits, quotas and fix-it links.
No arguments.
Errors
Errors use one envelope, with a stable machine-readable code — branch on
code, never on message text:
{
"error": {
"type": "invalid_request_error",
"code": "text_too_long",
"message": "text is 34,102 characters; the maximum for your plan is 20,000.",
"param": "text",
"next_step": "reduce_text",
"max_characters": 20000,
"characters": 34102,
"request_id": "ab12cd34"
}
}
Include request_id in support tickets. Two optional fields make an error
actionable without parsing message:
next_step— a stable enum naming what to do:request_access,mint_api_key,add_billing,top_up,reduce_text,retry_after.resolution_url— an absolute URL a person can open to fix it (a Studio billing or keys page, or the access-request page). Hand it to the user as a link rather than paraphrasing it.
Branch your code on code as always; use next_step to decide which of a
small set of remedies applies, and resolution_url as the link to offer.
Every 429/503 carries next_step: "retry_after". Two error codes carry
extra fields of their own: text_too_long adds max_characters and
characters (both integers); insufficient_balance adds balance_cents and
cost_cents (both numbers).
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_json |
Body is not a JSON object |
| 400 | missing_parameter |
A required parameter is absent or the wrong type |
| 400 | unknown_parameter |
Unrecognized parameter (check spelling) |
| 400 | invalid_encoding |
text is not valid UTF-8 |
| 400 | text_too_long |
Over your plan’s per-request cap |
| 400 | unsupported_language |
Not an accepted language code |
| 400 | unsupported_format |
format is not mp3, wav or opus |
| 400 | invalid_bitrate |
bitrate is not one of the values allowed for the format, or was sent with wav |
| 400 | invalid_speed |
speed is not a number between 0.5 and 2.0 |
| 400 | invalid_paragraph_pause |
paragraph_pause is not a number between 0 and 5 |
| 400 | invalid_pronunciations |
pronunciations is not an object of up to 50 string pairs, or a key is over 64 characters |
| 400 | invalid_parameter |
A /captions option (max_chars, max_lines, max_seconds) is out of range |
| 400 | invalid_status |
GET /v1/speech?status= is not one of the job statuses |
| 400 | invalid_markup |
An inline tag has a non-integer id, subvoice tags nest or do not balance |
| 403 | mixable_not_accessible |
A mixable in text belongs to another account |
| 404 | mixable_not_found |
No mixable with that id in text |
| 400 | invalid_callback_url |
Must be https with a hostname |
| 400 | consent_required |
consent must be true when cloning a voice |
| 400 | invalid_sample_url |
sample_url must be https with a hostname |
| 400 | sample_fetch_failed |
We could not fetch the audio at sample_url |
| 400 | invalid_sample |
The sample is too short, too large, or not a supported audio file |
| 401 | missing_api_key / invalid_api_key / api_key_revoked / api_key_expired |
Credential problems |
| 402 | insufficient_balance |
Prepaid balance doesn’t cover the request — add credit; on a plan, only for a job beyond the included lanes |
| 403 | wrong_key_type |
Use an api_ key (not pk_/tkn_) |
| 403 | voice_not_accessible |
Voice belongs to another account |
| 403 | voice_removed |
Everlit removed this cloned voice; the message carries the reason |
| 403 | voice_clone_limit_reached |
Account is at max_voice_clones — delete one or ask support |
| 403 | tts_api_not_enabled |
No paid plan and pay-as-you-go not set up — upgrade or add a card in Studio |
| 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. |
| 403 | invalid_download_link |
A download_url was tampered with or is over 24h old — fetch a fresh one |
| 404 | job_not_found / voice_not_found / timings_not_available / access_request_not_found / unknown_endpoint |
Nothing at that address |
| 409 | idempotency_key_reuse |
Same key, different body |
| 409 | job_not_complete |
Audio requested before the job finished |
| 410 | audio_expired / audio_deleted |
Audio is gone (job record remains) |
| 422 | no_speakable_text |
No letters or digits to narrate |
| 429 | rate_limit_exceeded / read_rate_limit_exceeded / concurrency_limit_exceeded |
Slow down (see Retry-After) |
| 500 | internal_error |
Our fault; the job was not billed |
| 502 | voice_processing_failed |
Voice cloning failed; nothing was stored — retry |
| 503 | capacity_unavailable |
Temporarily saturated; retry |
Notes on input handling
- HTML in
textis stripped before narration but counts toward billing — send plain text. Inline markup is the exception: its characters are never billed. - Everlit control sequences other than the inline markup
family (
[Everlit-Sponsored:N], unknown tags) are stripped. - Control characters are removed; text is Unicode-normalized (NFC).
Changelog
1.7.2 — 2026-09-11
- The job resource carries
voice_nameon every read (it was only on the list).GET /v1/usagedaily rows are per key per day; the key column is nowapi_key_id, matchingapi_key.idonGET /v1/account(wasauth_token_id, undocumented).
1.7.1 — 2026-09-11
/timingsand/captionsno longer carry inline-markup tokens: a tag that stood alone is dropped and one glued to a word is peeled off, so the word list is the spoken text.POST /v1/speech/estimatereturnsprice_centsas a JSON number (it was a string), which the MCPestimate_speechoutput schema requires.
1.7.0 — 2026-09-11
- Inline markup in
text:[everlit-pause:N],[everlit-mixable:N]and[everlit-mixable-background:N]for music beds fromGET /v1/mixables, and[everlit-subvoice-open:N]…[everlit-subvoice-close]for a passage in another voice. Tags are case-insensitive and every id is validated against the account. New error codesinvalid_markup,mixable_not_accessible,mixable_not_found. See Inline markup. - Inline markup is never billed:
billable_charactersleaves tag characters out, whileusage.charactersand the per-request cap counttextexactly as submitted. - MCP server 1.14.0 → 1.15.0:
create_speech/estimate_speechdescribe the markup.
1.6.1 — 2026-09-11
- Documentation only: the status page moved to https://everlit.openstatus.dev
(
server.status_urlonGET /v1/accountfollows); the errors table now lists every validation code (invalid_bitrate,invalid_speed,invalid_paragraph_pause,invalid_pronunciations,invalid_parameter,invalid_status) and the OpenAI-compatible endpoint’s own codes; the pronunciation-library endpoints appear in the endpoints table.
1.6.0 — 2026-09-09
- Voice cloning:
POST /v1/voicesclones a voice from an uploaded sample or asample_urlwith a recorded consent attestation,GET /v1/voices/{id}reads one back andDELETE /v1/voices/{id}removes one. A clone narrates as soon as it is created; Everlit can remove one afterwards for policy reasons, which setsstatustoremovedand makes synthesis with it403 voice_removed. Voice rows gainedstatus,removedandconsent;GET /v1/accountgainedlimits.max_voice_clonesandusage.voice_clones. See Cloning a voice. - MCP server 1.14.0 adds
create_voice,get_voiceanddelete_voice.
1.5.1 — 2026-09-09
- Voices: new
native_language(the voice’s accent and strongest language).is_multilingualis now a provider fact, not a tag: OmniVoice and ElevenLabs voices narrate every language onGET /v1/languages; Kokoro and WellSaid voices only their native language.languageslists the full set (native first) and?language=returns every voice that can narrate the language, native speakers first.
1.5.0 — 2026-09-08
POST /v1/speechtakesformatwavandopusalongsidemp3, plusbitrate(mp3 64–320 kbps, opus 32–128).audio.content_typeand the download filename follow the format.- New
speed(0.5–2.0, applied after synthesis; word timings are scaled with it) andparagraph_pause(0–5 s at blank-line paragraph breaks), and[everlit-pause:N]tags you can put anywhere in the text. See Pacing, pauses and pronunciation. - New per-request
pronunciationsmap, for a one-off respelling that should not join the account-wide pronunciation library. - New
POST /v1/speech/estimate: billable characters, price andestimated_duration_secondsbefore you create anything. Voices now carrypacing.characters_per_second, their measured speaking pace. See Estimating cost and duration. - New
GET /v1/speech/{id}/captions: SubRip, WebVTT or cue objects built from the job’s word timings. See Captions. - New
POST /v1/audio/speech, an OpenAI-compatible synchronous endpoint that returns the audio bytes instead of a job. See OpenAI-compatible endpoint. - MCP server 1.9.0 → 1.10.0: new
estimate_speechandget_speech_captionstools, a key-free downloadresource_linkoncreate_speech/get_speech/list_speechresults, and output schemas on the job- and estimate-shaped tools.
1.4.2 — 2026-09-09
- A pending
GET /v1/speech/{id}/get_speech(stillqueuedorprocessing) no longer counts against the read rate limit when it waitedpoll_after_secondssince the previous poll of that job; a faster re-poll still counts. See Rate limits. - The MCP server now also hosts playlist and pronunciation tools; see the Articles API guide.
- MCP server
version1.8.0 → 1.9.0.
1.4.1 — 2026-09-08
- New key-free
POST /v1/access-requestsandGET /v1/access-requests/{id}: an agent whose user has no Everlit account can ask for one and poll the answer. See Getting access. - MCP: a client with no credentials gets
tools/list, resources and prompts, and exactly two tools —request_accessandget_access_request.initializeandpingwithout credentials still answer401with theWWW-Authenticatechallenge, which is what starts the OAuth login in Claude.ai, Claude Code and Cursor. - The
401 missing_api_keymessage andresolution_urlnow name the access-request path. - New
next_stepvalues:authorize_oauth,wait,contact_support.
1.4.0 — 2026-09-08
- New
GET /v1/account/get_account: publisher, key, entitlements, billing mode, balance and price, enforced limits, plan quotas — the first call to make on a new key. See Account. - Errors carry
next_step(request_access,mint_api_key,add_billing,top_up,reduce_text,retry_after) andresolution_url;text_too_longaddsmax_characters/characters;insufficient_balanceaddsbalance_cents/cost_cents. See Errors. - Breaking:
GET /v1/usage’slimitskeys are renamed (thetts_prefix is dropped) and reduced to the four limits the API actually enforces; plan-wide keys moved toGET /v1/account’splan. Also addsbilling_modeandbalance_cents. - Voices: new
languages,is_multilingual,accent,preview_urlfields; newqfilter; key-freeGET /v1/voices/{id}/preview. - New
GET /v1/mixables/list_mixables: the intro/outro music beds your account may use. get_speech/get_articleacceptidempotency_keyinstead ofid;list_speech/list_articlesacceptids(up to 50) andidempotency_key.- MCP: resources (
everlit://docs/tts-api,everlit://docs/articles-api,everlit://languages) and prompts (narrate_text,publish_from_url); theinitializeinstructions are shorter. - MCP server
version1.7.0 → 1.8.0.
1.3.1 — 2026-09-08
- MCP: the
initializehandshake now returns anMcp-Session-Idcarrying the server version. When we ship a new version, a client still holding the old one gets a404 Session not foundon its next call and re-handshakes automatically, so tool and instruction changes reach long-running clients without a manual reconnect. Sessionless clients (Claude.ai) are unaffected — they re-discover the server on every conversation. See Sessions and updates. - MCP results may carry
notices(structuredContent.notices, plusNOTICE (...)lines in the text) — used first for tool deprecations, which are now announced in the tool description and on every result rather than by removing a tool. - MCP server
version1.6.0 → 1.7.0.
1.3.0 — 2026-09-08
- New
download_urlon the job resource (and on webhook payloads): a signed, key-free link to the audio, valid 24 hours and re-minted on every read. See Shareable links.audio_urlis unchanged — still stable for the full 30 days, still requires your key. - MCP: agents are instructed to hand
download_urlto the user; on hosts that never reveal the connector’s key to the model (Claude.ai), that is the only link the model can give you. - MCP server
version1.5.0 → 1.6.0.
1.2.3 — 2026-09-08
- MCP
initializenow returns adescription, the Everliticonsand awebsiteUrlinserverInfo, next to thename,titleandversionit already sent; clients show them on the server’s card. A client that negotiates an older protocol version receives only the properties that version defines. See MCP. update_publicationis annotateddestructiveHint: true— it overwrites a publication’s settings for every future article — so clients prompt for confirmation before calling it.- MCP server
version1.4.1 → 1.5.0.
1.2.2 — 2026-09-03
- The MCP server accepts OAuth 2.1 connections (authorization code + PKCE, dynamic client registration, RFC 9728 discovery): connect Claude.ai, Claude Code, Cursor or the Inspector with a Log-in button instead of a pasted key. See MCP. Static
api_keys keep working unchanged everywhere. 401challenges now includeresource_metadata=(anderror="invalid_token"when a key was presented and rejected).
1.2.1 — 2026-09-02
DELETE /v1/speech/{id}on aqueued/processingjob now cancels it instead of leaving it to finish. Cancelled while queued: not billed. Cancelled while processing: billed in full. Both appear in the usage ledger with outcomecancelled.
1.2.0 — 2026-09-01
- The MCP server is now named
everlitand also hosts the Articles API tools (create_article,get_article,list_articles,list_publications). Reconfigure clients that referencedeverlit-tts. - A key without the TTS entitlement can still connect to the MCP server; TTS tools then return a
tts_api_not_enabledtool error instead of the server refusing the connection.
1.1.0 — 2026-09-01
- MCP server at
POST /v1/mcp(Streamable HTTP, same API keys) with one tool per REST endpoint; running jobs reportpoll_after_seconds. - Reads now have their own rate-limit bucket, separate from creates: new
429 read_rate_limit_exceeded.RateLimit-*headers appear on every response. 401responses carry aWWW-Authenticate: Bearerchallenge.
1.0.1 — 2026-08-31
GET /v1/languages: the supported-language catalog with billing multipliers and transcript availability.- Script-weighted billing:
usage.billable_characters(what you are billed) alongsideusage.characters(what you sent). Latin-script text is unchanged. - Optional
languagehint onPOST /v1/speech.
1.0.0 — 2026-08-07
- Initial release.
Support
- Email: support@everlit.audio
- OpenAPI spec: https://everlit.audio/docs/tts-openapi.yml
- Status: https://everlit.openstatus.dev