Migrate from OpenAI TTS to EasyVoice: change the base URL and key
Migrating off OpenAI's TTS API means changing the client constructor: your EasyVoice key plus base_url="https://easyvoice.ae/api/v1" — and you keep the official openai SDK. EasyVoice's endpoint is OpenAI-compatible: the request body shape (model, voice, input, response_format), the Bearer-token auth, and the response handling (raw audio bytes returned as a complete file) all match, and the six OpenAI voice names (alloy, echo, fable, onyx, nova and shimmer) work as they are. This guide walks through the full migration on a real openai.audio.speech.create call: the before/after Python diff, the equivalent Node diff, a plain-HTTP (requests / fetch) version if you'd rather drop the SDK, how the model parameter is handled (any model string is accepted and ignored), and response_format compatibility (request mp3, wav or pcm). The code samples below use real endpoints — set the right API keys and they run. Only nova (af_nova) is on the free tier; a free key that sends one of the other five gets 403 plan_required. The newer OpenAI names (ash, ballad, coral, sage, verse, marin and cedar) are not mapped: a Pro or pass key that sends one gets 400 invalid_voice.
5,000 characters per day free, no credit card. Pro $9.99/mo unlimited (fair use 10M characters per 30 days).
The diff — Python
Existing OpenAI code uses the official openai Python SDK, and it keeps working: the SDK accepts a custom base_url, and EasyVoice's /api/v1/audio/speech speaks the same request shape. The official Python SDK was tested live against production on 2026-09-15 (versions 2.54.0 and 3.13.0) — see /api-docs. The before/after below is the literal diff: change the client constructor, set EASYVOICE_API_KEY, and you're done.
The change is the client constructor: construct the client with api_key=os.environ["EASYVOICE_API_KEY"] and base_url="https://easyvoice.ae/api/v1" instead of the OpenAI defaults. The voice stays as it is: the six OpenAI voice names work as they are (alloy → af_alloy, echo → am_echo, fable → bm_fable, onyx → am_onyx, nova → af_nova, shimmer → bf_lily). The model parameter can stay too — EasyVoice accepts any model string and ignores it, so tts-1, tts-1-hd or kokoro-82m all work — and the endpoint path, the request shape and the Bearer key header stay the same. Errors come back in an OpenAI-style error envelope (error.message, error.type, error.code and a request_id), and EasyVoice adds error codes of its own, listed on the API reference spoke. If you'd rather drop the SDK dependency, the plain-requests version further down makes the same call directly over HTTP.
The diff — JavaScript / TypeScript
JavaScript apps using the OpenAI Node SDK make the same change: construct the client with apiKey: process.env.EASYVOICE_API_KEY and baseURL: "https://easyvoice.ae/api/v1". The voice name stays as it is. The response.arrayBuffer() → Buffer.from() pattern handles the audio response exactly as before. If you'd rather not depend on the SDK, a plain fetch POST to https://easyvoice.ae/api/v1/audio/speech with an Authorization: Bearer header does the same job (fetch is global in Node 18+).
For frameworks: Next.js Route Handlers should call EasyVoice server-side, not from the client, to avoid leaking the API key to the browser. Express, Hono, Fastify, and Bun's built-in HTTP server all use the same fetch pattern. If your existing OpenAI integration runs inside a Cloudflare Worker, Vercel Edge Function, or other edge runtime, the EasyVoice migration runs there too — no special edge-compatibility flags needed.
Model parameter
OpenAI's API accepts model: 'tts-1' (faster) or model: 'tts-1-hd' (higher fidelity). EasyVoice runs Kokoro-82M as a single model serving all voices — fidelity isn't gated by a model name. EasyVoice accepts any string in the model field and ignores it, so model: 'tts-1', 'tts-1-hd' or 'kokoro-82m' in your request body all succeed (no 400 error). This is intentional: existing OpenAI-shaped code with the model parameter still set works as-is, no defensive code needed.
If you want to be explicit, send model: 'kokoro-82m' in the request (the id /api-docs uses). You don't need to scrub 'tts-1' or 'tts-1-hd' out of your codebase before migrating — the point of accepting any model value is to remove a migration step. The same goes for stream_format and instructions, which newer OpenAI models take: EasyVoice accepts them and ignores them. Quality is a judgment call, not a spec-sheet number: OpenAI's tts-1-hd can win on close-mic'd character voice work, so run your own blind A/B on real content from your product (see the voices spoke) before committing.
Voice parameter
The six OpenAI voice names work as they are, so the voice field in your existing code can stay. EasyVoice resolves each name to a voice ID: alloy → af_alloy, echo → am_echo, fable → bm_fable, onyx → am_onyx, nova → af_nova, shimmer → bf_lily. The full 6-row mapping table, with tone notes and sample scripts, lives on the /voices spoke. Only nova (af_nova) is on the free tier; a free key that sends one of the other five gets 403 plan_required. The newer OpenAI names — ash, ballad, coral, sage, verse, marin and cedar — are not mapped: a Pro or pass key that sends one gets 400 invalid_voice, so use one of the six names above or an EasyVoice voice ID instead.
Optional: if you want to use EasyVoice voice IDs directly — for example to reach the other 60 voices in the catalog — put the ID in the voice field (af_nova, bm_fable, and so on). The OpenAI names keep working next to them, so you can move one voice at a time. The code sample below lists the mapping if you want to pin the IDs in your own config; nothing in the migration requires it.
Response format compatibility — mp3, wav, pcm
Request mp3, wav or pcm. EasyVoice accepts response_format: 'mp3' (default), 'wav' and 'pcm' (raw 16-bit mono PCM). OpenAI additionally accepts opus, aac and flac: EasyVoice accepts those values too, but the body it sends back is MP3 with HTTP 200, so do not rely on them. An mp3 file from OpenAI plays in the same player as an mp3 file from EasyVoice, with the same MIME type (audio/mpeg) and the same downstream library compatibility.
wav is the right choice for downstream audio processing pipelines — mixing into a podcast track, applying filters in Audacity, importing into a DAW — and pcm is the raw-samples option for tools that want headerless audio. Both vendors return raw audio bytes — there's no JSON envelope to parse, no base64 to decode. response.arrayBuffer() (JS), response.content (Python requests), or response.body in any other language returns the audio directly. The full file is returned in one response; write it to a file or feed it to a player once received.
Response handling — receiving the complete audio file
EasyVoice's endpoint returns the complete audio file in one response — the full MP3 or WAV buffer is synthesized server-side and returned once Kokoro finishes. There is no chunked/streaming endpoint; the response body contains the complete file. Because Kokoro synthesis is fast (~1.1 ms/char on GPU), typical requests return in a few seconds for standard narration lengths. Read the response as a complete buffer: response.arrayBuffer() in JavaScript, response.content in Python requests, ioutil.ReadAll(resp.Body) in Go. Write it directly to a file or feed it to a player once received.
OpenAI's audio.speech.create uses chunked transfer-encoding to begin sending audio before synthesis completes, which gives lower first-byte latency at the cost of a more complex client consume pattern (ReadableStream / iter_content). EasyVoice does not offer this mode — there is no streaming, and every request returns the complete file. For the dominant TTS use cases (narration, chatbot, accessibility, IVR), the synthesis speed of Kokoro (~1.1 ms/char on GPU) means full-file response latency is typically under a few seconds for standard inputs.
What stays the same — auth, the error envelope, caching
Bearer token authentication: identical. Both APIs accept Authorization: Bearer YOUR_KEY in the request header. Your existing pattern (env var → request header) works as-is, just with EASYVOICE_API_KEY in place of OPENAI_API_KEY. Both keys are rotatable from the respective vendor dashboards; if you need both vendors active during migration (e.g. existing-user cohort on OpenAI, new-user cohort on EasyVoice), keep both env vars set and route at the call site.
Error handling: EasyVoice answers with HTTP 401 (invalid key), 400 (malformed request), 429 (rate limit) and 5xx (server error) in an OpenAI-style error envelope, and adds error codes of its own — plan_required (403, a free key asking for a Pro voice), voice_not_found, invalid_voice and fair_use_paced — which the API reference spoke lists. Caching: if your app caches generated audio by (text, voice, format) hash, the cache works identically — the audio bytes returned are deterministic for the same input across requests, so cache hit rates don't change after migration. The full migration commit usually touches 1-3 files: the TTS service module, the env var schema (.env.example), and the deployment config that sets EASYVOICE_API_KEY.
Code samples
Real working code, not pseudo-code. Every request below assumes you've set EASYVOICE_API_KEY and OPENAI_API_KEY as env vars where shown.
Before — OpenAI Python
Existing openai SDK callimport os
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
response = client.audio.speech.create(
model="tts-1",
voice="nova",
input="Hello, this is OpenAI's TTS API.",
response_format="mp3",
)
with open("out.mp3", "wb") as f:
f.write(response.content)After — EasyVoice Python
Same openai SDK — only the client constructor changesimport os
from openai import OpenAI
client = OpenAI(api_key=os.environ["EASYVOICE_API_KEY"], base_url="https://easyvoice.ae/api/v1") # changed
response = client.audio.speech.create(
model="tts-1", # accepted and ignored (or "kokoro-82m")
voice="nova", # unchanged: nova is a free voice, the other five OpenAI names need Pro
input="Hello, this is EasyVoice TTS API.",
response_format="mp3",
)
with open("out.mp3", "wb") as f:
f.write(response.content)Before — OpenAI JavaScript
Existing openai Node SDK callimport OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const response = await client.audio.speech.create({
model: "tts-1",
voice: "nova",
input: "Hello, this is OpenAI's TTS API.",
response_format: "mp3",
});
const buffer = Buffer.from(await response.arrayBuffer());
require("fs").writeFileSync("out.mp3", buffer);After — EasyVoice JavaScript
Same openai Node SDK — only the client constructor changesimport OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.EASYVOICE_API_KEY, baseURL: "https://easyvoice.ae/api/v1" }); // changed
const response = await client.audio.speech.create({
model: "tts-1", // accepted and ignored (or "kokoro-82m")
voice: "nova", // unchanged: nova is a free voice, the other five OpenAI names need Pro
input: "Hello, this is EasyVoice TTS API.",
response_format: "mp3",
});
const buffer = Buffer.from(await response.arrayBuffer());
require("fs").writeFileSync("out.mp3", buffer);Voice map — both languages
Optional reference: EasyVoice resolves the OpenAI names for you; use this only if you want to pin EasyVoice voice IDs in your own config# Reference only: EasyVoice already maps the OpenAI names on its side.
# Python — voice_map.py
OPENAI_TO_EASYVOICE = {
"alloy": "af_alloy",
"echo": "am_echo",
"fable": "bm_fable",
"onyx": "am_onyx",
"nova": "af_nova",
"shimmer": "bf_lily",
}
# JavaScript — voiceMap.ts
# export const OPENAI_TO_EASYVOICE = {
# alloy: "af_alloy",
# echo: "am_echo",
# fable: "bm_fable",
# onyx: "am_onyx",
# nova: "af_nova",
# shimmer: "bf_lily",
# } as const;Without the SDK — Python (requests)
Receives the complete audio file in one response — write directly to disk# af_nova (the match for OpenAI's nova) is a free voice, so this runs on a free key;
# Pro voices such as af_alloy return 403 plan_required on a free key
import os, requests
res = requests.post(
"https://easyvoice.ae/api/v1/audio/speech",
headers={"Authorization": f"Bearer {os.environ['EASYVOICE_API_KEY']}"},
json={"voice": "af_nova", "input": "Hello from EasyVoice.", "response_format": "mp3"},
)
res.raise_for_status()
# res.content is the complete audio file (all bytes buffered server-side before delivery)
with open("out.mp3", "wb") as f:
f.write(res.content)Without the SDK — JavaScript (fetch)
Receives the complete audio file in one response — write or play once received// af_nova (the match for OpenAI's nova) is a free voice, so this runs on a free key;
// Pro voices such as af_alloy return 403 plan_required on a free key
const res = await fetch("https://easyvoice.ae/api/v1/audio/speech", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.EASYVOICE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
voice: "af_nova",
input: "Hello from EasyVoice.",
response_format: "mp3",
}),
});
// The response is a complete audio file — all bytes arrive together
const buffer = Buffer.from(await res.arrayBuffer());
require("fs").writeFileSync("out.mp3", buffer);Voices to try
Every voice below is callable via the same voice parameter — preview audio samples and read the full character profile.
Frequently asked questions
What do I change to migrate from OpenAI TTS to EasyVoice?▾
Change the base URL and key: construct the client with your EasyVoice key and base_url="https://easyvoice.ae/api/v1" (baseURL in Node). The six OpenAI voice names work as they are (nova is free; the other five need Pro), and the model parameter can stay as it is. The endpoint path, the request shape and the Bearer key header stay the same, and errors use an OpenAI-style error envelope; EasyVoice adds error codes of its own, listed on the API reference spoke. Request mp3, wav or pcm as the response format.
Can I keep using the openai Python or Node SDK?▾
Yes. Set base_url (Python) or baseURL (Node) to https://easyvoice.ae/api/v1 and use your EasyVoice key. The official Python SDK was tested live against production on 2026-09-15 (versions 2.54.0 and 3.13.0), including its typed AuthenticationError on an invalid key — see /api-docs. If you'd rather drop the dependency, a plain requests or fetch POST to /api/v1/audio/speech works too.
What about the model parameter — tts-1 vs tts-1-hd?▾
Keep it or drop it. EasyVoice serves every Kokoro voice from Kokoro-82M — fidelity isn't gated by a model name. The model field accepts any string and is ignored, so model: 'tts-1', 'tts-1-hd' or 'kokoro-82m' all succeed (no 400 error). This is intentional drop-in compatibility — your existing OpenAI-shaped code with the model parameter set still works as-is, no defensive code changes needed pre-migration.
Which response formats work: mp3, wav or pcm?▾
Request mp3, wav or pcm. Both APIs return raw audio bytes (not JSON-wrapped, not base64-encoded) for response_format mp3 and wav, and an mp3 from OpenAI plays in the same player as an mp3 from EasyVoice; pcm returns raw 16-bit mono PCM. EasyVoice accepts opus, aac and flac as values too, but the body it returns is MP3 with HTTP 200.
How does EasyVoice return the audio response?▾
EasyVoice returns the complete audio file in one response — not chunked streaming. The endpoint buffers the full file server-side and returns it once Kokoro finishes synthesis. Because Kokoro is fast (~1.1 ms/char on GPU), typical requests complete in a few seconds. Read the response with response.arrayBuffer() in JS, response.content in Python, or equivalent — write the buffer to a file or feed it to a player once received. EasyVoice does not currently offer a chunked/streaming TTS endpoint.
How do I run both vendors during migration?▾
Keep both env vars set (OPENAI_API_KEY and EASYVOICE_API_KEY), route at the call site by user cohort or feature flag. Common patterns: cohort by user creation date (existing users keep OpenAI, new users go EasyVoice), feature flag per user, or content-type routing (chatbot responses go EasyVoice for cost, brand voiceover stays OpenAI temporarily). Both keys can rotate independently.
Related OpenAI migration guides
OpenAI TTS voices, mapped to Kokoro alternatives
Map OpenAI's six original TTS voices (alloy, echo, fable, onyx, nova, shimmer) to the closest EasyVoice Kokoro voices: tones, when to use each, sample scripts.
OpenAI TTS pricing vs EasyVoice — when flat-rate wins
OpenAI TTS costs $15 per 1M characters on tts-1 and $30 on tts-1-hd, with no free tier. Monthly cost at 100K to 10M characters and the $9.99 flat breakeven.
OpenAI is removing tts-1, tts-1-hd and the gpt-4o-mini-tts snapshots on 6 January 2027
OpenAI removes tts-1, tts-1-hd and two gpt-4o-mini-tts snapshots on 6 January 2027. Switch the speech call to EasyVoice: change the base URL and key.
Vendor comparison: EasyVoice vs OpenAI TTS
Side-by-side feature comparison covering voices, languages, pricing tiers, free limits, API surface, and the why-people-look / where-each-wins breakdown.
API reference: /audio/speech, Jobs API and limits
Every request parameter, rate limit and error code, the Jobs API for long text, and copy-paste examples using the official openai SDK with the EasyVoice base URL.
Start migrating off OpenAI TTS today
5,000 characters per day free, no credit card. Pro $9.99/mo unlimited replaces per-character bills once they grow past the breakeven shown on the pricing page.