Cantonese-optimized Text-to-Speech API

v1Last updated OpenAPIllms.txtMarkdownAPI Reference

Cantonese speech, one API call away

Canto-Gen (港嘢) turns Cantonese text into natural, native-sounding speech — nine tones preserved, particles pronounced right. Submit a job (202), poll until it's done, download the audio. OpenAPI 3.1 spec included.

MCP / Agent setup

Canto-Gen provides a Local stdio MCP server (npm package `cantogen-mcp`) that enables zero-hosting-cost local deployment and empowers AI agents with custom voice management and Cantonese TTS generation. After registration, the live tool list is available via `tools/list`. You fill the API key into the config yourself (tick `voices:manage` for voice management); the agent never touches the credentials. Click the button below to copy the setup instructions and hand them to your agent.

Supported AI clients / developer tools

Claude CodeClaude CodeOpenCodeOpenCodeCodexCodexGemini CLIGemini CLICursorCursorCopilotCopilotClineClineAiderAider

MCP tools

ToolDescriptionAuth RequiredRequired Scope(s)
list_galleryList official preset voices (read-only)sk_ keytts:generate
list_my_voicesList personal voicessk_ keyvoices:manage
create_my_voiceCreate a personal voice from local audio (plan-gated)sk_ keyvoices:manage
delete_my_voiceDelete a personal voice (hidden instantly, can no longer generate; history keeps the name)sk_ keyvoices:manage
tts_generateGenerate in preset mode (voice slug)sk_ keytts:generate
generate_designGenerate in design mode (gender + age)sk_ keytts:generatetts:design
generate_cloneGenerate in clone mode (local reference audio; plan-gated)sk_ keytts:generatetts:clone
check_jobPoll async job status until done (how to track the async /v1/audio/jobs flow)sk_ keytts:generatetts:design
cancel_jobCancel a generation job (not started → full refund; in progress → cancelled after wrap-up, completed portion still billed; settled jobs cannot be cancelled)sk_ keytts:generatetts:design

MCP for AI agents

Canto-Gen provides a Local stdio MCP server (npm package `cantogen-mcp`) that enables zero-hosting-cost local deployment and empowers AI agents with custom voice management and Cantonese TTS generation.

Get a key

API requests authenticate with your Bearer key. Keys are shown exactly once at creation — only a SHA-256 hash is stored server-side, so a lost key must be revoked and replaced.

1

Create a key in the API Keys page

Open the API Keys page and create a key, then save it straight into an environment variable:

environmentbash
export CANTOGEN_API_KEY="sk_live_..."

Key prefixes

sk_live_…The only key format. Free-plan keys are subject to a daily generation limit; paid-plan usage counts against the key's monthly character cap.

Scopes

tts:generateDefault. Unlocks voice & design generation plus all read endpoints (voice catalog, job polling).
tts:cloneRequired for clone mode (voice cloning from a reference sample).
tts:designFine-grained scope for design mode (gender + age synthesized voices) — already covered by tts:generate; either one suffices.
voices:manageRequired for personal voice management (MCP list / create / delete my voices).

Limits attached to every key

A per-minute rate limit (default 60 requests) and an optional monthly character cap. All REST endpoints authenticate with Authorization: Bearer $CANTOGEN_API_KEY, except GET /v1/voices (the public catalog, anonymous access).

Pricing

