Skip to content

Ask AI

Ask anything about Destesi — setup, products, APIs.

Powered by Claude. Answers may be wrong — always verify against the docs.

Reference

The complete HTTP surface for Text-to-Speech.

Base URL: https://api.tts.destesi.io

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.

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.

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 dst CLI.
  • Tokens begin with idn_pat_. One token works for every workspace you belong to; the X-Destesi-Workspace header 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.
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.
Terminal window
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
}'
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.

instruct is capability-gated per voice — see Controls. Check capabilities on the voice before sending it:

Terminal window
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.

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.

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_url is a signed URL with a 1-hour lifetime. Fetch it within that window, or call GET /v1/jobs/{job_id}/audio for a fresh redirect.
  • duration_ms is the length of the rendered audio, not the request latency.
  • drive_file_id is the durable Drive file ID for the clip, present once the audio is registered.

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.

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

All output is 16-bit PCM WAV, 16 kHz, mono. There is no MP3 output.

Terminal window
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.

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.

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.

{
"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.

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.

Removes a job from your history. Workspace-scoped; an unknown or foreign id returns 404.

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.

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.

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.

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.

Every response carries an X-Request-Id header — echoed if you supply one, generated otherwise. Include it when reporting an issue.

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