Reference
Reference
Section titled “Reference”The complete HTTP surface for Text-to-Speech.
Base URL: https://api.tts.destesi.io
Authentication
Section titled “Authentication”Every authenticated route accepts either of two arms — pick whichever fits the caller. There are no product-specific API keys: a suite-wide Personal Access Token is the only token form Text-to-Speech accepts.
Browser session
Section titled “Browser session”When you open tts.destesi.io from the launcher, single sign-on sets a host-only session cookie automatically. This is what the web console uses; you don’t manage it yourself.
Personal Access Token
Section titled “Personal Access Token”For scripts, backends, or other tools, send a Personal Access Token:
Authorization: Bearer idn_pat_<your-token>X-Destesi-Workspace: <your-workspace-slug>- Mint, list, and revoke tokens at account.destesi.io/settings/api-tokens. The plaintext is shown exactly once at creation. See API access & the
dstCLI. - Tokens begin with
idn_pat_. One token works for every workspace you belong to; theX-Destesi-Workspaceheader selects which one a given request runs in. - Text-to-Speech never stores your token — it is validated against the central accounts service on each request. Revoke a token in accounts and the next call with it fails immediately.
Endpoints
Section titled “Endpoints”| Method | Path | Auth | Purpose |
|---|---|---|---|
GET |
/healthz |
none | Liveness probe. Returns ok. |
GET |
/readyz |
none | Readiness probe. Returns ok. |
GET |
/v1/auth/me |
session / token | Current identity. |
POST |
/v1/auth/logout |
session | Clears the session cookie. Idempotent — never 401s. |
GET |
/v1/voices |
session / token | List the workspace’s voices, each with its engine and capabilities. |
GET |
/v1/voices/{id} |
session / token | Single voice metadata. |
GET |
/v1/voices/{id}/preview |
session / token | Streams the voice’s sample clip (audio/wav). |
POST |
/v1/voices/{id}/favorite |
session / token | Mark a voice as a favorite so it sorts to the top of the picker. |
DELETE |
/v1/voices/{id}/favorite |
session / token | Remove the favorite mark. |
POST |
/v1/synthesize |
session / token | Generate speech audio. |
GET |
/v1/jobs |
session / token | List recent synthesis jobs (paginated). |
GET |
/v1/jobs/{id} |
session / token | Single job state. |
GET |
/v1/jobs/{id}/audio |
session / token | 302 to a signed URL for the generated clip. Add ?download=1 to get a link that saves the file rather than playing it inline. |
DELETE |
/v1/jobs/{id} |
session / token | Delete a job from your history. |
GET |
/v1/projects |
session / token | List projects. |
POST |
/v1/projects |
session / token | Create a project. |
GET |
/v1/projects/{id} |
session / token | Read one project. |
PATCH |
/v1/projects/{id} |
session / token | Rename a project. |
DELETE |
/v1/projects/{id} |
session / token | Delete a project. |
GET |
/v1/stats |
session / token | Usage totals for the workspace. |
POST |
/v1/chat |
session / token | The Text-to-Speech agent — describe what you want in words and it synthesizes it. |
GET |
/v1/conversations |
session / token | List the agent’s conversations. |
GET |
/v1/conversations/{id}/messages |
session / token | One page of a conversation’s messages. |
PATCH |
/v1/conversations/{id} |
session / token | Rename a conversation. |
DELETE |
/v1/conversations/{id} |
session / token | Delete a conversation. |
Synthesize
Section titled “Synthesize”POST /v1/synthesize
Section titled “POST /v1/synthesize”curl -sS -X POST https://api.tts.destesi.io/v1/synthesize \ -H "Authorization: Bearer $DESTESI_PAT" \ -H "X-Destesi-Workspace: $DESTESI_WORKSPACE" \ -H "Content-Type: application/json" \ -d '{ "text": "Hello from Destesi.", "voice_id": "voice_builtin_aria", "language": "en", "sync": true }'Request body
Section titled “Request body”| Field | Type | Required | Notes |
|---|---|---|---|
text |
string | yes | UTF-8. At least 1 character, at most 5,000. |
voice_id |
string | yes | A voice from the built-in catalog. |
language |
string | no | ISO-639-1 hint (en, es, zh, …). Omit to let the model auto-detect. |
sync |
bool | no | true renders inline and returns 201 with a playable audio_url. Inline rendering applies only when sync: true, the text is 500 characters or fewer, and the engine is warm; anything else is queued and returns 202. |
instruct |
string | no | Delivery direction, up to 500 characters. Only for voices whose capabilities include instruct. |
project_id |
string | no | Group the job under one of your projects. |
Synthesis controls
Section titled “Synthesis controls”instruct is capability-gated per voice — see Controls. Check capabilities on the voice before sending it:
curl -sS https://api.tts.destesi.io/v1/voices \ -H "Authorization: Bearer $DESTESI_PAT" \ -H "X-Destesi-Workspace: $DESTESI_WORKSPACE" \| jq '.items[] | {id, engine, capabilities}'| Situation | Result |
|---|---|
| Voice lists the capability | Applied. |
| Voice does not list the capability | 400 unsupported_parameter — never silently ignored. |
instruct longer than 500 characters |
400 invalid_instruct. |
Idempotency
Section titled “Idempotency”Send an Idempotency-Key header to make a synthesize call safe to retry:
Idempotency-Key: 6f1c2e9a-your-own-key- A replay of the same request under the same key returns the original job — no second clip, no second charge.
- Reusing the key with a different request body returns
409 idempotency_conflict. The mismatch is treated as a caller bug rather than resolved by guessing which body you meant. - Omit the header and every call creates a new job.
Sync response — 201
Section titled “Sync response — 201”Returned when sync: true and the text is short enough to render inline:
{ "job_id": "job_abc123", "status": "done", "audio_url": "https://…/jobs/job_abc123.wav?…", "duration_ms": 1840, "drive_file_id": "file_…"}audio_urlis a signed URL with a 1-hour lifetime. Fetch it within that window, or callGET /v1/jobs/{job_id}/audiofor a fresh redirect.duration_msis the length of the rendered audio, not the request latency.drive_file_idis the durable Drive file ID for the clip, present once the audio is registered.
Async response — 202
Section titled “Async response — 202”Returned when sync: false, when the text is too long for sync, or when the engine is cold:
{ "job_id": "job_abc123", "status": "queued" }Poll GET /v1/jobs/{job_id} until status is done (the payload then includes audio_url) or failed.
Errors
Section titled “Errors”| HTTP | error code |
Meaning | When it happens |
|---|---|---|---|
400 |
bad_json |
The request body was not valid JSON. | The body failed to parse — a stray comma, a truncated payload, or Content-Type: application/json set on a non-JSON body. |
400 |
missing_text |
text was empty or missing. |
The text field is absent, "", or only whitespace (it is trimmed before the check). |
400 |
text_too_long |
text exceeds 5,000 characters. |
The trimmed text is longer than 5,000 characters. Split it into chunks and synthesize each. |
400 |
missing_voice_id |
voice_id was empty or missing. |
The voice_id field is absent or only whitespace. Pass a catalog ID like voice_builtin_aria. |
400 |
unsupported_parameter |
You sent a control the chosen voice does not accept. | instruct on a voice whose capabilities lack instruct, or on a request routed at an external provider that has no such parameter. Check GET /v1/voices and drop the control or pick a different voice. |
400 |
invalid_instruct |
instruct was too long. |
The direction exceeded 500 characters. Shorten it — it is a delivery note, not a script. |
401 |
unknown_bearer_scheme |
The Bearer value is not an idn_pat_ token. |
You sent Authorization: Bearer <something> whose value does not start with idn_pat_. There is no cookie fallback once a Bearer header is present. |
401 |
missing_workspace_header |
A token was supplied without X-Destesi-Workspace. |
A valid idn_pat_ token was sent but the X-Destesi-Workspace header was missing or blank, so the request has no workspace to run in. |
401 |
invalid_token |
The token was rejected by accounts (revoked, expired, or not valid for this workspace). | The token failed validation against accounts: it was revoked, expired, or you do not belong to the workspace named in X-Destesi-Workspace. |
401 |
missing_session |
No credentials were supplied. | No Authorization: Bearer header and no tts_session cookie — an unauthenticated call. |
404 |
voice_not_found |
voice_id is not visible to this workspace. |
The voice_id does not exist. Check it against GET /v1/voices. |
409 |
idempotency_conflict |
An Idempotency-Key was reused with a different request body. |
The key already belongs to a different request. Use a fresh key, or resend the original body exactly. |
409 |
not_connected |
A synthesis provider you asked for isn’t connected for this workspace. | You routed the request at an external provider that has no credential in Connect. Connect it, or drop the provider and use a built-in voice. |
429 |
quota | The workspace’s plan allowance is exhausted. | The character allowance is measured per plan; see Plan allowances. |
502 |
provider_error |
An external synthesis provider returned an error. | Upstream failure, not a request problem. Retry; if it persists, report it with the request id. |
503 |
worker_unavailable |
No synthesis engine is configured at all. | Nothing can synthesize. On hosted Destesi this is an incident, not something you caused. |
Length and mode guidance
Section titled “Length and mode guidance”| Text length | Recommended mode |
|---|---|
| ≤ 250 chars | sync: true — feels conversational. |
| 250–500 chars | sync: true is fine. |
| 500–2,000 chars | sync: false — poll the job. |
| 2,000–5,000 chars | sync: false; consider chunking your text. |
| > 5,000 chars | Rejected. Split into chunks and concatenate the resulting clips. |
Audio format
Section titled “Audio format”All output is 16-bit PCM WAV, 16 kHz, mono. There is no MP3 output.
Voices
Section titled “Voices”GET /v1/voices
Section titled “GET /v1/voices”curl -sS https://api.tts.destesi.io/v1/voices \ -H "Authorization: Bearer $DESTESI_PAT" \ -H "X-Destesi-Workspace: $DESTESI_WORKSPACE"{ "items": [ { "id": "voice_builtin_aria", "display_name": "Aria", "language": "multi", "gender": "female", "origin": "builtin", "engine": "qwen", "capabilities": ["instruct"], "status": "ready", "created_at": "2026-05-13T00:00:00Z" } ]}origin: "builtin" voices are shared across every workspace. capabilities is computed from the voice rather than stored, so it always matches what the engine will actually accept — treat it as the authority on which controls you may send.
Built-in catalog
Section titled “Built-in catalog”The catalog is nine voices. Each one speaks every supported language — English, Spanish, Mandarin, French, German, Japanese, Korean, Italian, Portuguese, and Russian — so language reads multi. Capabilities: instruct.
| ID | Name | Gender | Notes |
|---|---|---|---|
voice_builtin_aria |
Aria | female | Warm, conversational. A good default. |
voice_builtin_nova |
Nova | female | Clear, broadcaster-style narration. |
voice_builtin_atlas |
Atlas | male | Deep, authoritative. Good for documentary voiceover. |
voice_builtin_indigo |
Indigo | male | Younger, casual delivery. |
voice_builtin_marlow |
Marlow | male | Mid-range, neutral. Strong for product walkthroughs. |
voice_builtin_orion |
Orion | male | Energetic, ad-read tone. |
voice_builtin_sage |
Sage | male | Calm, instructional. |
voice_builtin_lyra |
Lyra | female | Bright, animated. |
voice_builtin_juniper |
Juniper | female | Soft, intimate. |
Call GET /v1/voices for the live list with each voice’s exact ID, gender, and capabilities; this table is a map, not a substitute.
GET /v1/voices/{id}/preview
Section titled “GET /v1/voices/{id}/preview”Returns the voice’s short sample clip directly as audio/wav bytes (200). In the console, the play button on each voice card uses this.
GET /v1/jobs?limit=20&offset=0
Section titled “GET /v1/jobs?limit=20&offset=0”{ "items": [ { "id": "job_abc123", "voice_id": "voice_builtin_aria", "text": "Hello.", "status": "done", "duration_ms": 1840, "created_at": "2026-05-13T04:32:01Z", "finished_at": "2026-05-13T04:32:03Z" } ], "next_offset": 20}In the list view, text is truncated. GET /v1/jobs/{id} returns the full text and, when the status is done, the audio_url.
GET /v1/jobs/{id}/audio
Section titled “GET /v1/jobs/{id}/audio”302 redirect to a signed URL for the WAV (1-hour lifetime). Returns 404 no_audio if the job is not done or never produced audio.
Add ?download=1 for a link that prompts a file save instead of playing inline — that is what the console’s Download button uses.
DELETE /v1/jobs/{id}
Section titled “DELETE /v1/jobs/{id}”Removes a job from your history. Workspace-scoped; an unknown or foreign id returns 404.
Projects
Section titled “Projects”Projects group related jobs. Pass a project_id on synthesize to file the resulting job under one.
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/projects |
List the workspace’s projects. |
POST |
/v1/projects |
Create a project. |
GET |
/v1/projects/{id} |
Read one project. |
PATCH |
/v1/projects/{id} |
Rename a project. |
DELETE |
/v1/projects/{id} |
Delete a project. |
Synthesizing into a project that doesn’t exist in your workspace returns 404 project_not_found.
GET /v1/stats
Section titled “GET /v1/stats”Returns the workspace’s usage totals — how much you have synthesized — the same numbers the console’s usage panel shows. Use it to see where you stand against your plan allowances before a large batch.
The Text-to-Speech agent
Section titled “The Text-to-Speech agent”POST /v1/chat runs a conversational agent over this same API: it can list voices, read back each one’s capabilities, and synthesize on your behalf. Its conversations are stored server-side and served by the /v1/conversations* routes, so the thread follows you across devices.
The agent only ever sends a control a voice actually supports — it reads capabilities first — so it doesn’t produce unsupported_parameter failures on your behalf.
Conventions
Section titled “Conventions”Error format
Section titled “Error format”Every error response is a single JSON object with one field — a stable, machine-readable error code:
{ "error": "worker_unavailable" }Branch on error (the codes in the tables above) and the HTTP status. The exact set of codes can grow over time, so treat any unrecognised code in its status class the same way you would the closest documented one.
Request IDs
Section titled “Request IDs”Every response carries an X-Request-Id header — echoed if you supply one, generated otherwise. Include it when reporting an issue.
Plan allowances
Section titled “Plan allowances”Your plan carries a measured allowance: characters synthesized per month. It is metered per workspace, and GET /v1/stats reports where you stand. Consult your plan for the numbers — and note that a workspace over its allowance may be answered with a 429 rather than served, so treat the allowance as a real bound when sizing a batch.
There are no separate per-second rate limits. Synthesis throughput is bounded by the shared engine: for batch workloads, use sync: false and keep concurrency modest (4 or fewer in flight).