API access is available on any paid plan (Lite / Starter / Pro / Studio) at full speed with no daily cap. Free users can create an API key too (50 generation requests per day; polling and downloads don't count) — upgrading unlocks full access.

1 character = 1 character, no surcharge

API usage draws from the same account character pool as the web playground, resetting on your billing cycle — 1 character = 1 character, no multiplier, no surcharge. When the pool runs out, buy credit packs. The X-Credits-Charged response header shows the exact characters billed for each request.

Glossary

  • Daily generation cap — Free-plan keys: 50 generation requests per day (polling and downloads don't count). Paid plans have no daily cap.
  • Per-key monthly character cap — A key's month-to-date (UTC) billed-character cap; exceeding it returns 403 monthly_cap_reached. Unset by default.
  • Account character pool — Account-level character balance shared by the web playground and the API, resetting each billing cycle; requests return 402 character_limit_reached when exhausted.
  • Credit pack — One-time top-up for the account character pool; never expires.
  • Pending-job cap — Concurrent pending jobs per account, tiered by plan (3 / 4 / 5 / 10 / 20 / 20); exceeding returns 429 in_flight_limit_reached.

See /pricing for plan details.

Quickstart

Three minutes to your first job: list the voice catalog → submit a speech job → poll for the result.

1

List the voice catalog (GET /v1/voices)

list voicesbash
curl https://canto-gen.com/v1/voices \
  -H "Authorization: Bearer $CANTOGEN_API_KEY"
200 responsejson
{
  "voices": [
    {
      "slug": "sum",
      "name": "心妍",
      "gender": "female",
      "age": "adult",
      "styles": [],
      "preview_url": "https://canto-gen.com/api/voice-clip?slug=sum"
    }
  ]
}

The catalog is live — always pick a slug from your own response. "sum" (心妍) here is the documented example, or browse voices by ear in the Playground gallery.

2

Submit a speech job (POST /v1/audio/jobs)

# Async by design — submit returns 202 with a job_id, then poll.
export CANTOGEN_API_KEY="sk_live_…"

curl -X POST https://canto-gen.com/v1/audio/jobs \
  -H "Authorization: Bearer $CANTOGEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "cantogen-tts",
    "voice": "sum",
    "text": "你好,歡迎使用港嘢!",
    "response_format": "mp3"
  }'

Response — 202 Accepted:

202 responsehttp
HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "job_id": "b7e6f2a1-4c3d-4e5f-8a9b-0c1d2e3f4a5b",
  "status": "queued",
  "poll_url": "/v1/audio/jobs/b7e6f2a1-4c3d-4e5f-8a9b-0c1d2e3f4a5b",
  "srt_requested": false
}

Async by design — with a sync shortcut

The async endpoint returns 202 with job_id / status / poll_url / srt_requested. No long-lived HTTP connections, no timeouts on long text — poll at your own pace. Short text (≤300 chars, voice/design)? POST /v1/audio/speech returns the audio bytes directly (200) — or an honest 503 pool_cold while the sync queue is busy (nothing billed; waits over ~45 s turn into a 202 to poll).

Voice catalog

The voices and style presets below are synced automatically from the database — add a new voice or preset and this page updates itself. Every voice can be previewed in the Playground.

slugNameGenderAgeScenePreview
keung-gor強哥malemidagenarration
lok-zai樂仔maleadultstory
sung-pak崇伯maleeldernarration
fung-yee鳳儀femaleadultstory
sum-yu芯瑜femaleyoungads
wai小慧femaleadultstory
kay凱琪femaleyoungsports
lok嘉樂maleadultcomedy
sum心妍femaleadultnews
wing-lam-2穎琳femaleadultbusiness
bon阿邦maleadultnarration
ah-sum阿森maleyoungnarration
sze-kei思琪femalemidagenarration
hoi小海maleyoungads
wang-tat宏達maleyoungnarration

Generation presets (tempo · emotion · timbre)

Tempo一句一頓一字一頓一氣呵成中性自然含糊慵懶大起大落字正腔圓平鋪直敘抑揚起伏擲地有聲輕聲細語邊諗邊講
Emotion冷靜喜悅嘲諷好奇媚惑嫉妒安心寂寞平和心痛悲傷悲愴愉快感慨慈愛憤怒欣喜沮喪溫柔滿足激昂焦躁熱情絕望緊張自豪認真雀躍驚慌驚訝

Presets are generation parameters that pair with any slug above — synced from the same database.

Voice modes

Pick a gallery voice, describe one in text, or clone one from a sample. Each request uses exactly one mode.

Mutually exclusive

The three voice modes are mutually exclusive: send exactly one of voice, design, or clone per request — anything else returns 400 voice_mode_conflict. Designed and cloned voices are per-request: they are never saved as reusable voice assets.
voicedefault

Gallery voice

Pick a ready-made voice from the Cantonese-optimized gallery by slug.

  • Browse GET /v1/voices — every preset is Cantonese-native, with preview clips.
  • Pass the slug as voice. sum (心妍) is the documented example — always pick a slug from your own response. Slugs are stable ASCII identifiers (industry standard); Chinese names are for display.
