Ready to use the API?
Free account — no credit card required. Get an API key in 30 seconds.
Using an AI agent (Claude, ChatGPT, Cursor)? Connect over MCP instead → easyvoice.ae/mcp
Free TTS API Key: OpenAI-Compatible API Docs
OpenAI-compatible Text-to-Speech API. Switch from OpenAI by changing your base URL and API key; the six OpenAI voice names work as they are. Free keys work today: 5,000 characters/day, 12 voices, no card. Pro ($9.99/mo) unlocks all 66 voices, cloning, and unlimited characters.
See the /developers hub for the full per-1M pricing comparison and workflow overview.
Coming from OpenAI? See the migration guide and the OpenAI TTS pricing breakdown.
OpenAI's deprecations page lists tts-1, tts-1-hd and the gpt-4o-mini-tts snapshots for removal on 6 January 2027 (checked 16 October 2026); to keep an OpenAI SDK speech call working, change the base URL and the API key to EasyVoice's. See the OpenAI TTS removal page.
How to get a free TTS API key
To get a free TTS API key, create a free account, open the API Keys page and create a key. The key works with the OpenAI-compatible endpoint POST /api/v1/audio/speech, gives 12 voices and 5,000 characters a day shared with the web app, and needs no credit card. Pro removes the daily cap.
Quick Start
curl -X POST https://easyvoice.ae/api/v1/audio/speech \
-H "Authorization: Bearer ev_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"model": "kokoro-82m",
"input": "Hello, this is EasyVoice!",
"voice": "af_aoede"
}' \
--output speech.mp3This runs as-is with a free key — af_aoede is one of the 12 free voices. Create a free account, generate a key on the API keys page, paste it in. No card.
Prefer tools over raw HTTP? EasyVoice is also a hosted MCP server — speech, long-form narration, podcast episodes, and voice cloning from any MCP-capable agent, authenticated with these same ev_ keys. Connection instructions for Claude, ChatGPT, and Cursor live at /mcp; the TTS MCP server explainer covers what agents can do with it.
Agent framework compatibility
Tested request-shape/auth compatibility against base_url + Bearer key, live against production, 2026-09-15:
- OpenAI Python SDK (2.54.0 & 3.13.0) — worked. Both versions return an identical, clean, typed
openai.AuthenticationErroron an invalid key. No gap found. - LiveKit Agents'
openai.TTS(livekit-agents 1.8.1) — worked, with both the plugin's default model (gpt-4o-mini-tts) andtts-1explicit. Clean typedopenai.APIStatusError, no retry storm. The plugin's default voice isash, which is not mapped, so setvoiceto one of the six OpenAI voice names or an EasyVoice ID. - Pipecat
OpenAITTSService(pipecat-ai 1.10.0): not re-tested since release 12.
Release 12 (2026-10-10): the six OpenAI voice names are accepted as they are; pcm returns raw 16-bit mono PCM; speed outside 0.5-2.0 is clamped.
Audio-byte format compatibility of the 2026-09-15 runs (LiveKit's SSE-vs-non-SSE default) is untested — these tests used a synthetic invalid key, so every request short-circuits at a 401 before the response body is ever produced. Full detail: 118-COMPAT-TEST.md (not public).
Hear the exact output of the curl above
Same engine and voices as /api/v1/audio/speech — try it in the browser before you create a key. 2,000 free characters a day, no signup.
POST /api/v1/audio/speech
Generate speech from text. Returns audio file directly.
Headers
| Header | Value |
|---|---|
| Authorization | Bearer ev_your_api_key |
| Content-Type | application/json |
Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| model | string | No | Any string is accepted and ignored (default: "kokoro-82m") |
| input | string | Yes | Text to convert. Free: up to 5,000 chars (your daily pool). Pro: up to 50,000 per request. |
| voice | string | No | Voice ID (default: af_aoede), or one of the six OpenAI voice names alloy, echo, fable, onyx, nova and shimmer. Free keys: the 12 free voices, and of the six OpenAI names only nova is free. Pro: all 66 + cloned voices. |
| response_format | string | No | "mp3", "wav" or "pcm" (raw 16-bit mono PCM). Default: mp3. opus, aac and flac are accepted but return MP3. |
| speed | number | No | 0.5 to 2.0 (default: 1.0); a value outside that range is clamped |
EasyVoice Extensions
EasyVoice adds optional audio controls on top of the OpenAI-compatible request. On the public POST /v1/audio/speech endpoint these use an ev_ prefix. All are optional and default to a no-op, so standard OpenAI clients that omit them receive identical output to before. These are deterministic audio/voicing controls (pitch shift, output gain, and EQ tone presets) — not generative effects.
Audio parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| ev_pitch | number | No | Pitch shift in semitones. Range -4 to +4. Default: 0 (no shift). Applied on the Kokoro voices; cloned (voice_*) and Arabic (ar_*) voices ignore it for now. |
| ev_volume_db | number | No | Output gain in dB. Range -6 to +6. Default: 0 (no change). |
| ev_tone | string | No | EQ tone preset: "neutral" (default), "warm", "bright", or "bass". |
| <break> in input | markup | No | Insert silence: embed <break time="500ms"/> or <break time="0.5s"/> in the input text. Max 3000ms per break. |
On the web app (POST /api/tts/generate) the same controls are sent without the prefix: pitch, volume_db, and tone (same ranges and defaults). Out-of-range values are rejected with 400.
Example request
curl -X POST https://easyvoice.ae/api/v1/audio/speech \
-H "Authorization: Bearer ev_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"model": "kokoro-82m",
"input": "Take a breath. <break time=\"500ms\"/> Now continue.",
"voice": "af_aoede",
"ev_pitch": 2,
"ev_volume_db": -1.5,
"ev_tone": "warm"
}' \
--output speech.mp3Word-level timestamps — ev_timestamps (not available yet)
Word timings are not available yet. The field is still accepted: passing ev_timestamps: true on a Kokoro voice changes the response from raw audio bytes to a JSON envelope, { audio: <base64>, format, words, timestamps_available }, but today words is null and timestamps_available is false. EasyVoice does not produce caption (SRT/VTT) files. Check timestamps_available before reading words.
// Request
{
"model": "kokoro-82m",
"input": "Hello world.",
"voice": "af_aoede",
"ev_timestamps": true
}
// Response today (JSON envelope, no word timings)
{
"audio": "<base64-encoded mp3>",
"format": "mp3",
"words": null,
"timestamps_available": false
}Scope: Arabic (ar_*) and cloned (voice_*) voices reject the field with 400 timestamps_not_supported. When omitted or false, the endpoint returns raw audio bytes unchanged (fully backward-compatible).
Pauses — <break>
Embed <break time="500ms"/> (or <break time="0.5s"/>) anywhere in the input text to insert a silent pause. Accepts milliseconds (ms) or seconds (s), capped at 3000ms per break.
Pronunciation overrides — [word](/phonemes/)
Embed [word](/phonemes/) inline in your input text to control pronunciation using misaki IPA notation. Example: The [Louisville](/lˈuːɪvɪl/) skyline.
Python Example
from openai import OpenAI
client = OpenAI(
api_key="ev_your_api_key",
base_url="https://easyvoice.ae/api/v1"
)
response = client.audio.speech.create(
model="kokoro-82m",
voice="af_aoede",
input="Hello from EasyVoice!"
)
response.stream_to_file("output.mp3")Node.js Example
import OpenAI from "openai";
const client = new OpenAI({
apiKey: "ev_your_api_key",
baseURL: "https://easyvoice.ae/api/v1"
});
const mp3 = await client.audio.speech.create({
model: "kokoro-82m",
voice: "af_aoede",
input: "Hello from EasyVoice!"
});
const buffer = Buffer.from(await mp3.arrayBuffer());
await fs.promises.writeFile("output.mp3", buffer);Plans, Rate Limits & Fair Use
| Limit | Free | Pro ($9.99/mo) or 7-day pass ($4.99) |
|---|---|---|
| Characters | 5,000/day, shared with the web app. Resets midnight UTC. | Unlimited — fair use 10M characters per rolling 30 days, all surfaces together ≈ 180+ hours of audio |
| Past fair use | Not applicable (the daily pool above) | Slowed, never blocked or billed: 30 requests and 10,000 characters/min per account (10 and 3,000/min past 20M), characters on every surface incl. the studio; jobs queue behind others for up to 30 min and start when the budget allows |
| Requests per minute | 20 | 60 |
| Concurrent requests | 2 | No per-key cap |
| Max input per request | 5,000 characters | 50,000 (Kokoro) · 12,000 (Arabic) · 2,000 (cloned) — longer via the Jobs API |
| Voices | 12 free voices | All 66 + your cloned voices |
A 7-day pass ($4.99, one time, no subscription) gives a key these same limits for 7 days. The Jobs API (POST /v1/jobs) and the MCP long-form and podcast tools need Pro.
Fair use, in plain words: Pro is unlimited for real products and real audiences — narrate your videos, your app, your audiobooks without counting characters. 10,000,000 characters is roughly 180+ hours of finished audio. Fair use is 10,000,000 characters per rolling 30 days, counted across the studio, the API, MCP, long-form jobs and podcasts together. Past that line the account is slowed, never blocked or billed: generation is paced per account to 10,000 characters a minute (3,000 characters a minute past 20,000,000) on every surface, the studio included, and API and MCP requests, across all of the account's keys, to 30 a minute (10 a minute past 20,000,000). A paced request waits up to 20 seconds for its turn and is then served; a longer wait answers HTTP 429 fair_use_paced with Retry-After. Jobs wait behind other customers' jobs for up to 30 minutes and start when the account's character budget allows. The slowdown lifts by itself once the rolling total falls back below the line. No overage charges, ever. Reselling raw API capacity is the one thing fair use excludes, and the thresholds may change with notice. Selling what you make? See the full commercial-rights breakdown → /license.
Seeing where you stand: every Pro response from /v1/audio/speech, /v1/jobs and MCP carries X-EasyVoice-Fair-Use (ok or throttled), X-EasyVoice-Fair-Use-Chars-30d (your rolling 30-day total) and X-EasyVoice-Fair-Use-Limit. While slowed, X-EasyVoice-Fair-Use-Rpm is your paced request rate, X-EasyVoice-Fair-Use-Cpm your characters-per-minute budget and X-EasyVoice-Fair-Use-Delay-Ms how long that request waited for its turn. Your dashboard shows the same rolling total and, while slowed, the pace.
When you hit a limit the API returns a structured 429 with a Retry-After header: rate_limit_exceeded (per-minute window), concurrency_limit_exceeded (too many in flight), daily_quota_exceeded (free daily pool spent — reset_at in the body tells you when it refills), or fair_use_paced (a slowed account's request would wait longer than 20 seconds — for its turn in a burst of parallel requests, or for its characters-per-minute budget — retry after Retry-After, the seconds until it fits; over MCP the same message comes back as a tool error). Requesting a Pro voice with a free key returns 403 plan_required naming the voice.
Common errors
The four classified error codes any of EasyVoice's API surfaces can return, with the exact current server message and a link to the relevant section on this page.
Request too long for your plan
Input too long for a free API key. Free keys share the web free tier's 5,000 characters/day; Pro ($9.99/mo) raises the per-request limit to 50,000 characters.
See plans and rate limits →Daily limit reached
Daily free limit reached: 5,000/5,000 characters used today (shared with the web app). Resets at midnight UTC. A 7-day pass ($4.99, no subscription) or Pro ($9.99/mo) removes the daily limit and gives the same key all 66 voices and 60 requests a minute; Pro also adds long-form jobs. Pro: https://easyvoice.ae/pricing?autostart=monthly | 7-day pass: https://easyvoice.ae/pricing?autostart=pass7d
See plans and rate limits →Pro voice on a free key
'bm_george' is a Pro voice. Free API keys include 12 voices (10 English + ar_m1, ar_f1) — try voice: "af_aoede", or upgrade to Pro ($9.99/mo) for all 66 voices: https://easyvoice.ae/pricing
See voice library →Voice not found
Voice not found. Check the voice ID against GET /v1/voices or the voice browser at /voices.
See voice library →Available Voices
66 voices across 9 languages. Use the voice ID in your API requests.
Free API keys include 12 voices: af_aoede, af_heart, af_bella, af_nova, af_sky, am_adam, am_eric, am_michael, bf_emma, bm_daniel, plus Arabic ar_m1 and ar_f1. The other 54 return 403 plan_required on a free key.
See the voice browser for the full list with audio previews.
Arabic voices: ar_m1–ar_m5 and ar_f1–ar_f5 work directly in POST /v1/audio/speech. Arabic numerals, dates, and AED amounts are normalized to spoken form automatically. See Arabic text to speech.
Pro subscribers can also use cloned voice IDs (e.g. voice_abc123) returned by the GET /v1/voices endpoint.
Voice Cloning API
Pro RequiredEnroll, list, and delete custom voice clones. Cloned voice IDs can be used directly in POST /v1/audio/speech requests. Requires a Pro subscription and explicit consent for each enrolled voice.
POST /api/v1/voices — Enroll a voice clone
Multipart form upload. Returns 202 with {voice_id, status:"enrolling"}. Enrollment typically completes in 1–2 minutes; poll GET /v1/voices for status.
curl -X POST https://easyvoice.ae/api/v1/voices \ -H "Authorization: Bearer ev_your_api_key" \ -F "name=My Voice" \ -F "consent=true" \ -F "audio=@sample.wav"
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Display name for the cloned voice |
| consent | string | Yes | Must be "true" — attests you own/have consent to clone this voice |
| audio | file | Yes | WAV, MP3, MP4, or M4A, up to 10 MB. 10–30 seconds of clear speech — enrollment rejects shorter or longer clips. |
GET /api/v1/voices — List voice clones
Returns all cloned voices for the authenticated user. Use status === "ready" voices in speech requests.
curl https://easyvoice.ae/api/v1/voices \
-H "Authorization: Bearer ev_your_api_key"
# Response:
# {
# "voices": [
# { "id": "voice_abc123", "name": "My Voice", "status": "ready", "createdAt": 1234567890 }
# ]
# }DELETE /api/v1/voices/{id} — Delete a voice clone
Permanently deletes the voice clone and its embedding. Returns 200 on success.
curl -X DELETE https://easyvoice.ae/api/v1/voices/voice_abc123 \ -H "Authorization: Bearer ev_your_api_key"
Using a cloned voice in speech synthesis
Pass the voice_* ID as the voice field in any speech request. Pro required.
curl -X POST https://easyvoice.ae/api/v1/audio/speech \
-H "Authorization: Bearer ev_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"model": "kokoro-82m",
"input": "Hello, this is my cloned voice!",
"voice": "voice_abc123"
}' \
--output speech.mp3Using cloned voices commercially? Full rights and consent rules → /license
Async Jobs API
For long inputs, submit an asynchronous job instead of waiting on the synchronous endpoint. Jobs are queued, processed, and the result is fetched by polling. Works with any voice ID, including Arabic and cloned voices. API-key access to the Jobs API requires Pro — free keys use the synchronous /v1/audio/speech endpoint above.
POST /api/v1/jobs — Submit a TTS job
curl -X POST https://easyvoice.ae/api/v1/jobs \
-H "Authorization: Bearer ev_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"input": "A long article text...",
"voice": "af_aoede",
"format": "mp3"
}'
# 202 Accepted
# { "id": "1f6f7c2e-...", "status": "queued" }GET /api/v1/jobs/{id} — Poll job status
curl https://easyvoice.ae/api/v1/jobs/1f6f7c2e-... \
-H "Authorization: Bearer ev_your_api_key"
# { "id": "1f6f7c2e-...", "status": "completed",
# "audio_url": "/audio/....mp3", "created_at": ..., "completed_at": ... }Statuses: queued → active → completed / failed. Jobs are visible only to the account that created them. A slowed account's queued job also carries fair_use: while it waits for the characters-per-minute budget, {"waiting": true, "starts_in_seconds": 420} says when it starts — keep polling; resubmitting would queue a second job.
Optional: callback_url (webhook on completion)
Add callback_url to a job submission (long-form or podcast) and EasyVoice POSTs the completed job payload to that URL instead of you polling. Requires Bearer API-key auth and an https:// URL. Delivered on success only — a failed job still requires polling GET /v1/jobs/{id}; retried up to 3 times (2s/4s backoff) on a non-2xx response or network error.
curl -X POST https://easyvoice.ae/api/v1/jobs \
-H "Authorization: Bearer ev_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"type": "longform",
"input": "A long article text...",
"voice": "af_aoede",
"callback_url": "https://your-endpoint.example/webhook"
}'
# 202 Accepted -- on completion, EasyVoice POSTs the job payload to callback_url
# with header: X-EasyVoice-Signature: sha256=<hex>Verify the signature: compute sha256(your_raw_api_key) as hex, then HMAC-SHA256 that key over the raw request body; compare (constant-time) to the hex digest after sha256= in the X-EasyVoice-Signature header.
Long text (no per-request limit)
Set the X-EasyVoice-Long-Text: async header on an oversized POST /v1/audio/speech request and there's no character limit — it's queued to the async Jobs API instead of rejected. Requires Pro and a Kokoro-compatible voice (not Arabic or cloned); under-threshold requests and requests without the header are completely unaffected — same behavior as today.
On MCP, text_to_speech does this automatically and unconditionally — no header or opt-in needed. An over-cap call (over 8,000 characters, 4,000 for Arabic ar_* voices) is queued as a background job instead of erroring; poll get_job_status with the returned job ID for the result.
Oversized request with the opt-in header
curl -X POST https://easyvoice.ae/api/v1/audio/speech \
-H "Authorization: Bearer ev_your_api_key" \
-H "Content-Type: application/json" \
-H "X-EasyVoice-Long-Text: async" \
-d '{
"model": "kokoro-82m",
"input": "A very long article, over 50,000 characters...",
"voice": "af_aoede"
}'
# 202 Accepted
# { "id": "1f6f7c2e-...", "status": "queued", "job_url": "/api/v1/jobs/1f6f7c2e-..." }Poll the returned job_url
curl https://easyvoice.ae/api/v1/jobs/1f6f7c2e-... \
-H "Authorization: Bearer ev_your_api_key"
# { "id": "1f6f7c2e-...", "status": "completed",
# "audio_url": "/audio/....mp3", "created_at": ..., "completed_at": ... }Without the header (or with any other header value), an oversized request keeps returning today's existing error — see the ceilings table above and the input_too_long card in Common errors.
Podcast API
Pro RequiredGenerate two-host dialogue podcasts from a list of script segments. Submit a job via POST /api/v1/jobs with type:"podcast", then poll for the stitched result. Per-segment audio URLs are returned alongside the final episode URL.
POST /api/v1/jobs — Submit a podcast job
curl -X POST https://easyvoice.ae/api/v1/jobs \
-H "Authorization: Bearer ev_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"type": "podcast",
"voices": { "A": "af_aoede", "B": "am_echo" },
"format": "mp3",
"segments": [
{ "speaker": "A", "text": "Welcome to the show. Today we explore the future of AI." },
{ "speaker": "B", "text": "Thanks for having me. AI is evolving at a remarkable pace." },
{ "speaker": "A", "text": "Absolutely. What do you think the next five years will look like?" },
{ "speaker": "B", "text": "I expect multimodal models to become the default in most apps." }
]
}'
# 202 Accepted
# { "id": "3a9f1b7c-...", "status": "queued" }GET /api/v1/jobs/{id} — Poll podcast job
curl https://easyvoice.ae/api/v1/jobs/3a9f1b7c-... \
-H "Authorization: Bearer ev_your_api_key"
# {
# "id": "3a9f1b7c-...",
# "status": "completed",
# "audio_url": "/audio/episode-3a9f1b7c.mp3",
# "segments": [
# { "speaker": "A", "audioUrl": "/audio/seg-0.mp3" },
# { "speaker": "B", "audioUrl": "/audio/seg-1.mp3" },
# { "speaker": "A", "audioUrl": "/audio/seg-2.mp3" },
# { "speaker": "B", "audioUrl": "/audio/seg-3.mp3" }
# ],
# "error": null,
# "created_at": 1749500000000,
# "completed_at": 1749500045000
# }Tier limits & voice options
| Plan | Max chars / episode | Cloned hosts |
|---|---|---|
| Free | 2,000 | No |
| Pro | 30,000 | Yes — use voice_* IDs |
Use standard voice IDs (e.g. af_aoede, am_echo) for hosts A and B. Pro subscribers can assign cloned voice_* IDs returned by GET /v1/voices.