API Reference
Era Voice is a REST API for voice cloning, text-to-speech, dubbing and transcription. Create an API key on the API Keys page and send it as a Bearer token with every request. The whole surface is described by an OpenAPI 3.1 document and wrapped by Python and JavaScript SDKs.
Authentication
Every /v1 endpoint requires Authorization: Bearer ev_…. Keys are shown once at creation; revoke them any time in the portal.
Key scopes
Every key carries a set of scopes chosen when it is created (all of them unless you untick some). A request outside the key's scopes is refused with 403 naming the missing scope, so a key that only speaks cannot list, clone or read usage.
tts— POST /v1/tts, POST /v1/tts/stream, POST /v1/dialogue and the MCP voice_speak toolvoices:read— GET /v1/voices and the MCP voice_list toolvoices:write— POST /v1/voices, POST /v1/voices/design and the MCP voice_clone toolaudio:read— GET /v1/audio/:generation_idusage:read— GET /v1/usagealign— POST /v1/aligndubbing— the /v1/dubbing endpointstranscription— the /v1/transcriptions endpoints
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
{
"type": "about:blank",
"title": "Insufficient scope",
"status": 403,
"detail": "This API key does not have the \"tts\" scope required for POST /v1/tts. Create a key with that scope in the portal."
} Per-key monthly caps
Each key can have a monthly character cap and a separate transcription cap in seconds. Set either cap to zero to use the account allowance. Usage from other keys does not consume this key's cap; all keys share the account allowance. Reservations include work in progress, so concurrent requests cannot spend the same remaining allowance. Choose scopes and caps on the API Keys page; existing keys keep their chosen scopes.
Allowances reset each UTC calendar month. A request reserves its billing month when admitted; finishing after midnight does not move that charge into the next month. The usage report shows completion dates separately. Eligible speech reruns remain free at the cap. Failed transcription jobs release their reservation; successful recordings use their measured input duration.
HTTP/1.1 402 Payment Required
Content-Type: application/problem+json
{
"type": "about:blank",
"title": "Allowance exceeded",
"status": 402,
"detail": "request exceeds the remaining allowance"
} POST /v1/tts — generate speech
Body: {text, voice_id, language?, format?, normalize?, seed?}. Returns audio bytes. Languages: english, chinese, korean, japanese, german, french, russian, portuguese, spanish, italian.
textstring, required — Up to 10,000 characters. Longer than 2,000 is split on sentence boundaries, synthesized in order and stitched into one response.voice_idstring, required — Any voice from GET /v1/voices — your clones or the preset library.languagestring, default "auto" — One of the ten languages, or auto to detect it from the text. The resolved language comes back in X-Language.formatstring, default "wav" — Output audio format; see the table below.normalizestring, default "auto" — Spell out numbers, times, currency and abbreviations before synthesis. auto normalizes only when the text contains digits or abbreviations; on always; off never.seedinteger, optional — 0–4294967295, forwarded to the voice model's sampler and echoed as X-Seed. It narrows variation between runs; GPU sampling is not byte-deterministic, so the same seed does not reproduce identical audio.
curl -X POST https://voice.erasmuslabs.ai/v1/tts \
-H "Authorization: Bearer ev_your_key" \
-H "Content-Type: application/json" \
-d '{"text": "Hello from Era Voice.", "voice_id": "VOICE_ID", "language": "english", "format": "mp3"}' \
-o hello.mp3 POST /v1/dialogue — multi-voice dialogue
Send 1–50 ordered turns, each with voice_id and text, for one combined audio response. Use up to 10,000 total trimmed Unicode characters, including break markup, in at most 256 KiB of JSON. Each turn must contain speech. Turns join without an added gap; use explicit breaks for pauses.
Optional turn language overrides the global language; auto detection happens per turn. Format, speed, normalization, pronunciation and seed apply to the whole request. All TTS output formats work. The response includes X-Dialogue-ID, X-Chars, X-Audio-Ms and X-Regeneration. X-Language appears only when every turn uses the same language. The dialogue ID identifies accounting metadata; save the returned bytes to keep the audio.
curl -X POST https://voice.erasmuslabs.ai/v1/dialogue \
-H "Authorization: Bearer ev_your_key" \
-H "Content-Type: application/json" \
-d '{"turns":[{"voice_id":"VOICE_A","text":"Welcome to Era."},{"voice_id":"VOICE_B","text":"<break time=\"500ms\"/>Thank you."}],"language":"auto","format":"mp3","seed":42}' \
-o dialogue.mp3 A dialogue uses one dictionary snapshot and is charged once after all audio is complete. Failures before accounting commits return no partial audio and add no charge. Two identical whole-dialogue reruns within two hours of a paid take are free, including at the cap; changing only the seed preserves eligibility. Order, voices, effective speech and pauses, languages, speed and format determine a match. Repeated lines within a new dialogue are billed normally. Plan and key allowances are reserved before generation and settled atomically when charging. A connection failure after a completed charge does not automatically refund it. An error with an unresolved accounting outcome may still have committed and incurred a charge.
Combined limits are 128 break tags, 60 seconds of explicit silence, 20,000 characters after pronunciation replacement, 100,000 normalized characters and 128 synthesis calls. Output is limited to 15 minutes and 64 MiB. The work deadline is 115 seconds, with up to five additional seconds to resolve an uncertain accounting outcome after a lost commit acknowledgement. SDK dialogue methods default to 180 seconds and allow a custom timeout. Timestamps and streaming are unsupported.
Output formats
format picks the encoding. Everything is mono and derives from the model's 24 kHz output; lower rates are resampled with a windowed-sinc filter, so ulaw_8000 and alaw_8000 drop straight into a telephony bridge. The response carries the matching Content-Type and X-Audio-Ms.
wavaudio/wav — RIFF WAV, 24 kHz 16-bit mono PCM (default)mp3_24000_128audio/mpeg — MP3, 24 kHz, 128 kbps (alias: mp3)mp3_24000_160audio/mpeg — MP3, 24 kHz, 160 kbps — the highest rate MPEG-2 Layer III defines at 24 kHz (alias: mp3_24000_192)pcm_24000audio/L16; rate=24000 — raw signed 16-bit little-endian PCM, 24 kHz monopcm_16000audio/L16; rate=16000 — raw signed 16-bit little-endian PCM, 16 kHz monopcm_8000audio/L16; rate=8000 — raw signed 16-bit little-endian PCM, 8 kHz monoulaw_8000audio/basic — G.711 μ-law, 8 kHz (telephony)alaw_8000audio/PCMA — G.711 A-law, 8 kHz (telephony)
Aliases: mp3 means mp3_24000_128, and mp3_24000_192 resolves to mp3_24000_160 — MPEG‑2 Layer III defines no 192 kbps mode at 24 kHz. The resolved name is what the JSON format field reports back.
# 24 kHz MP3 at 128 kbps (the "mp3" alias)
curl -X POST https://voice.erasmuslabs.ai/v1/tts \
-H "Authorization: Bearer ev_your_key" -H "Content-Type: application/json" \
-d '{"text": "Hello.", "voice_id": "VOICE_ID", "format": "mp3_24000_128"}' \
-o hello.mp3
# 8 kHz G.711 mu-law for a telephony bridge (headerless, audio/basic)
curl -X POST https://voice.erasmuslabs.ai/v1/tts \
-H "Authorization: Bearer ev_your_key" -H "Content-Type: application/json" \
-d '{"text": "Hello.", "voice_id": "VOICE_ID", "format": "ulaw_8000"}' \
-o hello.ul
# 16 kHz raw PCM to feed straight into a recognizer (audio/L16; rate=16000)
curl -X POST https://voice.erasmuslabs.ai/v1/tts \
-H "Authorization: Bearer ev_your_key" -H "Content-Type: application/json" \
-d '{"text": "Hello.", "voice_id": "VOICE_ID", "format": "pcm_16000"}' \
-o hello.pcm Long text, language detection, normalization and seeds
Long text. Requests take up to 10,000 characters. Past 2,000 — the voice service's own limit — the text is split on sentence boundaries, synthesized in order and stitched back into one audio response, metered once. Word timestamps still work: the stitched audio is aligned as a whole.
Language. language defaults to auto: Han, Hangul, kana and Cyrillic are read from the script, and Latin-script text is scored on stopwords and diacritics. The result is always one of the ten supported languages (English when nothing distinguishes the text) and is returned in X-Language.
Normalization. normalize rewrites text the way a narrator reads it before it reaches the model: English numbers, ordinals, decimals, percentages, currency, years, clock times and common abbreviations (Dr., Mr., km, kg); Chinese Arabic digits as Han numerals for counts, dates and times; Korean digits as Sino-Korean for dates and minutes, native numerals for hours and counters. Identifiers, versions, IP addresses and phone numbers are left alone.
Seed. seed is forwarded to the voice model's sampler and echoed as X-Seed. It narrows how much two runs of the same text differ; it is not a reproducibility guarantee, because GPU sampling is not byte-deterministic.
# language omitted: the server detects it and answers X-Language: german
curl -sD - -X POST https://voice.erasmuslabs.ai/v1/tts \
-H "Authorization: Bearer ev_your_key" -H "Content-Type: application/json" \
-d '{"text": "Guten Morgen, wie geht es Ihnen heute?", "voice_id": "VOICE_ID"}' \
-o guten.wav
# normalize spells the numbers out: "nine forty-five", "twelve dollars and
# fifty cents", "third floor"
curl -X POST https://voice.erasmuslabs.ai/v1/tts \
-H "Authorization: Bearer ev_your_key" -H "Content-Type: application/json" \
-d '{"text": "Meeting at 9:45, $12.50, 3rd floor", "voice_id": "VOICE_ID", "normalize": "on"}' \
-o meeting.wav
# seed narrows the variation between runs (it does not reproduce audio byte for byte)
curl -X POST https://voice.erasmuslabs.ai/v1/tts \
-H "Authorization: Bearer ev_your_key" -H "Content-Type: application/json" \
-d '{"text": "Hello.", "voice_id": "VOICE_ID", "seed": 12345}' \
-o seeded.wav Response headers
X-Chars— Characters metered for this request.X-Audio-Ms— Duration of the returned audio in milliseconds.X-Language— The language used — the one you asked for, or the detected one.X-Regenerationfree | metered — free when the request was an unmetered rerun; see below.X-Seed— Present only when you sent a seed.
Free regenerations
Reruns are for tuning, not for paying twice. Repeating a request with the same text, voice, language and format within two hours of a metered generation is free — up to twice per metered run. Free reruns are recorded but not metered, and answer X-Regeneration: free; everything else answers metered. Change any of the four inputs, or come back after the two hours, and the request is metered again.
POST /v1/tts/stream — streaming (SSE)
Same body as /v1/tts (format, language, normalize and seed included), up to 20,000 characters. The server splits the text into sentences and streams one Server-Sent Event per synthesized chunk — first audio arrives after roughly one sentence. Each chunk is encoded in the requested format and carries its own container, so a wav or mp3 chunk plays on its own; the headerless formats (pcm_*, ulaw_8000, alaw_8000) concatenate into one continuous stream. The final event carries {"done":true, total_duration_ms, chars, language}; a mid-stream failure ends the stream with an {"error": …} event (only delivered chunks are metered). See the voice-agent quickstart.
curl -N -X POST https://voice.erasmuslabs.ai/v1/tts/stream \
-H "Authorization: Bearer ev_your_key" \
-H "Content-Type: application/json" \
-d '{"text": "First sentence. Second sentence. Third one!", "voice_id": "VOICE_ID", "language": "english"}'
# Server-Sent Events, one per sentence chunk, then a final done event:
# data: {"seq":1,"audio_base64":"...","duration_ms":1810,"format":"wav"}
# data: {"seq":2,"audio_base64":"...","duration_ms":1420,"format":"wav"}
# data: {"seq":3,"audio_base64":"...","duration_ms":990,"format":"wav"}
# data: {"done":true,"total_duration_ms":4220,"chars":47,"language":"english"} GET /v1/tts/websocket — streamed text input
Connect over WSS with an Authorization: Bearer ev_… header and a key with the tts scope. Send a start message, wait for ready, then send text frames and commit. Audio arrives as ordered JSON messages with type: audio, seq, audio_base64 and duration_ms, followed by done with total duration and character count. Each connection handles one utterance. Text is buffered until commit, and the model synthesizes complete sentence chunks before sending audio.
{"type":"start","voice_id":"VOICE_ID","language":"english","format":"wav"}
// After receiving {"type":"ready"}:
{"type":"text","text":"Welcome to Era Voice. "}
{"type":"text","text":"This short sample measures streaming latency."}
{"type":"commit"} Limits: 16 KiB per frame, 128 text frames, and 20,000 characters total. Connections have a five-minute read-idle deadline and a 15-second write deadline. Send {"type":"cancel"} after commit to cancel remaining inference. Disconnecting also cancels work; only successfully written audio chunks settle usage. See the measured latency notes.
POST /v1/tts?timestamps=word — word timestamps
With ?timestamps=word the response is JSON carrying the audio as base64 plus the resolved format and language and word-level timings (milliseconds) — built for karaoke-style highlighting and captions. Chunked long text is aligned as one stitched clip, so timings run across the whole request. words can be empty if the alignment service is temporarily unavailable; the audio is always returned.
curl -X POST "https://voice.erasmuslabs.ai/v1/tts?timestamps=word" \
-H "Authorization: Bearer ev_your_key" \
-H "Content-Type: application/json" \
-d '{"text": "Hello from Era Voice.", "voice_id": "VOICE_ID", "language": "english"}'
# 200 application/json
# {
# "audio_base64": "...",
# "format": "wav",
# "language": "english",
# "duration_ms": 1840,
# "words": [
# {"word": "Hello", "start_ms": 120, "end_ms": 480},
# {"word": "from", "start_ms": 480, "end_ms": 660}
# ]
# } GET /v1/voices — list voices
Returns your cloned voices plus the shared preset library. Library voices carry "library": true and are usable in /v1/tts like any other voice.
curl https://voice.erasmuslabs.ai/v1/voices -H "Authorization: Bearer ev_your_key" POST /v1/voices — clone a voice
Multipart form: sample (wav/mp3/m4a, max 10MB), name, optional language_hint, and the required consent_attestation — the exact sentence below. See the voice consent policy.
curl -X POST https://voice.erasmuslabs.ai/v1/voices \
-H "Authorization: Bearer ev_your_key" \
-F "name=My voice" \
-F "language_hint=english" \
-F "consent_attestation=I confirm I have the right to clone this voice and consent to its use." \
-F "sample=@sample.wav" POST /v1/voices/design — design a voice (preview)
Synthesizes a brand-new voice from a text description — no sample, no source speaker. Voice design is still rolling out; servers without it answer 501 Not Implemented.
curl -X POST https://voice.erasmuslabs.ai/v1/voices/design \
-H "Authorization: Bearer ev_your_key" \
-H "Content-Type: application/json" \
-d '{"name": "Nova", "description": "A calm elderly storyteller", "language": "english"}' POST /v1/transcriptions — transcribe recordings
Upload one recording per job as multipart audio, with optional language (auto, a supported language name or short code). WAV, MP3, M4A, MP4, WebM and OGG are accepted, up to 25 MiB and 15 decoded minutes. MP4 must contain audio. The complete request is limited to 26 MiB. This route requires the transcription scope; create a new key with it if your older key lacks it.
curl -X POST https://voice.erasmuslabs.ai/v1/transcriptions \
-H "Authorization: Bearer ev_your_key" \
-F "audio=@interview.m4a" -F "language=auto"
# 202 {"id":"TRANSCRIPTION_ID","status":"queued"}
# Poll until done, failed or expired:
curl https://voice.erasmuslabs.ai/v1/transcriptions/TRANSCRIPTION_ID \
-H "Authorization: Bearer ev_your_key"
# A done job includes:
# "duration_ms": 1840,
# "language": "en",
# "transcript": {
# "text": "Hello from Era Voice.",
# "segments": [{"text":"Hello from Era Voice.","start_ms":120,"end_ms":1380}],
# "words": [{"word":"Hello","start_ms":120,"end_ms":480}]
# } A 202 response contains id and status. Poll the job URL while it is queued or processing. A done job contains detected language, measured duration in milliseconds and a transcript with text, segments and words. Segment and word timings are milliseconds from the recording's start. Silence can succeed with empty text and timing arrays. Failed jobs include an error code and message.
Default monthly input allowances are 30 minutes for Free, 300 for Starter and 1,200 for Pro; your deployment can configure them. Transcription uses a separate input-audio allowance and no speech characters. Duration is measured before recognition and charged once on success, rounded up to the next millisecond. An allowance failure appears on the job after decoding.
List job metadata with GET /v1/transcriptions (limit 20 by default, at most 100; pass the returned next_cursor as before). Delete a job with DELETE /v1/transcriptions/:id. Content is private to your account. Source audio is removed after completion or failure; transcript content expires after 30 days by default and is hidden immediately at expiry. Deletion removes content and fences running work; a previously completed charge remains recorded.
For multiple files, the SDKs provide transcribe_batch and transcribeBatch: each file has its own server job and ordered outcome. The helpers submit at most four files concurrently (two by default), poll results and accept a caller timeout or cancellation. They do not automatically repeat a POST whose outcome is unknown. Check recent jobs before resubmitting after a connection failure; cancellation of a client wait does not delete an accepted job. The queue holds at most ten pending or cleanup-pending jobs per account. Capacity returns 429; an unavailable bounded decoder or disabled submissions returns 503.
POST /v1/dubbing — dub a recording into another language
Upload speech in one language and get the same speech in another, spoken in the original speaker’s voice or any voice you own, with subtitles for both scripts. Era Voice transcribes the recording, translates it segment by segment, re-voices each segment and lays it back on a silent timeline of the original length — so the dub stays in sync with the source video or slides.
Multipart form: audio (wav/mp3/m4a/mp4/webm/ogg, max 25MB and 15 minutes; MP4 must contain audio), target_language, optional source_language (default auto), optional voice_id, and optional webhook_url. Leave voice_id out to clone the source speaker: the first clear speech in the upload becomes a new voice on your account, so consent_attestation is then required — the same sentence as POST /v1/voices.
# Dub a recording into Spanish in the original speaker's voice.
curl -X POST https://voice.erasmuslabs.ai/v1/dubbing \
-H "Authorization: Bearer ev_your_key" \
-F "audio=@interview.wav" \
-F "target_language=spanish" \
-F "consent_attestation=I confirm I have the right to clone this voice and consent to its use."
# -> 202 {"job_id": "8cdcebf2-…", "status": "queued"} GET /v1/dubbing/:id — poll a job
Jobs run one at a time and move through queued → transcribing → translating → synthesizing → assembling → done, or failed with an error you can show a user. Poll every couple of seconds, or pass a webhook_url and wait.
curl https://voice.erasmuslabs.ai/v1/dubbing/JOB_ID -H "Authorization: Bearer ev_your_key"
{
"job_id": "8cdcebf2-…",
"status": "done",
"source_language": "english",
"target_language": "spanish",
"voice_id": "4791d705-…",
"duration_ms": 10492,
"chars": 174,
"audio_url": "https://voice.erasmuslabs.ai/v1/dubbing/8cdcebf2-…/audio",
"transcript_srt_url": "https://voice.erasmuslabs.ai/v1/dubbing/8cdcebf2-…/transcript.srt",
"translation_srt_url": "https://voice.erasmuslabs.ai/v1/dubbing/8cdcebf2-…/translation.srt",
"segments": [
{"i": 1, "start_ms": 300, "end_ms": 3542,
"text": "Hello, and welcome.", "translation": "Hola, y bienvenido.",
"dub_start_ms": 300, "dub_end_ms": 3541, "stretch": 1.058}
]
} Audio, subtitles and cleanup
A finished job serves its dub at /audio (24 kHz mono WAV, the same length as the source) and SubRip subtitles at /transcript.srt (what was said) and /translation.srt (what the dub says, timed where each segment actually landed). All three need the same Bearer key. A whole job is metered once, on the translated character count. Exact translated characters are reserved before speech synthesis against your account and key allowances. Failed attempts release that reservation; dubbing does not consume standalone transcription minutes or qualify for free reruns. Deleting a job removes its files; a cloned speaker voice stays in your account.
curl https://voice.erasmuslabs.ai/v1/dubbing/JOB_ID/audio \
-H "Authorization: Bearer ev_your_key" -o dubbed.wav
curl https://voice.erasmuslabs.ai/v1/dubbing/JOB_ID/translation.srt \
-H "Authorization: Bearer ev_your_key" -o dubbed.srt
# List every job, then clean up.
curl https://voice.erasmuslabs.ai/v1/dubbing -H "Authorization: Bearer ev_your_key"
curl -X DELETE https://voice.erasmuslabs.ai/v1/dubbing/JOB_ID -H "Authorization: Bearer ev_your_key" Dubbing webhooks
With webhook_url set, Era Voice POSTs a JSON body on dubbing.done and dubbing.failed, signed with X-Era-Signature: an HMAC-SHA256 of the raw body keyed with the stored hash of the API key that submitted the job. Non-2xx answers are retried three times.
POST https://your-app.example.com/hooks/era
X-Era-Signature: sha256=96716e00c79ed…
{"event": "dubbing.done", "job": {"job_id": "8cdcebf2-…", "status": "done", …}}
# Verify (Python):
# expected = "sha256=" + hmac.new(api_key_hash, raw_body, hashlib.sha256).hexdigest()
# hmac.compare_digest(expected, request.headers["X-Era-Signature"]) GET /v1/audio/:generation_id — download stored audio
Audio generated through the MCP voice_speak tool is stored and downloadable here with the same Bearer key (account-scoped).
curl https://voice.erasmuslabs.ai/v1/audio/GENERATION_ID \
-H "Authorization: Bearer ev_your_key" -o generation.wav GET /v1/usage — month-to-date usage
curl https://voice.erasmuslabs.ai/v1/usage -H "Authorization: Bearer ev_your_key" This API retains its speech-only month, chars, audio_seconds and generations fields, based on completion dates. The character_allowance object reports the current period (week or month), limit, settled used characters, starts_at and resets_at as UTC timestamps. Work is charged to its admission period, which can differ from completion dates. The Usage page reports transcription input time separately. Filter completed activity by an inclusive UTC date range of up to 366 days and by all keys, one key, or portal/unattributed requests. Filters do not change your character allowance. Download CSV for the same selection, with completion time, billing month, characters, generated audio and input-audio milliseconds. Exports above 50,000 events ask you to narrow the range.
POST /v1/align — forced alignment
Word timestamps for audio you already have: multipart audio (up to 25MB), text (its transcript, up to 20,000 characters) and an optional language (a name like english or a code like en; default en). The call runs through the same alignment service as word timestamps with a 60-second budget (504 when exceeded). It costs no characters — only the per-key rate limit applies — and needs the align scope.
curl -X POST https://voice.erasmuslabs.ai/v1/align \
-H "Authorization: Bearer ev_your_key" \
-F "audio=@narration.wav" \
-F "text=Hello from Era Voice." \
-F "language=english"
# 200 application/json
# {
# "language": "en",
# "words": [
# {"word": "Hello", "start_ms": 120, "end_ms": 480},
# {"word": "from", "start_ms": 480, "end_ms": 660},
# {"word": "Era", "start_ms": 660, "end_ms": 900},
# {"word": "Voice.", "start_ms": 900, "end_ms": 1380}
# ]
# } OpenAPI document
The full /v1 surface — every endpoint, parameter, header, scope and the problem+json error schema — is published as OpenAPI 3.1: openapi.json · openapi.yaml. Point Swagger UI, Postman, Bruno or a client generator at either URL; the servers entry already names this deployment.
SDKs
Thin typed clients over the REST API, installed straight from the repository (not published to PyPI or npm yet). Both expose tts, tts_stream / ttsStream, voices, create_voice / createVoice, design_voice / designVoice, usage, align and transcription jobs with batch helpers, and raise typed errors built from the problem+json body (authentication, scope, plan or key cap, validation, rate limit).
Python (3.9+, httpx)
pip install "git+https://github.com/alexfzh/era-voice.git#subdirectory=sdk/python"
from era_voice import EraVoice, CONSENT_ATTESTATION, EraVoiceError
client = EraVoice("ev_your_key", base_url="https://voice.erasmuslabs.ai")
voice = client.create_voice("My voice", open("sample.wav", "rb").read(),
consent_attestation=CONSENT_ATTESTATION, language_hint="english")
speech = client.tts("Hello from Era Voice.", voice.id, language="english", format="wav")
open("hello.wav", "wb").write(speech.audio) # speech.audio_ms, speech.chars
for chunk in client.tts_stream("First sentence. Second one.", voice.id, language="english"):
play(chunk.audio) # bytes per sentence, chunk.duration_ms
timings = client.align(open("hello.wav", "rb").read(), "Hello from Era Voice.", language="en")
print(timings.words[0].word, timings.words[0].start_ms)
try:
client.usage()
except EraVoiceError as e: # ScopeError, PlanLimitError, RateLimitError, …
print(e.status, e.title, e.detail) JavaScript / TypeScript (Node 18+, ESM)
git clone https://github.com/alexfzh/era-voice.git && npm install ./era-voice/sdk/js
import { EraVoice, CONSENT_ATTESTATION, EraVoiceError } from "era-voice";
import { readFile, writeFile } from "node:fs/promises";
const client = new EraVoice("ev_your_key", { baseUrl: "https://voice.erasmuslabs.ai" });
const voice = await client.createVoice({ name: "My voice", sample: await readFile("sample.wav"),
consentAttestation: CONSENT_ATTESTATION, languageHint: "english" });
const speech = await client.tts({ text: "Hello from Era Voice.", voiceId: voice.id, language: "english" });
await writeFile("hello.wav", Buffer.from(speech.audio)); // speech.audioMs, speech.chars
for await (const chunk of client.ttsStream({ text: "First sentence. Second one.", voiceId: voice.id, language: "english" })) {
play(chunk.audio); // Uint8Array per sentence, chunk.durationMs
}
const timings = await client.align({ audio: speech.audio, text: "Hello from Era Voice.", language: "en" });
console.log(timings.words[0]);
try { await client.usage(); } catch (e) { if (e instanceof EraVoiceError) console.log(e.status, e.title, e.detail); } Errors
Errors are RFC 7807 application/problem+json: 401 for bad keys, 403 when the key lacks a scope, 402 when the plan cap or the key's own cap is reached, 400 for validation, 404 for unknown voices, 429 for rate limits, 502 for upstream voice-service failures, 504 when alignment times out.
HTTP/1.1 402 Payment Required
Content-Type: application/problem+json
{
"type": "about:blank",
"title": "Allowance exceeded",
"status": 402,
"detail": "request exceeds the remaining allowance"
} Custom finetuned voices
Cloning covers most needs, but when a voice has to carry a register cloning cannot reach — a specific whisper, an accent, singing-adjacent delivery — we finetune a dedicated model per voice. It is a contact-driven service, not self-serve: request it from the Voices page (“Custom voices”) with your goal and any base voice, and we follow up on scope, data (usually 1–2h of clean recordings, with the same consent requirements as cloning), and pricing. Delivered voices appear in your account like any other voice and work across Text to Speech, Studio, the API, and MCP.
Plan caps
Free resets Monday at 00:00 UTC; paid character allowances reset on the first day of each month at 00:00 UTC. Unused characters do not roll over. Transcription allowances and per-key caps remain monthly. Pricing is provisional while Era Voice is in early access.