voice examplebash
# Async by design — submit returns 202 with a job_id, then poll.
export CANTOGEN_API_KEY="sk_live_…"

curl -X POST https://canto-gen.com/v1/audio/jobs \
  -H "Authorization: Bearer $CANTOGEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "cantogen-tts",
    "voice": "sum",
    "text": "你好,歡迎使用港嘢!",
    "response_format": "mp3"
  }'
designzero audio

Designed voice

Describe a synthetic voice in plain text — gender + age. No audio files, nothing to upload.

  • gender: male | female · age: child | teen | young | adult | midage | elder — both required.
  • Great for product personas; combine freely with emotion and srt.
design examplebash
curl -X POST https://canto-gen.com/v1/audio/jobs \
  -H "Authorization: Bearer $CANTOGEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "cantogen-tts",
    "design": { "gender": "female", "age": "young" },
    "text": "今日天氣幾好,一齊去行山啦。",
    "style": "brisk",
    "emotion": "happy",
    "srt": true
  }'
clonetts:clone scope

Cloned voice

Upload a reference sample — the platform transcribes it internally (ASR) and applies the transcript automatically. Zero extra calls on your side.

  • reference_audio: data URL or raw base64 — audio/mpeg, wav, webm, mp4/m4a, ogg. One request is capped at 4.4 MB (≈3 MB of audio once base64-encoded).
  • Samples must be at least 8 seconds (shorter is rejected); processing auto-trims to ~15–20 s on sentence boundaries (30 s cap) — a clean sample works best.
  • Only reference_audio is needed; the legacy prompt_text field is retired (any value is ignored).
  • Requires an API key with the tts:clone scope.
clone examplebash
B64=$(base64 -w0 sample.wav)

curl -X POST https://canto-gen.com/v1/audio/jobs \
  -H "Authorization: Bearer $CANTOGEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"model\": \"cantogen-tts\",
    \"clone\": {
      \"reference_audio\": \"data:audio/wav;base64,$B64\"
    },
    \"text\": \"用我把聲講呢句嘢。\"
  }"

Generation parameters (all modes)

styleDelivery style — English slugs recommended. Rhythm: steady(平穩) / composed(從容) / rapid(急促) / slow(慢板) / drawl(拖長) / brisk(明快) / dynamic(起伏) / staccato(頓挫). Timbre: mellow(圓潤) / cute(奶音) / sharp(尖細) / lazy(懶音) / breathy(氣聲) / hoarse(沙啞) / deep(洪厚) / crisp(清脆) / feeble(無力) / trembling(顫音). Scene and character slugs are accepted too (corporate / promo / … / storytelling / sports — full list in the OpenAPI schema). The original Chinese preset values (明快 / 圓潤 etc.) remain accepted as aliases — both are normalized internally to the same canonical value; any other free text also passes through.
emotionOne of 9 supported values (see chips below). Note: the API enum uses English values (happy / excited etc.), passed to the engine as-is. This is a distinct system from the Chinese emotion presets in the Playground UI (雀躍 / 冷靜 etc.) — those are Playground-only; the API emotion parameter does not accept Chinese values.
instructionFree-text delivery direction, up to 500 characters. Priority: style → emotion → instruction — instruction has the final say.
pitchDeprecated (2026-09-20): no-op under raw audio delivery (accepted but ignored). Historical range −12…+12 semitones, default 0.
volumeDeprecated (2026-09-20): no-op under raw audio delivery (accepted but ignored). Historical range 0–10, default 1.
response_formatmp3 (default) / wav.
srttrue → the completed job carries an srt field (billed at a higher character rate). Available on all plans.
happysadangryfearfuldisgustedsurprisedneutralexcitedgentle

