VoiceLabs

Developer API

The VoiceLabs /v1 REST API — API keys and scopes, text-to-speech and transcription endpoints, an OpenAI-compatible /v1/audio/speech, RFC 9457 errors, rate limits, idempotency, and the official @voicelabs/sdk TypeScript SDK.

VoiceLabs exposes a versioned public REST API at https://app.voicelabs.now/v1. Generate speech in your own cloned voices, transcribe audio, and read everything on your account — over plain HTTP, from anything that can send a request. Inference runs on VoiceLabs' GPU servers, so there is no model to download and no GPU to rent. It sits on the same engine bridge as the MCP tools, so an HTTP client and an AI agent get identical behavior.

Already using OpenAI's text-to-speech? Point your existing SDK here by changing its base URL — see OpenAI-compatible endpoint.

This page is the prose introduction: what the API does, how to authenticate, and what it will and will not do for you. The operation-by-operation reference — every parameter, schema, and response, with a request builder — lives at API Reference, generated from the same OpenAPI 3.1 document the server publishes.

The API is part of Pro

The HTTP API is a VoiceLabs Pro feature, and it is enforced at the credential, not per endpoint: on an account without an active subscription or trial, every /v1 request — reads included — is refused with 403 pro_plan_required, carrying a settings_url that points at the billing page for the account that made the call.

Nothing is destroyed when a subscription lapses. Keys are not revoked, they simply stop being accepted, and the same key starts working again on the very next request once the account is back on Pro. There is nothing to re-create and nothing to swap into your environment.

API keys

Create and manage API keys from the developer console at app.voicelabs.now/connections, on the API keys tab. Create a key, tick the scopes it needs, and copy the secret:

# The secret is shown ONCE, at creation. VoiceLabs stores a SHA-256 hash of it and keeps
# only the leading characters in plain text, so a lost key is replaced, not recovered.
# Revoking a key takes effect on its very next request.

export VOICELABS_API_KEY="vl_sandbox_…"

Authenticate with the key in an x-api-key header:

curl https://app.voicelabs.now/v1/voices \
  -H "x-api-key: vl_sandbox_..."

Send it as Authorization: Bearer instead if your HTTP client only speaks bearer auth. Browser session cookies are never accepted. The public API is credential-authenticated only, which is what lets it serve a permissive CORS policy safely.

Scopes, honestly

You choose a key's scopes when you mint it, and a key can do exactly what its scopes allow — nothing wider:

ScopeGrants
voice:readRead your voices, profiles, and past generations.
voice:generateGenerate speech, transcribe audio, and add built-in (preset) voices to your account on your behalf. Cannot clone a real person's voice.

A key without voice:generate is refused with 403 insufficient_scope on the two generation endpoints and cannot spend the account's allowance — which makes a read-only key safe to hand to something you would rather not trust with your quota. Reading never draws on the plan allowance; generating does.

An API key is one of two credentials this API accepts. The other is an OAuth 2.1 access token, for software acting on behalf of other people's VoiceLabs accounts rather than your own. It is the same authorization server the VoiceLabs MCP server uses — authorization code with PKCE and dynamic client registration — and the reference documents its endpoints under the oauth2 scheme. Both carry the same scopes and are gated by the same check.

Endpoints

Method and pathScopeWhat it does
POST /v1/speechvoice:generateSynthesize speech in one of your voice profiles. Async: returns a generation id with status generating.
POST /v1/audio/speechvoice:generateThe same generation, in OpenAI's shape. Synchronous: returns the audio bytes. See OpenAI-compatible endpoint.
POST /v1/transcriptionsvoice:generateTranscribe an audio clip with VoiceLabs' Whisper; returns the transcript synchronously, plus the capture it created.
GET /v1/generations/{generationId}voice:readPoll a generation for its status — generating, completed, or failed — and, once complete, the audio URL.
GET /v1/voicesvoice:readList your voice profiles (cloned voices and presets), with language and usage counts.
GET /v1/capturesvoice:readList your recent captures with transcripts, most-recent first (paginated).
GET /v1/audio/{generationId}voice:readFetch a completed generation's audio via a short-lived signed URL.

Speech generation is asynchronous because it is GPU work: POST /v1/speech returns a generation id straight away, and polling GET /v1/generations/{generationId} yields an audio_url once it completes. That URL is signed and short-lived — about fifteen minutes, scoped to that one generation — so it can be handed directly to a media player, and re-polling mints a fresh one rather than reviving an expired link. GET /v1/audio/{generationId} is reached by following that signed link, not by constructing it. Transcription, by contrast, returns its transcript on the same response.

OpenAI-compatible endpoint

If you already have code written against OpenAI's text-to-speech API, you do not have to rewrite it. POST /v1/audio/speech serves OpenAI's createSpeech contract, so pointing an OpenAI SDK at VoiceLabs is a base-URL swap and an API-key swap — nothing else changes.

