JSF Labs Developer API

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.

REST · JSON & multipartKey auth50 requests / minWAV output
Overview

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.

Overview

Base URL

All endpoints live under a single versioned base. Every path in these docs is written in full, including the version prefix.

Base URL
https://www.jsflabs.io/api/v1

Requests must be sent over HTTPS. Plain HTTP is not supported.

Overview

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
  }'
Core concepts

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.

Header
x-api-key: jsf_your_api_key
Your key is a secret. Never expose it in client-side code, mobile apps, or public repositories — call the API from your server. A leaked key can spend your character balance.

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.

Core concepts

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:

429 Too Many Requests
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.

Core concepts

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 text in 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 object
"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.

Core concepts

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.

Example voice ID
jsf_c3BlZWNoaWZ5X3ZvaWNlX2lk

Treat 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.

Endpoint

Clone a voice

Upload a clean audio sample and receive a reusable voice. Send the request as multipart/form-data.

POSThttps://www.jsflabs.io/api/v1/voice-clone

Body — multipart/form-data

audio
filerequired
The voice sample. WAV or MP3, up to 25 MB. A clean 10–30 second recording with no background noise gives the best clone.
name
stringrequired
A label for the voice, 2–80 characters. Shown back to you and stored with the clone.
gender
stringoptional
One of Male, Female, Neutral, Unspecified. Defaults to Unspecified.
language
stringoptional
Two-letter ISO 639-1 code, e.g. 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

JSON
{
  "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 }
}
Store the voiceId. It's permanent and reusable — you never need to re-upload the sample to speak in this voice again.
Endpoint

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.

POSThttps://www.jsflabs.io/api/v1/text-to-speech

Body — application/json

text
stringrequired
The text to speak. Up to 50,000 characters. Long text is automatically split, generated in parallel, and stitched into one continuous file.
voice_id
stringrequired
A jsf_ voice ID returned by Clone a voice.
speed
numberoptional
Playback pace from 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

JSON
{
  "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 }
}
The audio_url points to a hosted WAV file, already loudness-normalized to -14 LUFS and peak-limited. Download it or stream it directly.
Reference

Response format

Every response uses the same envelope, so you can branch on one field. Success and failure never share a shape.

Success

2xx
{ "success": true, "message": "...", "data": { ... }, "credits": { ... } }

Failure

4xx / 5xx
{
  "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).

Reference

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

HTTPCodeMeaning
401MISSING_API_KEYNo x-api-key header was sent.
401INVALID_KEY_FORMATThe key doesn't start with jsf_ or is too short.
401INVALID_API_KEYThe key doesn't match any account.
403SUBSCRIPTION_INACTIVEThe account's subscription is turned off.
403KEY_EXPIREDThe key's expiry date has passed.
503AUTH_SERVICE_ERRORAuthentication backend was briefly unavailable. Retry shortly.

Request & quota

HTTPCodeMeaning
429RATE_LIMITEDOver 50 requests/min. Wait for Retry-After seconds.
400INVALID_JSONText-to-speech body wasn't valid JSON.
400INVALID_CONTENT_TYPEClone body wasn't multipart/form-data.
422VALIDATION_ERROROne or more fields are invalid. See the fields object.
400INVALID_VOICE_IDThe voice_id is not a valid jsf_ id.
402INSUFFICIENT_CHARACTERSNot enough character balance for this request.

Cloning

HTTPCodeMeaning
422AUDIO_REJECTEDThe sample couldn't be processed. Use a clearer 10–30s recording.
413AUDIO_TOO_LARGEThe sample exceeds the size limit. Upload something shorter.
429CLONING_CAPACITYThe cloning engine is momentarily at capacity. Retry soon.
503CLONING_SERVICE_DOWNThe cloning engine is temporarily unavailable.
502CLONING_FAILEDCloning failed unexpectedly. Retry.

Text to speech

HTTPCodeMeaning
504GENERATION_TIMEOUTGeneration took too long. Try shorter text.
502GENERATION_FAILEDGeneration failed unexpectedly. Retry.
502AUDIO_SAVE_FAILEDAudio was generated but couldn't be stored. Retry.

Server

HTTPCodeMeaning
500DB_SAVE_FAILEDThe voice was created but the record couldn't be saved. Contact support with the returned reference.
500INTERNAL_ERRORAn unexpected server error. Retry, then contact support.
405METHOD_NOT_ALLOWEDThe endpoint only accepts POST.
JSF Labs API · v1Questions? support@jsflabs.io