TTS API

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 statuspending, 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.52.0 (default 1.0), applied after synthesis; word timings are scaled to match.
paragraph_pause number no Seconds of silence (05) 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.characters counts the tag text; billable_characters does not.
  • usage.audio_seconds and audio.duration_seconds include the music. A foreground bed adds its full length; a background bed adds nothing.
  • POST /v1/speech/estimate predicts 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_available follows 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.

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.modeplan 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.52.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_url must be https:// 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.

Cloning a voice requires the consent of the person it belongs to, and the API records that you have it:

  • consent — must be true. 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 is 400 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) or other.
  • 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 with timings_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, in structuredContent:

    {
      "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; notices is 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 text is 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_name on every read (it was only on the list). GET /v1/usage daily rows are per key per day; the key column is now api_key_id, matching api_key.id on GET /v1/account (was auth_token_id, undocumented).

1.7.1 — 2026-09-11

  • /timings and /captions no 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/estimate returns price_cents as a JSON number (it was a string), which the MCP estimate_speech output 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 from GET /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 codes invalid_markup, mixable_not_accessible, mixable_not_found. See Inline markup.
  • Inline markup is never billed: billable_characters leaves tag characters out, while usage.characters and the per-request cap count text exactly as submitted.
  • MCP server 1.14.0 → 1.15.0: create_speech / estimate_speech describe the markup.

1.6.1 — 2026-09-11

  • Documentation only: the status page moved to https://everlit.openstatus.dev (server.status_url on GET /v1/account follows); 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/voices clones a voice from an uploaded sample or a sample_url with a recorded consent attestation, GET /v1/voices/{id} reads one back and DELETE /v1/voices/{id} removes one. A clone narrates as soon as it is created; Everlit can remove one afterwards for policy reasons, which sets status to removed and makes synthesis with it 403 voice_removed. Voice rows gained status, removed and consent; GET /v1/account gained limits.max_voice_clones and usage.voice_clones. See Cloning a voice.
  • MCP server 1.14.0 adds create_voice, get_voice and delete_voice.

1.5.1 — 2026-09-09

  • Voices: new native_language (the voice’s accent and strongest language). is_multilingual is now a provider fact, not a tag: OmniVoice and ElevenLabs voices narrate every language on GET /v1/languages; Kokoro and WellSaid voices only their native language. languages lists 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/speech takes format wav and opus alongside mp3, plus bitrate (mp3 64–320 kbps, opus 32–128). audio.content_type and the download filename follow the format.
  • New speed (0.5–2.0, applied after synthesis; word timings are scaled with it) and paragraph_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 pronunciations map, for a one-off respelling that should not join the account-wide pronunciation library.
  • New POST /v1/speech/estimate: billable characters, price and estimated_duration_seconds before you create anything. Voices now carry pacing.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_speech and get_speech_captions tools, a key-free download resource_link on create_speech / get_speech / list_speech results, and output schemas on the job- and estimate-shaped tools.

1.4.2 — 2026-09-09

  • A pending GET /v1/speech/{id} / get_speech (still queued or processing) no longer counts against the read rate limit when it waited poll_after_seconds since 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 version 1.8.0 → 1.9.0.

1.4.1 — 2026-09-08

  • New key-free POST /v1/access-requests and GET /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_access and get_access_request. initialize and ping without credentials still answer 401 with the WWW-Authenticate challenge, which is what starts the OAuth login in Claude.ai, Claude Code and Cursor.
  • The 401 missing_api_key message and resolution_url now name the access-request path.
  • New next_step values: 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) and resolution_url; text_too_long adds max_characters/characters; insufficient_balance adds balance_cents/cost_cents. See Errors.
  • Breaking: GET /v1/usage’s limits keys are renamed (the tts_ prefix is dropped) and reduced to the four limits the API actually enforces; plan-wide keys moved to GET /v1/account’s plan. Also adds billing_mode and balance_cents.
  • Voices: new languages, is_multilingual, accent, preview_url fields; new q filter; key-free GET /v1/voices/{id}/preview.
  • New GET /v1/mixables / list_mixables: the intro/outro music beds your account may use.
  • get_speech/get_article accept idempotency_key instead of id; list_speech/list_articles accept ids (up to 50) and idempotency_key.
  • MCP: resources (everlit://docs/tts-api, everlit://docs/articles-api, everlit://languages) and prompts (narrate_text, publish_from_url); the initialize instructions are shorter.
  • MCP server version 1.7.0 → 1.8.0.

1.3.1 — 2026-09-08

  • MCP: the initialize handshake now returns an Mcp-Session-Id carrying the server version. When we ship a new version, a client still holding the old one gets a 404 Session not found on 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, plus NOTICE (...) 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 version 1.6.0 → 1.7.0.

1.3.0 — 2026-09-08

  • New download_url on 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_url is unchanged — still stable for the full 30 days, still requires your key.
  • MCP: agents are instructed to hand download_url to 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 version 1.5.0 → 1.6.0.

1.2.3 — 2026-09-08

  • MCP initialize now returns a description, the Everlit icons and a websiteUrl in serverInfo, next to the name, title and version it 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_publication is annotated destructiveHint: true — it overwrites a publication’s settings for every future article — so clients prompt for confirmation before calling it.
  • MCP server version 1.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.
  • 401 challenges now include resource_metadata= (and error="invalid_token" when a key was presented and rejected).

1.2.1 — 2026-09-02

  • DELETE /v1/speech/{id} on a queued/processing job 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 outcome cancelled.

1.2.0 — 2026-09-01

  • The MCP server is now named everlit and also hosts the Articles API tools (create_article, get_article, list_articles, list_publications). Reconfigure clients that referenced everlit-tts.
  • A key without the TTS entitlement can still connect to the MCP server; TTS tools then return a tts_api_not_enabled tool 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 report poll_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.
  • 401 responses carry a WWW-Authenticate: Bearer challenge.

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) alongside usage.characters (what you sent). Latin-script text is unchanged.
  • Optional language hint on POST /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