from pathlib import Path
from openai import OpenAI

client = OpenAI(
    api_key="vl_...",                            # your VoiceLabs API key
    base_url="https://app.voicelabs.now/v1",     # the only other change
)

with client.audio.speech.with_streaming_response.create(
    model="tts-1",       # or a VoiceLabs engine id: kokoro, qwen, luxtts, chatterbox, …
    voice="Narrator",    # a VoiceLabs voice profile id or name — see GET /v1/voices
    input="The quick brown fox jumped over the lazy dog.",
) as response:
    response.stream_to_file(Path("speech.mp3"))
import fs from "node:fs/promises";
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.VOICELABS_API_KEY,
  baseURL: "https://app.voicelabs.now/v1",
});

const speech = await client.audio.speech.create({
  model: "tts-1",
  voice: "Narrator",
  input: "The quick brown fox jumped over the lazy dog.",
});

await fs.writeFile("speech.mp3", Buffer.from(await speech.arrayBuffer()));

Unlike POST /v1/speech, this endpoint is synchronous: it holds the response open until the audio is ready and returns the bytes, because that is what an OpenAI client expects. It still carries X-VoiceLabs-Generation-Id, so the same generation is readable at GET /v1/generations/{generationId} afterwards.

What each parameter does here

Anything VoiceLabs cannot honour is refused with a 400 naming the field, never accepted and quietly ignored — a parameter that silently does nothing is worse than one that fails.

ParameterBehaviour
modelA VoiceLabs engine id (qwen, qwen_custom_voice, luxtts, chatterbox, chatterbox_turbo, tada, kokoro) selects that engine. OpenAI's tts-1, tts-1-hd and gpt-4o-mini-tts are accepted so existing code keeps working, but they select nothing — the voice's own default engine is used. Echoed back on X-VoiceLabs-Engine.
inputThe text. OpenAI caps it at 4096 characters; VoiceLabs accepts up to 10000.
voiceA VoiceLabs voice profile id or name — list them with GET /v1/voices. OpenAI's custom-voice object, { "id": "…" }, works for the same value. OpenAI's built-in voice names (alloy, echo, nova, …) are not VoiceLabs voices, so a request naming one is refused rather than silently substituted.
response_formatmp3 (the default, as on OpenAI), opus, wav. OpenAI's aac, flac and pcm are not encoded by VoiceLabs and are refused.
speedOnly 1. The generation path behind this endpoint has no speed control, and returning audio at the wrong speed would be worse than saying so.
instructionsNot supported. VoiceLabs takes voice direction from the profile's own personality prompt, not from a per-request string.
stream_formatOnly audio (the OpenAI default). The sse event stream is not implemented.
seedA VoiceLabs extension — not an OpenAI parameter. See Reproducible takes.
temperatureA VoiceLabs extension — not an OpenAI parameter. See Reproducible takes.

The last two widen what this endpoint accepts; they take nothing away, so an unmodified OpenAI integration is unaffected — it never sends them. Reaching them from an OpenAI SDK means that SDK's own escape hatch for parameters outside OpenAI's schema: extra_body={"seed": 4242} in the Python client, a cast on the request object in the TypeScript one. They are forwarded to the generator like any other field; nothing here is accepted and dropped.

Errors on this one endpoint use OpenAI's {"error": {...}} envelope rather than the RFC 9457 problem document described below, so an OpenAI SDK raises a typed exception with a readable message and a param naming the field at fault. The code member is the same stable token every other operation carries, so you can still branch on it.

Everything else is unchanged: the same API key, the same voice:generate scope, the same rate limits, and the same plan allowance. This is a second shape on the existing endpoint, not a second product.

Reproducible takes

Two optional parameters control the sampling, on both speech endpoints — POST /v1/speech and POST /v1/audio/speech — because both forward to the same generator:

ParameterBehaviour
seedA non-negative integer that pins the random seed. The same seed with the same text and the same voice returns the same audio every time; a different seed genuinely re-rolls the take. Every VoiceLabs engine both accepts a seed and changes its output when it changes.
temperature0.052. Only the chatterbox and chatterbox_turbo engines forward a temperature to the model. Ask any other engine for one and the request is refused with 400 invalid_request — never generated at a setting that was quietly discarded, because a knob that silently does nothing is indistinguishable from the drift you set it to fix.
curl https://app.voicelabs.now/v1/speech \
  -H "x-api-key: $VOICELABS_API_KEY" \
  -H "content-type: application/json" \
  -d '{"text":"Chapter one.","voice_name":"Narrator","seed":4242,"temperature":0.8}'

Omit either and the voice profile's own pin applies, then the engine's default — so a pin set once on the voice carries across every chapter without being repeated on each call. Pins are set on the voice in the studio's Voices tab.

