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
Codex
AiderMCP tools
| Tool | Description | Auth Required | Required Scope(s) |
|---|---|---|---|
| list_gallery | List official preset voices (read-only) | sk_ key | tts:generate |
| list_my_voices | List personal voices | sk_ key | voices:manage |
| create_my_voice | Create a personal voice from local audio (plan-gated) | sk_ key | voices:manage |
| delete_my_voice | Delete a personal voice (hidden instantly, can no longer generate; history keeps the name) | sk_ key | voices:manage |
| tts_generate | Generate in preset mode (voice slug) | sk_ key | tts:generate |
| generate_design | Generate in design mode (gender + age) | sk_ key | tts:generatetts:design |
| generate_clone | Generate in clone mode (local reference audio; plan-gated) | sk_ key | tts:generatetts:clone |
| check_job | Poll async job status until done (how to track the async /v1/audio/jobs flow) | sk_ key | tts:generatetts:design |
| cancel_job | Cancel a generation job (not started → full refund; in progress → cancelled after wrap-up, completed portion still billed; settled jobs cannot be cancelled) | sk_ key | tts:generatetts:design |
MCP for AI agents
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.
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:
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:generate | Default. Unlocks voice & design generation plus all read endpoints (voice catalog, job polling). |
| tts:clone | Required for clone mode (voice cloning from a reference sample). |
| tts:design | Fine-grained scope for design mode (gender + age synthesized voices) — already covered by tts:generate; either one suffices. |
| voices:manage | Required for personal voice management (MCP list / create / delete my voices). |
Limits attached to every key
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
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.
List the voice catalog (GET /v1/voices)
curl https://canto-gen.com/v1/voices \
-H "Authorization: Bearer $CANTOGEN_API_KEY"{
"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.
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:
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
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.
| slug | Name | Gender | Age | Scene | Preview |
|---|---|---|---|---|---|
| keung-gor | 強哥 | male | midage | narration | |
| lok-zai | 樂仔 | male | adult | story | |
| sung-pak | 崇伯 | male | elder | narration | |
| fung-yee | 鳳儀 | female | adult | story | |
| sum-yu | 芯瑜 | female | young | ads | |
| wai | 小慧 | female | adult | story | |
| kay | 凱琪 | female | young | sports | |
| lok | 嘉樂 | male | adult | comedy | |
| sum | 心妍 | female | adult | news | |
| wing-lam-2 | 穎琳 | female | adult | business | |
| bon | 阿邦 | male | adult | narration | |
| ah-sum | 阿森 | male | young | narration | |
| sze-kei | 思琪 | female | midage | narration | |
| hoi | 小海 | male | young | ads | |
| wang-tat | 宏達 | male | young | narration |
Generation presets (tempo · emotion · timbre)
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
voicedefaultGallery 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.
# 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 audioDesigned 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.
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 scopeCloned 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.
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)
| style | Delivery 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. |
| emotion | One 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. |
| instruction | Free-text delivery direction, up to 500 characters. Priority: style → emotion → instruction — instruction has the final say. |
| pitch | Deprecated (2026-09-20): no-op under raw audio delivery (accepted but ignored). Historical range −12…+12 semitones, default 0. |
| volume | Deprecated (2026-09-20): no-op under raw audio delivery (accepted but ignored). Historical range 0–10, default 1. |
| response_format | mp3 (default) / wav. |
| srt | true → the completed job carries an srt field (billed at a higher character rate). Available on all plans. |
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_idGET /v1/audio/jobs/{id}poll · 2–5 s backoffstatus: completedaudio_url readyGET audio_urlpre-signed · no authPoll a job
curl https://canto-gen.com/v1/audio/jobs/b7e6f2a1-4c3d-4e5f-8a9b-0c1d2e3f4a5b \
-H "Authorization: Bearer $CANTOGEN_API_KEY"Completed response
{
"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 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
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
Rate limits & caps
| Response header | Description |
|---|---|
| X-RateLimit-Limit | Per-minute request cap for this key (default 60; configurable at key creation). |
| X-RateLimit-Remaining | Requests left in the current 60-second sliding window. |
| X-RateLimit-Reset | Window reset time (Unix epoch seconds). |
| Retry-After | Attached 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.update | OpenAI-native | Open/update the session: voice, language, output_audio_format, max_session_chars, tts.segmentation. |
| conversation.item.create | OpenAI-native | Send one explicit utterance (input_text); triggers synthesis. |
| input_text.append | Canto-Gen extension | Send raw token deltas — stream as you generate. |
| input_text.commit | Canto-Gen extension | Force a segment boundary (flush now). |
| response.create | OpenAI-native | Trigger synthesis. |
| response.cancel | OpenAI-native | Barge-in: stop the current utterance and refund it. |
| session.close | Canto-Gen extension | Close the session and settle. |
Events — server → client
| session.created | OpenAI-native | Session opened; carries the server-minted session id. |
| session.updated | OpenAI-native | Config ack. |
| response.created | OpenAI-native | An utterance begins. |
| response.output_audio.delta | OpenAI-native | A base64 PCM16 @ 48 kHz audio chunk. |
| response.output_audio.done | OpenAI-native | Utterance audio complete. |
| response.done | OpenAI-native | Utterance done; usage carries input_chars / billed_chars / remaining (extension fields). |
| error | OpenAI-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.
// 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_key | 401 | Key missing, invalid, expired, or wrong scope. |
| rate_limit_exceeded | 429 | Per-key per-minute limit hit; carries retry_after. |
| sandbox_daily_limit | 429 | Free plan's 50 generations per day are used up (shared counter with REST); try again tomorrow or upgrade. |
| monthly_cap_reached | 429 | The key's monthly character cap is reached. |
| character_limit_reached | 402 | Account character pool exhausted — the utterance is not synthesized. |
| concurrency_limit_reached | 429 | Concurrent session or un-settled utterance cap reached. |
| session_char_limit_exceeded | 403 | This session's character cap is reached. |
| raw_limit_exceeded | 413 | Raw input volume (including punctuation/whitespace) exceeds the cap. |
| text_too_long | 400 | A single utterance exceeds the length limit. |
| invalid_session | 400 | The session is closed or otherwise invalid. |
| engine_unavailable | 503 | The realtime engine is temporarily unavailable; retry. |
Errors
Errors use a stable machine-readable error code plus an optional human-readable message.
{
"error": "text_too_long",
"message": "text exceeds 500 characters."
}| HTTP | error code | Cause | Fix |
|---|---|---|---|
| 400 | invalid_model | model must be "cantogen-tts" (or omitted). | Omit model or set "cantogen-tts". |
| 400 | voice_mode_conflict | More than one of voice / design / clone was sent — the three modes are mutually exclusive. | Send exactly one voice mode per request. |
| 400 | text_required | text is missing or empty. | Include a non-empty text (1–500 characters). |
| 400 | text_too_long | text exceeds 500 characters. | Split the text — over-limit requests are rejected, not truncated. |
| 400 | unsupported_format | response_format must be mp3 or wav. | Use mp3 or wav. |
| 400 | invalid_pitch | pitch must be between −12 and +12 semitones. | Adjust pitch to the valid range. |
| 400 | invalid_volume | volume must be between 0 and 10. | Adjust volume to the valid range. |
| 400 | invalid_emotion | emotion is not one of the 9 supported values. | Use one of the 9 supported emotions. |
| 400 | invalid_srt | srt must be a boolean. | Use true / false. |
| 400 | reference_audio_required | clone.reference_audio is missing. | Attach a base64 reference sample. |
| 400 | invalid_reference_audio | clone.reference_audio is not valid base64 audio. | Send a data URL (data:audio/…;base64,…) or raw base64. |
| 400 | invalid_reference_audio_size | clone.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. |
| 400 | spoken_text_required | text has no speakable content — zero billable characters (only punctuation / whitespace / emoji / tone tags). | Include text with readable words. |
| 400 | design_gender_and_age_required | Both design.gender and design.age are required. | Provide both fields. |
| 400 | invalid_job_id | The poll or cancel id is not a UUID. | Check the job_id. |
| 400 | invalid_request | The 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. |
| 413 | payload_too_large | The 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. |
| 401 | invalid_or_expired_token | API key missing, invalid, expired, or revoked. | Create a new key and check the Bearer header. |
| 402 | character_limit_reached | The account character pool cannot cover this generation (the body includes remaining). | Buy a credit pack or wait for the monthly reset. |
| 403 | insufficient_scope | The key lacks the required scope (cloning needs tts:clone); the body includes "scope". | Create a key with the required scope. |
| 403 | monthly_cap_reached | The key's monthly character cap is reached; the body includes "used". | Wait for the UTC month to reset or raise the cap. |
| 404 | job_not_found | No job with this id (or it belongs to another key). | Check the job_id and the key used to submit. |
| 409 | already_settled | The 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. |
| 409 | duplicate_request | The 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. |
| 404 | not_found | No such endpoint (typo in the path). | Check the paths in /v1/openapi.json. |
| 403 | batch_requires_pro | Bulk 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. |
| 429 | capacity_exceeded | The public API generation pool is at its global concurrency cap. | Wait Retry-After seconds, or use the async `POST /v1/audio/jobs` path. |
| 429 | rate_limit_exceeded | Per-key per-minute limit hit. | Wait Retry-After seconds (also mirrored in the body as retry_after), then retry. |
| 429 | sandbox_daily_limit | Free 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. |
| 429 | api_busy | The API generation queue is at its global capacity cap (rejected before reserve — nothing billed). | Wait a few seconds and retry. |
| 502 | audio_delivery_failed | Generation completed but the audio could not be fetched. | Poll GET /v1/audio/jobs/{id} and download the audio_url instead. |
| 503 | sync_generation_failed | The job failed or was cancelled during the sync wait (already refunded automatically). | Retry via POST /v1/audio/jobs (async). |
| 503 | reference_upload_failed | The clone reference audio could not be stored temporarily. | Simply retry. |
| 503 | voice_gallery_unavailable / jobs_unavailable | Transient backend issue. | Retry with backoff. |
| 400 | unsupported_response_format | Transcription response_format currently supports json / text only — srt / verbose_json need segment-level timestamps and are not yet available. | Use json or text. |
| 402 | insufficient_balance | The 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). |
| 429 | daily_limit_exceeded | The daily billable-audio cap (24 hours/day, Hong Kong day boundary) is reached. | Retry tomorrow (Hong Kong time). |
| 503 | engine_unavailable | The 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.
/v1/audio/speech200 syncShort-text synchronous generation — 200 audio bytes (voice/design, ≤300 chars).
完整 schema → Scalar Reference/v1/audio/jobs202 asyncSubmit a speech job (single or bulk ≤25; bulk requires Starter or above). Returns 202 with job_id(s) and poll_url.
完整 schema → Scalar Reference/v1/audio/jobs/{id}pollPoll a job; when completed the response carries a pre-signed audio_url.
完整 schema → Scalar Reference/v1/voicesList the Cantonese-optimized voice catalog, with preview clips. This endpoint allows anonymous access.
完整 schema → Scalar Reference/v1/asr/transcribe200 syncTranscribe audio to text synchronously; billed by audio duration (HK$1.40/hour), success-only.
完整 schema → Scalar Reference/v1/balanceGet the prepaid balance (HKD), transcription/dictation rates and auto top-up settings.
完整 schema → Scalar Reference# 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.txtCopy this page as Markdown, or grab the Markdown / llms.txt / OpenAPI machine-readable versions.