Parameter limits

  • text (async) — POST /v1/audio/jobs text cap: 500 characters; over it returns 400 text_too_long (never truncated).
  • text (sync) — POST /v1/audio/speech cap: 300 billable characters (voice/design); use the async endpoint beyond that.
  • instruction — Free-text delivery instruction cap: 500 characters; over it returns 400 invalid_instruction.
  • style — English slugs (rhythm / timbre / scene / character); the original Chinese presets remain accepted as aliases (both normalize to the same canonical value); any other free text passes through.
  • emotion — One of the 9 closed English enum values (see chips); Chinese values are not accepted.
  • synthesis order — style → emotion → instruction; instruction has the final say.
  • clone.reference_audio — base64 data URL or raw, ≤4.4 MB per request (≈3 MB of audio); only reference_audio is needed — the legacy prompt_text is retired (any value ignored).
  • design — gender (male / female) + age (child / teen / young / adult / midage / elder).
  • speed — Removed (2026-10-14) — silently ignored if sent; express speed via the clone `control` descriptor (slow / normal / fast).
  • transcription(文件轉寫) — POST /v1/asr/transcribe — file ≤20 MB; ≈9 minutes of audio per request on the synchronous endpoint (4 hours is planned, not live); 24 billable audio hours per day; response_format json / text (srt / verbose_json not yet supported).

Polling & SRT

Jobs settle in seconds for short text. Poll with a 2–5 s backoff until the job completes, then download the pre-signed audio.

POST /v1/audio/jobs202 · job_id
GET /v1/audio/jobs/{id}poll · 2–5 s backoff
status: completedaudio_url ready
GET audio_urlpre-signed · no auth

Poll a job

GET /v1/audio/jobs/{id}bash
curl https://canto-gen.com/v1/audio/jobs/b7e6f2a1-4c3d-4e5f-8a9b-0c1d2e3f4a5b \
  -H "Authorization: Bearer $CANTOGEN_API_KEY"

Completed response

completed jobjson
{
  "job_id": "b7e6f2a1-4c3d-4e5f-8a9b-0c1d2e3f4a5b",
  "status": "completed",
  "poll_url": "/v1/audio/jobs/b7e6f2a1-4c3d-4e5f-8a9b-0c1d2e3f4a5b",
  "srt_requested": true,
  "srt": "1\n00:00:00,000 --> 00:00:02,400\n你好,歡迎使用港嘢!",
  "audio_url": "https://canto-gen.com/storage/v1/object/sign/audio_history/..."
}
poll with backoff (Node)js
// Poll until completed — back off 2–5 s between attempts.
async function pollJob(jobId, maxAttempts = 60) {
  for (let i = 0; i < maxAttempts; i++) {
    const res = await fetch(`https://canto-gen.com/v1/audio/jobs/${jobId}`, {
      headers: { Authorization: `Bearer ${process.env.CANTOGEN_API_KEY}` },
    });
    const job = await res.json();
    if (job.status === "completed") return job; // job.audio_url is ready
    if (job.status === "failed" || job.status === "cancelled") {
      throw new Error(`job ${job.status}`);
    }
    await new Promise((r) => setTimeout(r, Math.min(2000 + i * 500, 5000)));
  }
  throw new Error("poll timeout");
}

audio_url expires — download promptly

audio_url is a pre-signed URL that expires: valid for 1 hour when served from the materialized audio library, and 7 days when served straight from the generation worker. Download promptly and re-poll the job for a fresh URL. audio_url is absent while the job is queued/processing and on failed/cancelled jobs.

Subtitles (SRT)

Send "srt": true on submit and the completed job's poll response carries an srt field with the subtitle content (as in the completed response above). srt_requested is always present; srt only appears when subtitles were requested and are ready. Subtitles are available on all plans.

Billing nuance

Billing nuance: account credit for the job includes an SRT surcharge (billedCharacters exceeds base characters when srt: true), while the per-key monthly cap ledger counts the submitted text length flat. Subtitles are available on all plans.

Rate limits & caps

Response headerDescription
X-RateLimit-LimitPer-minute request cap for this key (default 60; configurable at key creation).
X-RateLimit-RemainingRequests left in the current 60-second sliding window.
X-RateLimit-ResetWindow reset time (Unix epoch seconds).
Retry-AfterAttached to 429 responses; wait this many seconds before retrying.