Which engine serves a request matters here, and neither endpoint lets you name one directly except POST /v1/audio/speech's model: otherwise the voice profile's own default engine decides. So whether a temperature is honoured or refused depends on the voice you name.

Errors

Every error is application/problem+jsonRFC 9457 — with a stable code member to branch on:

{
  "type": "https://voicelabs.now/errors/insufficient_scope",
  "title": "Insufficient scope",
  "status": 403,
  "detail": "This credential does not carry the voice:generate scope.",
  "instance": "/v1/speech",
  "code": "insufficient_scope",
  "required_scope": "voice:generate"
}

Branch on code, not on status or on the prose in detail: several problems share a status, and detail is written for humans and may be reworded. Every error code has its own documentation page under voicelabs.now/errors, which is exactly where the problem document's type URI points.

Rate limits and what your plan covers

Two different budgets apply, and they are worth keeping apart.

The first is a request rate: 600 requests per hour, per key, counting the requests that do work — POST, PUT, PATCH and DELETE. Reads (GET) never draw on that hour: polling GET /v1/generations/{id} while a generation runs is what the API asks you to do, so it cannot lock you out of the create you are waiting to make. Reads are bounded separately by a short burst window instead (300 per minute per key, advertised as the "read-burst" policy). Responses to an API-key request carry the IETF draft-11 RateLimit and RateLimit-Policy headers, so a client can see its remaining budget without guessing, and exceeding either ceiling returns 429 rate_limit_exceeded with a Retry-After. Those headers describe a key's budget, so they are absent when you authenticate with an OAuth 2.1 access token — there is no per-key counter to report.

The second is your plan's audio allowance, metered server-side in minutes of generated audio. VoiceLabs Pro is the only paid plan, and it is a flat subscription: no per-character fee, no per-generation fee, no metered overage.

The two failures are told apart on purpose. An exhausted allowance is 429 quota_exhausted, which resolves itself next period or with an upgrade; a plan that never included the capability is 403 feature_not_enabled, which only an upgrade resolves. Both carry a settings_url pointing at where to fix it.

Per-key spend ceilings

There is a third budget, and it is yours to set. Any key can carry a monthly spend ceiling — a whole number of minutes of generated audio, set on the API keys tab of the console when you create the key or at any time after. A key with no ceiling has none: your plan's allowance is its only limit.

A ceiling bounds one key without bounding the account, which is what makes it useful for the key you paste into a script, hand to a contractor, or point at a staging environment. Crossing it returns 429 key_ceiling_exceeded, whose settings_url points at the developer console rather than at billing — the account may have hours of allowance left, and the only thing that clears this refusal is you raising or removing the number.

Three details worth knowing:

  • Only generated audio counts. Reads never move it, and neither do transcriptions, which produce no audio.
  • The figure is measured, never estimated. A charge is the real output duration of a generation, read from the same record the account meter is charged from.
  • POST /v1/speech is asynchronous, so a generation counts toward the ceiling once its duration is known rather than at the instant you submit it. A key can therefore overshoot its ceiling by whatever it has in flight when it crosses; nothing after that is admitted. The console shows any still-finishing generations separately for the same reason.

The console shows each key's current-month spend against its ceiling, counted from the same ledger the API enforces against.

Idempotency

Writes accept an Idempotency-Key header. Replaying the same key with the same body returns the original response byte-for-byte instead of generating a second time, so a retry after a dropped connection cannot cost you twice.

What the free allowance covers

There is no guest mode: you must be signed in to generate audio. Signed-in accounts that are not subscribed get a monthly allowance of generated audio — the live figure is shown in the app, because it is tuned on the engine and any number printed here would be one deploy away from being false. Past it, generating needs an active trial or subscription. That allowance is spent in the app, not here: API access itself is Pro-only and is enforced as described in The API is part of Pro. See Usage and limits and Pricing.

Reference, document, and SDK

  • Interactive API reference: API Reference — one page per operation, generated from the same schemas the handlers validate with. A change to a handler's contract that is not reflected in the document fails our build, so the reference describes the running API rather than an account of it.
  • Raw OpenAPI 3.1 document: app.voicelabs.now/openapi.json. Absolute and at the API host on purpose: the URL you copy into an SDK generator or an API client has to work from anywhere, not only from the host you happened to land on.
  • TypeScript SDK: the official @voicelabs/sdk package (MIT licensed) covers text-to-speech and transcription over HTTP.
  • For AI agents: Connecting AI assistants — the same capabilities as MCP tools, so Claude and other MCP clients can use your voice studio directly.

Same account, same metering

API usage is scoped to your VoiceLabs account and metered by your plan exactly like the app and MCP surfaces — see Usage and limits.

On this page