Give any application a voice.
Clone a voice from a single audio sample, then turn text into studio-mastered speech in that voice — over one simple REST API, authenticated with a single key.
Introduction
The JSF Labs API exposes two capabilities behind one gateway. You send audio and get back a reusable voice; you send text and get back speech in that voice. Both endpoints share the same authentication, rate limits, character-based billing, and error format, so once you've called one you already know how to call the other.
- Clone a voice — upload a short sample and receive a portable
jsf_voice ID. - Text to speech — pass that voice ID with your text and receive a hosted audio file, loudness-normalized and peak-safe.
Everything is synchronous: each request returns the finished result, so there are no jobs to poll and no webhooks to configure.
Base URL
All endpoints live under a single versioned base. Every path in these docs is written in full, including the version prefix.
https://www.jsflabs.io/api/v1
Requests must be sent over HTTPS. Plain HTTP is not supported.
Quickstart
Clone a voice, then speak with it — two calls, start to finish.
1 — Clone a voice from a sample
curl -X POST https://www.jsflabs.io/api/v1/voice-clone \ -H "x-api-key: jsf_your_api_key" \ -F "audio=@sample.wav" \ -F "name=Morgan" \ -F "gender=Female" \ -F "language=en"
2 — Speak with the cloned voice
curl -X POST https://www.jsflabs.io/api/v1/text-to-speech \ -H "x-api-key: jsf_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "text": "The quick brown fox jumps over the lazy dog.", "voice_id": "jsf_c3BlZWNoaWZ5X3ZvaWNlX2lk", "speed": 1 }'
Authentication
Every request is authenticated with your secret API key, sent in the x-api-key header. Keys always begin with jsf_. You can find and manage keys in your JSF Labs dashboard.
x-api-key: jsf_your_api_key
Requests with a missing key, a malformed key, an unknown key, an inactive subscription, or an expired key are rejected before any work is done. See Error codes for the exact responses.
Rate limits
Each API key may make up to 50 requests per minute, measured as a sliding window. The limit is per key, so separate keys have separate budgets.
When you exceed it, the API responds with 429 and tells you how long to wait:
Retry-After: 37 X-RateLimit-Limit: 50 X-RateLimit-Remaining: 0 { "success": false, "error": { "code": "RATE_LIMITED", "message": "Rate limit exceeded. You can make up to 50 requests per minute.", "retryAfterSeconds": 37 } }
Read the Retry-After header and back off for that many seconds before retrying. Need a higher ceiling? Contact support@jsflabs.io.
Credits & billing
Usage is metered in characters. Your key carries a total allocation and a running used count; the difference is your available balance.
- Text to speech costs one character per character of
textin the request. - Cloning is currently free — it costs zero characters.
Every successful response echoes your balance so you can track it without a separate call:
"credits": { "cost": 137, // charged by this request "used": 137, // total used so far "remaining": 49863, // what's left "total": 50000 // your allocation }
If a request would exceed your balance, it's rejected with 402 and INSUFFICIENT_CHARACTERS before any audio is generated — you're never charged for a request you couldn't afford.
Voice IDs
Cloning returns a portable voice identifier prefixed with jsf_. This is the only identifier you ever need — pass it straight into text to speech as voice_id.
jsf_c3BlZWNoaWZ5X3ZvaWNlX2lkTreat the value as opaque — don't parse or modify it. A value that doesn't start with jsf_, or that has been altered, is rejected with INVALID_VOICE_ID.
Clone a voice
Upload a clean audio sample and receive a reusable voice. Send the request as multipart/form-data.
Body — multipart/form-data
WAV or MP3, up to 25 MB. A clean 10–30 second recording with no background noise gives the best clone.Male, Female, Neutral, Unspecified. Defaults to Unspecified.en, fr, ar. Defaults to en.Request
curl -X POST https://www.jsflabs.io/api/v1/voice-clone \ -H "x-api-key: jsf_your_api_key" \ -F "audio=@sample.wav" \ -F "name=Morgan" \ -F "gender=Female" \ -F "language=en"
Response · 201 Created
{ "success": true, "message": "Your voice has been cloned successfully.", "data": { "voiceId": "jsf_c3BlZWNoaWZ5X3ZvaWNlX2lk", "name": "Morgan", "language": "en", "gender": "Female", "provider": "speechify", "sampleUrl": "https://cdn.jsflabs.io/clones/.../sample.wav", "createdAt": "2026-08-12T09:20:44.120Z" }, "credits": { "cost": 0, "used": 0, "remaining": 50000, "total": 50000 } }
Text to speech
Turn text into speech in a cloned voice. Send the request as application/json. The response is a hosted, loudness-normalized WAV file.
Body — application/json
jsf_ voice ID returned by Clone a voice.0.5 to 2.0. 1 is natural pace (default). Lower is slower, higher is faster.Request
curl -X POST https://www.jsflabs.io/api/v1/text-to-speech \ -H "x-api-key: jsf_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "text": "The quick brown fox jumps over the lazy dog.", "voice_id": "jsf_c3BlZWNoaWZ5X3ZvaWNlX2lk", "speed": 1 }'
Response · 200 OK
{ "success": true, "message": "Your audio has been generated successfully.", "data": { "audio_url": "https://cdn.jsflabs.io/tts/.../a1b2c3.wav", "voice_id": "jsf_c3BlZWNoaWZ5X3ZvaWNlX2lk", "format": "wav", "characters": 44, "speed": 1 }, "credits": { "cost": 44, "used": 44, "remaining": 49956, "total": 50000 } }
Response format
Every response uses the same envelope, so you can branch on one field. Success and failure never share a shape.
Success
{ "success": true, "message": "...", "data": { ... }, "credits": { ... } }
Failure
{ "success": false, "error": { "code": "VALIDATION_ERROR", "message": "Some fields are invalid. Check the fields object for details.", "fields": { "audio": "Unsupported audio format. Please upload a WAV or MP3 file." } } }
Always branch on success. On failure, error.code is a stable machine-readable string you can switch on; error.message is human-readable and safe to surface to your users. Some errors add extra fields (like fields or retryAfterSeconds).
Error codes
The full set of error codes, grouped by what caused them. Codes are stable — safe to match on in your integration.
Authentication & access
| HTTP | Code | Meaning |
|---|---|---|
| 401 | MISSING_API_KEY | No x-api-key header was sent. |
| 401 | INVALID_KEY_FORMAT | The key doesn't start with jsf_ or is too short. |
| 401 | INVALID_API_KEY | The key doesn't match any account. |
| 403 | SUBSCRIPTION_INACTIVE | The account's subscription is turned off. |
| 403 | KEY_EXPIRED | The key's expiry date has passed. |
| 503 | AUTH_SERVICE_ERROR | Authentication backend was briefly unavailable. Retry shortly. |
Request & quota
| HTTP | Code | Meaning |
|---|---|---|
| 429 | RATE_LIMITED | Over 50 requests/min. Wait for Retry-After seconds. |
| 400 | INVALID_JSON | Text-to-speech body wasn't valid JSON. |
| 400 | INVALID_CONTENT_TYPE | Clone body wasn't multipart/form-data. |
| 422 | VALIDATION_ERROR | One or more fields are invalid. See the fields object. |
| 400 | INVALID_VOICE_ID | The voice_id is not a valid jsf_ id. |
| 402 | INSUFFICIENT_CHARACTERS | Not enough character balance for this request. |
Cloning
| HTTP | Code | Meaning |
|---|---|---|
| 422 | AUDIO_REJECTED | The sample couldn't be processed. Use a clearer 10–30s recording. |
| 413 | AUDIO_TOO_LARGE | The sample exceeds the size limit. Upload something shorter. |
| 429 | CLONING_CAPACITY | The cloning engine is momentarily at capacity. Retry soon. |
| 503 | CLONING_SERVICE_DOWN | The cloning engine is temporarily unavailable. |
| 502 | CLONING_FAILED | Cloning failed unexpectedly. Retry. |
Text to speech
| HTTP | Code | Meaning |
|---|---|---|
| 504 | GENERATION_TIMEOUT | Generation took too long. Try shorter text. |
| 502 | GENERATION_FAILED | Generation failed unexpectedly. Retry. |
| 502 | AUDIO_SAVE_FAILED | Audio was generated but couldn't be stored. Retry. |
Server
| HTTP | Code | Meaning |
|---|---|---|
| 500 | DB_SAVE_FAILED | The voice was created but the record couldn't be saved. Contact support with the returned reference. |
| 500 | INTERNAL_ERROR | An unexpected server error. Retry, then contact support. |
| 405 | METHOD_NOT_ALLOWED | The endpoint only accepts POST. |