Monthly character cap — the key's month-to-date (UTC) billed characters must stay under its cap (null = unlimited). Exceeding it returns 403 monthly_cap_reached with a used figure until the month resets. X-RateLimit-* headers are attached to the 202 and 429 responses of POST /v1/audio/jobs and the 429 responses of POST /v1/audio/speech (200 responses carry the usage headers — X-Credits-Charged etc.); the read endpoints (GET /v1/audio/jobs/{id}, GET /v1/voices) are not rate-limited. Both meters are designed to fail open — a metering outage never locks you out.

Realtime WebSocket

Built for interactive, low-latency voice experiences.

Alongside REST, Canto-Gen exposes a WebSocket realtime endpoint for text-in → streamed PCM audio-out: keep one long-lived connection and stream tokens in while audio comes out — built for bring-your-own-LLM voice apps. The protocol is based on OpenAI Realtime; every Canto-Gen extension event and field is marked as such.

wss://api.canto-gen.com/v1/audio/realtime

Auth

Authenticate with the same sk_ API key as REST; the required scope is tts:generate. Prefer the Authorization: Bearer sk_… header. The browser WebSocket API cannot set custom headers, so browsers may fall back to the query parameter ?api_key=sk_… (alias ?token=). Query parameters are written to URLs, server logs and browser history, so keep them browser-only — code running on a server must always use the header.

Events — client → server

session.updateOpenAI-nativeOpen/update the session: voice, language, output_audio_format, max_session_chars, tts.segmentation.
conversation.item.createOpenAI-nativeSend one explicit utterance (input_text); triggers synthesis.
input_text.appendCanto-Gen extensionSend raw token deltas — stream as you generate.
input_text.commitCanto-Gen extensionForce a segment boundary (flush now).
response.createOpenAI-nativeTrigger synthesis.
response.cancelOpenAI-nativeBarge-in: stop the current utterance and refund it.
session.closeCanto-Gen extensionClose the session and settle.

Events — server → client

session.createdOpenAI-nativeSession opened; carries the server-minted session id.
session.updatedOpenAI-nativeConfig ack.
response.createdOpenAI-nativeAn utterance begins.
response.output_audio.deltaOpenAI-nativeA base64 PCM16 @ 48 kHz audio chunk.
response.output_audio.doneOpenAI-nativeUtterance audio complete.
response.doneOpenAI-nativeUtterance done; usage carries input_chars / billed_chars / remaining (extension fields).
errorOpenAI-native{type, code, message, http_status}.

Segmentation (tts.segmentation)

tts.segmentation (extension) controls utterance segmentation: {"mode":"server"} (default) lets the server split on punctuation and synthesize utterance by utterance — the client just keeps appending tokens; {"mode":"client"} lets the client mark boundaries itself via conversation.item.create / input_text.commit. Optional min_chars / max_chars / punctuation.

