Build with PlayHT
A simple, powerful REST API for text-to-speech, voice cloning, dubbing, and every other PlayHT feature.
# 1. Submit the job — returns immediately with a task id
curl -s -X POST https://yourapp.com/api/v1/tts \
-H "Authorization: Bearer pk_live_..." \
-H "Content-Type: application/json" \
-d '{"text": "Hello world!", "voice_id": "..."}'
# 2. Poll until it's done (generation is async, not instant)
curl -s https://yourapp.com/api/v1/tts-job/<job_id> \
-H "Authorization: Bearer pk_live_..."
# => {"status": "complete", "audio_url": "https://yourapp.com/app/audio/..."}
Get started in 3 steps
Get your API key
Sign up for a free account and grab your API key from the Dashboard → API Keys page.
Open API Keys →Submit a job
POST to /api/v1/tts (or any other tool endpoint) with your text and voice ID. It returns a task id immediately — generation runs in the background.
API Reference →Poll for the result
GET /api/v1/tts-job/{jobId} until status is complete. There is no push/webhook notification yet — polling is the only way to know when a job finishes.
Full endpoint list →Let Claude (or any MCP client) call PlayHT directly
PlayHT runs an MCP (Model Context Protocol) server — add it as a custom connector in Claude.ai, Claude Desktop, or any other MCP-compatible client, and the assistant can generate speech, list voices, dub audio, and run every other tool below on your behalf, in the conversation, with no separate app to open.
Available tools (11)
list_voices
generate_speech
generate_dialogue
change_voice
dub_audio
generate_sound_effect
generate_music
transcribe_audio
isolate_voice
get_task_status
get_tts_job_status
Each tool mirrors an endpoint from the REST API reference below — a job that doesn't finish within a few seconds returns a task id, and the assistant checks back with get_task_status / get_tts_job_status, the same async pattern the REST API itself uses.
API Reference
Every endpoint below is async: a POST returns a task_id
or job_id immediately, and you poll a status endpoint until it's done.
There's no push/webhook notification yet.
Voice Library
List available voices — the public catalog, or your own cloned voices.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| source | query | string | no | signature (default) · natural · quick · compact · playht · clone. clone returns only your own ready cloned voices, read straight from the database — no vendor call. |
| provider | query | string | no | Deprecated alias for source: elevenlabs · minimax · edge · kokoro · playht · clone. |
| language | query | string | no | Filter by language code, e.g. en. |
| gender | query | string | no | Filter by voice gender. |
| accent | query | string | no | Filter by accent. |
| age | query | string | no | Filter by age category. |
| use_case | query | string | no | Filter by use case (e.g. narration, conversational). |
| q | query | string | no | Free-text search. |
| page | query | integer | no | Page number for pagination. |
| page_size | query | integer | no | Results per page. |
Request
curl -s "https://playht.co/api/v1/voices?source=signature&language=en" \ -H "Authorization: Bearer pk_live_..."
Response
{
"status": 200,
"body": {
"voices": [
{
"canonical_id": "abc123",
"voice_source": "signature",
"provider_brand": "elevenlabs",
"name": "Rachel",
"gender": "female",
"language": "en",
"accent": "american",
"age": "young",
"use_case": "narration",
"description": "Calm, professional narration voice.",
"preview_url": "https://yourapp.com/app/media?sig=...",
"is_clone": false
}
],
"pagination": { "page": 1, "page_size": 20, "total": 534 }
}
}
Error responses
| Status | Error key | Message |
|---|---|---|
| 401 | unauthenticated | Missing, invalid, expired, or revoked Bearer token. |
Results are cached per filter combination for 1 hour. Preview URLs are always routed through the signed media proxy — a vendor domain never reaches the response. provider_brand is a deprecated alias for voice_source, kept for backward compatibility — new integrations should use voice_source.
Text to Speech
Convert text (or SSML) to speech. Long text is automatically chunked and merged server-side.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| text | body | string | yes, unless ssml is given | Plain text to speak. |
| ssml | body | string | yes, unless text is given | SSML markup to speak. |
| voice_id | body | string | no | Canonical voice ID from GET /voices. |
| voice_source | body | string | no | signature (default) · natural · quick · compact · playht · clone. Matches the voice_source field GET /voices returned for voice_id. |
| provider_brand | body | string | no | Deprecated alias for voice_source: elevenlabs · minimax · edge · kokoro · playht · clone. |
| output_format | body | string | no | mp3 (default) · wav · ogg · flac. |
| stability | body | number | no | 0–1. |
| similarity_boost | body | number | no | 0–1. |
| speed | body | number | no | 0.5–2. |
Request
curl -s -X POST https://playht.co/api/v1/tts \
-H "Authorization: Bearer pk_live_..." \
-H "Content-Type: application/json" \
-d '{"text": "Hello world!", "voice_id": "abc123", "output_format": "mp3"}'
Response
{
"status": 200,
"body": {
"status": "processing",
"task_id": "t1_9f8c2a..."
}
}
// or, when the vendor is temporarily at capacity and queueing is enabled:
// HTTP 202
{
"status": "queued",
"job_id": "7f3e1a2b-...",
"message": "The service is busy. Your job has been queued and will be processed automatically."
}
Error responses
| Status | Error key | Message |
|---|---|---|
| 401 | unauthenticated | Missing, invalid, expired, or revoked Bearer token. |
| 402 | insufficient_credits | You have no credits remaining this month. Please upgrade your plan. |
| 402 | team_limit_reached | You've used your team spending allocation for this period. Ask your team owner to raise it, or wait for the next reset. |
| 404 | voice_unavailable | This voice is temporarily unavailable. We're monitoring it and will restore it automatically. |
| 422 | — | Laravel validation error — see the specific field rules for this endpoint below. |
| 429 | concurrency_limit_reached | You have {n} job(s) already running, the max your plan allows at once. Wait for one to finish or upgrade your plan. |
| 503 | vendor_busy | The service is currently at capacity. Please try again in a moment. |
| 503 | vendor_daily_limit | Service capacity for today has been reached. Please try again tomorrow. |
| 503 | — | This tool is currently unavailable. Please try again later. |
Text longer than the configured chunk size (default 800 chars) is split, submitted as separate vendor tasks, and merged with ffmpeg once every chunk completes — poll GET /tts-job/{job_id} for those. Short, single-chunk requests return a task_id instead — poll GET /tts-poll/{task_id}. Credits are reserved on submission and only charged once audio is confirmed stored.
Other AI Tools
Generate a multi-speaker spoken dialogue from a script.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| speakers | body | array | yes | Array of { name (required), voice_id (nullable) }, up to your plan's max speaker count. |
| inputs | body | array | yes | Array of { speaker (required, matches a name in speakers), text (required) } lines, in order. |
Request
curl -s -X POST https://playht.co/api/v1/dialogue \
-H "Authorization: Bearer pk_live_..." \
-H "Content-Type: application/json" \
-d '{
"speakers": [{"name": "Alex", "voice_id": "abc123"}, {"name": "Jordan", "voice_id": "def456"}],
"inputs": [{"speaker": "Alex", "text": "Hey, have you tried the new API?"}, {"speaker": "Jordan", "text": "Not yet, how is it?"}]
}'
Response
{
"status": 200,
"body": {
"status": "processing",
"task_id": "t1_4b7d1e..."
}
}
Error responses
| Status | Error key | Message |
|---|---|---|
| 401 | unauthenticated | Missing, invalid, expired, or revoked Bearer token. |
| 402 | insufficient_credits | You have no credits remaining this month. Please upgrade your plan. |
| 402 | team_limit_reached | You've used your team spending allocation for this period. Ask your team owner to raise it, or wait for the next reset. |
| 429 | concurrency_limit_reached | You have {n} job(s) already running, the max your plan allows at once. Wait for one to finish or upgrade your plan. |
| 503 | vendor_busy | The service is currently at capacity. Please try again in a moment. |
| 503 | — | This tool is currently unavailable. Please try again later. |
| 422 | — | Laravel validation error — see the specific field rules for this endpoint below. |
Poll the returned task_id with GET /task/{task_id}. Max speakers and max characters are admin-configured per vendor.
Re-voice an uploaded audio file as a different voice.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| file | body (multipart) | file | yes | Source audio file. |
| voice_id | body | string | no | Target canonical voice ID. |
| voice_source | body | string | no | signature (default) · natural · quick · compact · playht. Matches the voice_source field GET /voices returned for voice_id. |
| provider_brand | body | string | no | Deprecated alias for voice_source: elevenlabs · minimax · edge · kokoro · playht. |
| model_id | body | string | no | Vendor model override, when supported. |
| remove_background_noise | body | boolean | no | Strip background noise before conversion. |
Request
curl -s -X POST https://playht.co/api/v1/voice-changer \ -H "Authorization: Bearer pk_live_..." \ -F "[email protected]" \ -F "voice_id=abc123"
Response
{
"status": 200,
"body": {
"status": "processing",
"task_id": "t1_2c9a4f..."
}
}
Error responses
| Status | Error key | Message |
|---|---|---|
| 401 | unauthenticated | Missing, invalid, expired, or revoked Bearer token. |
| 402 | insufficient_credits | You have no credits remaining this month. Please upgrade your plan. |
| 402 | team_limit_reached | You've used your team spending allocation for this period. Ask your team owner to raise it, or wait for the next reset. |
| 429 | concurrency_limit_reached | You have {n} job(s) already running, the max your plan allows at once. Wait for one to finish or upgrade your plan. |
| 503 | vendor_busy | The service is currently at capacity. Please try again in a moment. |
| 503 | — | This tool is currently unavailable. Please try again later. |
| 422 | — | Laravel validation error — see the specific field rules for this endpoint below. |
multipart/form-data, not JSON. Max file size is admin-configured (default 25MB). Poll with GET /task/{task_id}.
Translate and dub an audio/video file into another language.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| target_lang | body | string | yes | Target language code. |
| file | body (multipart) | file | no | Source audio/video file. |
| source_lang | body | string | no | Source language code, auto-detected if omitted. |
| num_speakers | body | integer | no | 0–20. |
| disable_voice_cloning | body | boolean | no | Use a stock voice instead of cloning the original speaker. |
| start_time | body | number | no | Trim start (seconds). |
| end_time | body | number | no | Trim end (seconds). |
Request
curl -s -X POST https://playht.co/api/v1/dubbing \ -H "Authorization: Bearer pk_live_..." \ -F "[email protected]" \ -F "target_lang=es"
Response
{
"status": 200,
"body": {
"status": "processing",
"dubbing_id": "t1_7a1d3c..."
}
}
Error responses
| Status | Error key | Message |
|---|---|---|
| 401 | unauthenticated | Missing, invalid, expired, or revoked Bearer token. |
| 402 | insufficient_credits | You have no credits remaining this month. Please upgrade your plan. |
| 402 | team_limit_reached | You've used your team spending allocation for this period. Ask your team owner to raise it, or wait for the next reset. |
| 429 | concurrency_limit_reached | You have {n} job(s) already running, the max your plan allows at once. Wait for one to finish or upgrade your plan. |
| 503 | vendor_busy | The service is currently at capacity. Please try again in a moment. |
| 503 | — | This tool is currently unavailable. Please try again later. |
| 422 | — | Laravel validation error — see the specific field rules for this endpoint below. |
multipart/form-data. Max file size is admin-configured (default 100MB). Poll with GET /task/{dubbing_id}.
Generate a sound effect from a text prompt.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| text | body | string | yes | Description of the sound effect. |
| duration_seconds | body | number | no | 0.5 up to the admin-configured max (default 30). |
| prompt_influence | body | number | no | 0–1. |
Request
curl -s -X POST https://playht.co/api/v1/sfx \
-H "Authorization: Bearer pk_live_..." \
-H "Content-Type: application/json" \
-d '{"text": "Glass shattering on a tile floor", "duration_seconds": 3}'
Response
{
"status": 200,
"body": {
"status": "processing",
"task_id": "t1_5e8b2d..."
}
}
Error responses
| Status | Error key | Message |
|---|---|---|
| 401 | unauthenticated | Missing, invalid, expired, or revoked Bearer token. |
| 402 | insufficient_credits | You have no credits remaining this month. Please upgrade your plan. |
| 402 | team_limit_reached | You've used your team spending allocation for this period. Ask your team owner to raise it, or wait for the next reset. |
| 429 | concurrency_limit_reached | You have {n} job(s) already running, the max your plan allows at once. Wait for one to finish or upgrade your plan. |
| 503 | vendor_busy | The service is currently at capacity. Please try again in a moment. |
| 503 | — | This tool is currently unavailable. Please try again later. |
| 422 | — | Laravel validation error — see the specific field rules for this endpoint below. |
Poll with GET /task/{task_id}.
Generate music from a text prompt.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| prompt | body | string | yes | Description of the music to generate. |
| duration_seconds | body | number | no | 5 up to the admin-configured max (default 300). |
Request
curl -s -X POST https://playht.co/api/v1/music \
-H "Authorization: Bearer pk_live_..." \
-H "Content-Type: application/json" \
-d '{"prompt": "Upbeat lo-fi hip hop, 90 BPM", "duration_seconds": 30}'
Response
{
"status": 200,
"body": {
"status": "processing",
"task_id": "t1_1f4a9b..."
}
}
Error responses
| Status | Error key | Message |
|---|---|---|
| 401 | unauthenticated | Missing, invalid, expired, or revoked Bearer token. |
| 402 | insufficient_credits | You have no credits remaining this month. Please upgrade your plan. |
| 402 | team_limit_reached | You've used your team spending allocation for this period. Ask your team owner to raise it, or wait for the next reset. |
| 429 | concurrency_limit_reached | You have {n} job(s) already running, the max your plan allows at once. Wait for one to finish or upgrade your plan. |
| 503 | vendor_busy | The service is currently at capacity. Please try again in a moment. |
| 503 | — | This tool is currently unavailable. Please try again later. |
| 422 | — | Laravel validation error — see the specific field rules for this endpoint below. |
Poll with GET /task/{task_id}.
Transcribe an audio file to text.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| file | body (multipart) | file | no | Audio file to transcribe. |
| language | body | string | no | Language hint. |
| diarize | body | boolean | no | Label which speaker said what. |
| smart_format | body | boolean | no | Apply punctuation/casing formatting. |
Request
curl -s -X POST https://playht.co/api/v1/stt \ -H "Authorization: Bearer pk_live_..." \ -F "[email protected]" \ -F "diarize=true"
Response
{
"status": 200,
"body": {
"status": "processing",
"task_id": "t1_6c3e8a..."
}
}
// once complete, GET /task/{task_id} body includes:
{
"status": "done",
"output": {
"text": "Hey, have you tried the new API?",
"transcript": [...],
"words": [...],
"language": "en",
"duration": 4.2
}
}
Error responses
| Status | Error key | Message |
|---|---|---|
| 401 | unauthenticated | Missing, invalid, expired, or revoked Bearer token. |
| 402 | insufficient_credits | You have no credits remaining this month. Please upgrade your plan. |
| 402 | team_limit_reached | You've used your team spending allocation for this period. Ask your team owner to raise it, or wait for the next reset. |
| 429 | concurrency_limit_reached | You have {n} job(s) already running, the max your plan allows at once. Wait for one to finish or upgrade your plan. |
| 503 | vendor_busy | The service is currently at capacity. Please try again in a moment. |
| 503 | — | This tool is currently unavailable. Please try again later. |
multipart/form-data. Max file size is admin-configured (default 25MB). All fields are optional, so this endpoint has no field-validation errors of its own.
Strip background noise from an audio file, isolating the voice.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| file | body (multipart) | file | no | Audio file to process. |
Request
curl -s -X POST https://playht.co/api/v1/voice-isolate \ -H "Authorization: Bearer pk_live_..." \ -F "[email protected]"
Response
{
"status": 200,
"body": {
"status": "processing",
"task_id": "t1_3d7f2b..."
}
}
Error responses
| Status | Error key | Message |
|---|---|---|
| 401 | unauthenticated | Missing, invalid, expired, or revoked Bearer token. |
| 402 | insufficient_credits | You have no credits remaining this month. Please upgrade your plan. |
| 402 | team_limit_reached | You've used your team spending allocation for this period. Ask your team owner to raise it, or wait for the next reset. |
| 429 | concurrency_limit_reached | You have {n} job(s) already running, the max your plan allows at once. Wait for one to finish or upgrade your plan. |
| 503 | vendor_busy | The service is currently at capacity. Please try again in a moment. |
| 503 | — | This tool is currently unavailable. Please try again later. |
multipart/form-data. Max file size is admin-configured (default 25MB). Poll with GET /task/{task_id}.
Task & Job Status
Poll the status of any task returned by dialogue, voice-changer, dubbing, sfx, music, stt, or voice-isolate.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| task_id | path | string | yes | The neutral-prefixed id (t1_.../t2_...) returned when the job was submitted. |
Request
curl -s https://playht.co/api/v1/task/t1_9f8c2a... \ -H "Authorization: Bearer pk_live_..."
Response
{
"status": 200,
"body": {
"status": "done",
"audio_url": "https://yourapp.com/app/media?sig=...",
"url": "https://yourapp.com/app/media?sig=...",
"output_url": "https://yourapp.com/app/media?sig=..."
}
}
Error responses
| Status | Error key | Message |
|---|---|---|
| 401 | unauthenticated | Missing, invalid, expired, or revoked Bearer token. |
| 400 | — | Invalid task ID format. |
| 403 | — | Access denied — this task_id belongs to a different account. |
Ownership is enforced: a task_id only resolves for the account that created it. All vendor-hosted media URLs are rewritten to the signed media proxy — a vendor domain never appears in the response.
Poll a single-chunk (short, unsplit) TTS submission. Finalizes the job — charges credits — on completion.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| task_id | path | string | yes | The task_id returned by POST /tts for a short (non-chunked) request. |
Request
curl -s https://playht.co/api/v1/tts-poll/t1_9f8c2a... \ -H "Authorization: Bearer pk_live_..."
Response
{
"status": 200,
"body": {
"status": "done",
"audio_url": "https://yourapp.com/app/audio/...",
"url": "https://yourapp.com/app/audio/...",
"output_url": "https://yourapp.com/app/audio/..."
}
}
Error responses
| Status | Error key | Message |
|---|---|---|
| 401 | unauthenticated | Missing, invalid, expired, or revoked Bearer token. |
| 400 | — | Invalid task ID format. |
| 403 | — | Access denied — this task_id belongs to a different account. |
Only use this for short text that did NOT get chunked (see POST /tts notes). Credits are charged here, on confirmed completion — not at submission time. If audio download/storage fails transiently, the response reports { "status": "doing", "progress": 99 } so the client keeps polling instead of a false failure.
Poll a chunked TTS job (long text that was automatically split and is being merged).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| job_id | path | string | yes | The job_id (a UUID, not a t1_/t2_-prefixed task id) returned by POST /tts for long text. |
Request
curl -s https://playht.co/api/v1/tts-job/7f3e1a2b-... \ -H "Authorization: Bearer pk_live_..."
Response
{
"status": "complete",
"audio_url": "https://yourapp.com/app/audio/...",
"total_chunks": 4,
"completed_chunks": 4
}
// while still running:
{
"status": "processing",
"total_chunks": 4,
"completed_chunks": 2
}
// on failure:
{
"status": "failed",
"error": "1 chunk(s) failed to generate."
}
Error responses
| Status | Error key | Message |
|---|---|---|
| 401 | unauthenticated | Missing, invalid, expired, or revoked Bearer token. |
| 404 | — | Job not found. |
Each call polls any still-processing chunks, and once every chunk is done, merges them with ffmpeg server-side and charges credits (capped at the amount reserved at submission). If a chunk permanently fails, remaining credits for that job are released back to your balance automatically.
Authentication
Every request needs a single header — no separate user/account id: