# Everlit TTS API

Version: 1.7.2
Last Updated: 2026-09-11

High-quality text-to-speech as a simple REST API: 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.

**Base URL:** `https://api.everlit.audio`

---

## Quickstart

```bash
# 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](#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`.

```python
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)
```

```javascript
// 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()));
```

```ruby
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](#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](#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](#mcp)). If you
have none — or you are an agent whose user has none — ask for one without a
key:

```bash
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](#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](#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](#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](/docs/articles-api#pronunciations)) |
| `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](#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](#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.

```json
{
  "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](#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](#cloning-a-voice) 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](#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](#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:

```json
{
  "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](#inline-markup) for the rest of the family.

**`pronunciations`** fixes a name or an acronym for **this request only**:

```json
{
  "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.

```json
{
  "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](#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](#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):

```json
{
  "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:

```bash
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"}'
```

```json
{
  "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](#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. |

```bash
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:

```json
{
  "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.

```bash
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
```

```python
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 `id`s 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:

```json
"pacing": { "characters_per_second": 16.8, "measured_at": "2026-09-02T11:04:00Z", "source": "jobs" }
```

Every row also carries `status` and `consent`:

```json
"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](#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`](#estimating-cost-and-duration) 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:

```bash
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."
```

```bash
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."
      }'
```

```python
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:

```json
{
  "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.

### Consent

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:

```bash
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`](#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`](#estimating-cost-and-duration) 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](#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](#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). You can remove
the card from the same page at any time; credit you've already bought stays
spendable, but nothing is charged again (auto top-up included) until you add a
new one.

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`:

```json
{
  "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...
```

```python
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](https://modelcontextprotocol.io) 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](/docs/articles-api#mcp) — 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](/docs/articles-api#mcp) 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](#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**

```bash
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.

```json
{
  "mcpServers": {
    "everlit": {
      "type": "http",
      "url": "https://api.everlit.audio/v1/mcp",
      "headers": { "Authorization": "Bearer api_YOUR_KEY" }
    }
  }
}
```

```bash
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](#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](#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](#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`:

  ```json
  {
    "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.

<!-- mcp-tools:start (generated by `rake docs:tts_api`; do not edit by hand) -->

### `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._

<!-- mcp-tools:end -->

---

## Errors

Errors use one envelope, with a stable machine-readable `code` — branch on
`code`, never on `message` text:

```json
{
  "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](#inline-markup) 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](#inline-markup) is the
  exception: its characters are never billed.
- Everlit control sequences other than the [inline markup](#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).
- 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](#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](#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](#estimating-cost-and-duration).
- New `GET /v1/speech/{id}/captions`: SubRip, WebVTT or cue objects built from
  the job's word timings. See [Captions](#captions).
- New `POST /v1/audio/speech`, an OpenAI-compatible synchronous endpoint that
  returns the audio bytes instead of a job. See
  [OpenAI-compatible endpoint](#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](#rate-limits).
- The MCP server now also hosts playlist and pronunciation tools; see the
  [Articles API guide](/docs/articles-api#mcp).
- 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](#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](#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](#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](#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](#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](#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](#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](/docs/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