Limits & timeouts

  • idle_timeout — Idle 120 s (no app-level activity) → the server closes the connection and settles.
  • max_session_duration — Free plan: a single session lasts at most 5 minutes; paid plans have no duration cap. It is closed at the cap.
  • max_sessions_per_user — Concurrent sessions per user are plan-tiered: free 1 / lite 2 / starter 3 / pro 4 / studio 5 / enterprise 5; unknown plans are treated as free.
  • max_sessions_global — At most 8 concurrent sessions platform-wide.
  • max_session_chars — Billable-character cap per session (default 2000, adjustable via session.update); exceeding it returns session_char_limit_exceeded.
  • max_in_flight — At most 3 un-settled utterances per session.
  • raw_ingress — Raw input cap of 20,000 characters per session and 5,000 per minute (punctuation and whitespace included); exceeding it returns raw_limit_exceeded, fail-closed.
  • lease — Each utterance carries a 60-second settlement lease; if it is not settled in time it is refunded automatically.
  • rate_limit / monthly_cap — The same per-key per-minute rate limit and monthly character cap apply; usage is recorded one row per utterance.
  • daily_generation_limit — Free plan: 50 generations per day (shared with the REST counter; polling and downloads don't count); exceeding it returns sandbox_daily_limit (429). Paid plans have no daily cap.
  • frame_size — Single WS message cap: 64 KiB.
  • heartbeat — The server pings periodically; pings do not count as app activity.
Node example clientjs
// npm i ws   — one long-lived connection: stream tokens in, stream PCM out.
import WebSocket from "ws";

const ws = new WebSocket("wss://api.canto-gen.com/v1/audio/realtime", {
  headers: { Authorization: `Bearer ${process.env.CANTOGEN_API_KEY}` },
});

ws.on("open", () => {
  ws.send(JSON.stringify({
    type: "session.update",
    session: { voice: "sum", language: "yue", output_audio_format: "pcm16", max_session_chars: 500 },
  }));
  // One explicit utterance — or keep sending input_text.append deltas.
  ws.send(JSON.stringify({
    type: "conversation.item.create",
    item: { type: "message", content: [{ type: "input_text", text: "你好,歡迎使用港嘢!" }] },
  }));
});

const chunks = [];
ws.on("message", (raw) => {
  const ev = JSON.parse(raw.toString());
  if (ev.type === "session.created") console.log("session", ev.session.id);
  if (ev.type === "response.output_audio.delta") chunks.push(Buffer.from(ev.delta, "base64"));
  if (ev.type === "response.done") {
    // PCM16 @ 48 kHz — feed an AudioWorklet or write the chunks to disk.
    console.log("billed", ev.response.usage.billed_chars, "remaining", ev.response.usage.remaining);
    ws.close();
  }
  if (ev.type === "error") console.error(ev.error.code, ev.error.message);
});

Error codes

invalid_api_key401Key missing, invalid, expired, or wrong scope.
rate_limit_exceeded429Per-key per-minute limit hit; carries retry_after.
sandbox_daily_limit429Free plan's 50 generations per day are used up (shared counter with REST); try again tomorrow or upgrade.
monthly_cap_reached429The key's monthly character cap is reached.
character_limit_reached402Account character pool exhausted — the utterance is not synthesized.
concurrency_limit_reached429Concurrent session or un-settled utterance cap reached.
session_char_limit_exceeded403This session's character cap is reached.
raw_limit_exceeded413Raw input volume (including punctuation/whitespace) exceeds the cap.
text_too_long400A single utterance exceeds the length limit.
invalid_session400The session is closed or otherwise invalid.
engine_unavailable503The realtime engine is temporarily unavailable; retry.

Errors

Errors use a stable machine-readable error code plus an optional human-readable message.

error envelopejson
{
  "error": "text_too_long",
  "message": "text exceeds 500 characters."
}
HTTPerror codeCauseFix
400invalid_modelmodel must be "cantogen-tts" (or omitted).Omit model or set "cantogen-tts".
400voice_mode_conflictMore than one of voice / design / clone was sent — the three modes are mutually exclusive.Send exactly one voice mode per request.
400text_requiredtext is missing or empty.Include a non-empty text (1–500 characters).
400text_too_longtext exceeds 500 characters.Split the text — over-limit requests are rejected, not truncated.
400unsupported_formatresponse_format must be mp3 or wav.Use mp3 or wav.
400invalid_pitchpitch must be between −12 and +12 semitones.Adjust pitch to the valid range.
400invalid_volumevolume must be between 0 and 10.Adjust volume to the valid range.
400invalid_emotionemotion is not one of the 9 supported values.Use one of the 9 supported emotions.
400invalid_srtsrt must be a boolean.Use true / false.
400reference_audio_requiredclone.reference_audio is missing.Attach a base64 reference sample.
400invalid_reference_audioclone.reference_audio is not valid base64 audio.Send a data URL (data:audio/…;base64,…) or raw base64.
400invalid_reference_audio_sizeclone.reference_audio exceeds ~3 MB of audio (the JSON API's per-request cap).Trim the sample (8 s or more is enough) and retry.
400spoken_text_requiredtext has no speakable content — zero billable characters (only punctuation / whitespace / emoji / tone tags).Include text with readable words.
400design_gender_and_age_requiredBoth design.gender and design.age are required.Provide both fields.
400invalid_job_idThe poll or cancel id is not a UUID.Check the job_id.
400invalid_requestThe body is not valid JSON, or a non-JSON Content-Type was sent (/v1 accepts application/json only); for transcription, multipart is missing `file`/`model`, the file is empty, or the audio type is unsupported.Send Content-Type: application/json with a valid JSON body.
413payload_too_largeThe request exceeds its size cap — 4.4 MB for the JSON API (usually an over-long clone reference sample); 25 MB for transcription audio files.Trim the reference sample (≤3 MB of audio; 8 s or more is enough); for transcription, use a shorter audio file.
401invalid_or_expired_tokenAPI key missing, invalid, expired, or revoked.Create a new key and check the Bearer header.
402character_limit_reachedThe account character pool cannot cover this generation (the body includes remaining).Buy a credit pack or wait for the monthly reset.
403insufficient_scopeThe key lacks the required scope (cloning needs tts:clone); the body includes "scope".Create a key with the required scope.
403monthly_cap_reachedThe key's monthly character cap is reached; the body includes "used".Wait for the UTC month to reset or raise the cap.
404job_not_foundNo job with this id (or it belongs to another key).Check the job_id and the key used to submit.
409already_settledThe job already settled (completed/failed/cancelled) and can no longer be cancelled; no refund applies.Poll GET /v1/audio/jobs/{id} for the final state instead.
409duplicate_requestThe same Idempotency-Key (alias x-idempotency-key) was already submitted (retry safety: nothing was re-billed).Poll the original job_id, or submit with a new key.
404not_foundNo such endpoint (typo in the path).Check the paths in /v1/openapi.json.
403batch_requires_proBulk generation (items) requires the Starter/Pro/Studio plan. Concurrent pending jobs are tiered by plan (3 / 4 / 5 / 10 / 20 / 20 pending on free/lite/starter/pro/studio/enterprise); exceeding the cap returns 429 in_flight_limit_reached.Wait for current jobs to finish, or upgrade to Starter/Pro/Studio for bulk generation and a higher concurrency cap.
429capacity_exceededThe public API generation pool is at its global concurrency cap.Wait Retry-After seconds, or use the async `POST /v1/audio/jobs` path.
429rate_limit_exceededPer-key per-minute limit hit.Wait Retry-After seconds (also mirrored in the body as retry_after), then retry.
429sandbox_daily_limitFree plan: the daily limit of 50 generation requests has been reached; polling and downloads are unaffected.Try again tomorrow, or upgrade to the Lite plan for full API access.
429api_busyThe API generation queue is at its global capacity cap (rejected before reserve — nothing billed).Wait a few seconds and retry.
502audio_delivery_failedGeneration completed but the audio could not be fetched.Poll GET /v1/audio/jobs/{id} and download the audio_url instead.
503sync_generation_failedThe job failed or was cancelled during the sync wait (already refunded automatically).Retry via POST /v1/audio/jobs (async).
503reference_upload_failedThe clone reference audio could not be stored temporarily.Simply retry.
400unsupported_response_formatTranscription response_format currently supports json / text only — srt / verbose_json need segment-level timestamps and are not yet available.Use json or text.
402insufficient_balanceThe prepaid balance cannot cover the hold for this transcription (nothing processed, nothing billed).Top up at /account/billing and retry (failed requests are never billed).
429daily_limit_exceededThe daily billable-audio cap (24 hours/day, Hong Kong day boundary) is reached.Retry tomorrow (Hong Kong time).
503engine_unavailableThe transcription service is temporarily unavailable (duration-probe, billing or pipeline failure; audio beyond the current synchronous delivery window also lands here). Any hold is refunded in full.Retry later; nothing is billed.

Infrastructure

Built to stay fast when it matters.

API speech workloads run on a dedicated GPU pool — fully isolated from the interactive playground, so your production traffic and someone's browser session never compete for the same capacity.

Reference & downloads

This guide is the step-by-step walkthrough; /v1/docs/reference (Scalar) is the detailed API Reference — full request/response schemas for every endpoint, all error codes, and a try-it panel live there. Everything on this page also ships in machine-readable form.

downloadsbash
# OpenAPI 3.1 spec — generate a typed client in your language
curl https://canto-gen.com/v1/openapi.json

# This page as clean Markdown
curl https://canto-gen.com/v1/docs/en.md

# Agent index (llms.txt)
curl https://canto-gen.com/llms.txt

Copy this page as Markdown, or grab the Markdown / llms.txt / OpenAPI machine-readable versions.