# SpoonAI TTS API > SpoonAI (Lucy) is a Vietnamese-first text-to-speech and voice-cloning platform. > This document lets an AI agent generate speech audio programmatically using an > API key. All calls go through a single JSON-RPC-style HTTP endpoint. Base URL: `https://spoonai-tts-api.lucylab.io` RPC endpoint: `POST https://spoonai-tts-api.lucylab.io/json-rpc` ## Authentication - Every request needs a header: `Authorization: Bearer `. - API keys start with `sk_`. Create and manage keys in the web dashboard at `/docs/api-keys` — keys **cannot** be created via the API for security. - Each account has one active key; revoking it in the dashboard invalidates it immediately. The key acts on behalf of the account that owns it. ## Request / response format Not classic JSON-RPC 2.0 — a small custom envelope: - Request body (always `POST`, `Content-Type: application/json`): ```json { "method": "", "input": { /* method params */ } } ``` - Success (HTTP 200): `{ "result": }` - Error (also HTTP 200): `{ "error": { "code": "...", "message": "...", "data": ... } }` Always check for an `error` field before using `result`. Errors return HTTP 200, so do not rely on the status code alone. ## Quick start — synthesize a short line (`tts`) `tts` is synchronous: it waits for the audio and returns a ready URL. It is a **preview** call and is not charged credits. Best for single sentences/blocks. ```bash curl -X POST https://spoonai-tts-api.lucylab.io/json-rpc \ -H "Authorization: Bearer sk_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "method": "tts", "input": { "text": "Xin chào, đây là giọng nói AI.", "userVoiceId": "YOUR_VOICE_ID", "speed": 1.0 } }' ``` Response: `{ "result": { "url": "https://.../audio.wav" } }` `tts` input: - `text` (string, required) - `userVoiceId` (string, required) — see "Finding a voice" below - `speed` (number, optional) — default `1.0`, range `0.5`–`2.0` - `model` (string, optional) — pick a specific model of the voice (see Models) - `blockVersion` (number, optional) — bump to force a fresh generation ## Long text — async export (`ttsLongText` → poll `getExportStatus`) For paragraphs/articles, use `ttsLongText`. It splits the text into sentences, runs an export job, and returns an id you poll until it finishes. This path **consumes credits** based on character count. 1. Start the job: ```json { "method": "ttsLongText", "input": { "text": "", "userVoiceId": "YOUR_VOICE_ID", "speed": 1.0 } } ``` Response: `{ "result": { "projectExportId": "...", "characterCount": 1234, "blockCount": 12 } }` 2. Poll every few seconds until done: ```json { "method": "getExportStatus", "input": { "projectExportId": "..." } } ``` Response while running: `{ "result": { "state": "processing", "progress": 0.4 } }` Response when done: `{ "result": { "state": "completed", "url": "https://.../final.mp3", "srtUrl": "https://.../subs.srt" } }` `state` is one of: `pending`, `processing`, `completed`, `failed`, `canceled`. Read `url` (and optional `srtUrl` subtitles) once `state === "completed"`. On `failed`, read `error`. ## Finding a voice (`userVoiceId`) Every synthesis needs a `userVoiceId`. Discover ids via: - `getUserVoices` — voices owned by the account. input: `{ "search"?: string, "model"?: string, "limit"?: number, "page"?: number }` - `getCommunityVoices` — public community voices. input: `{ "tags"?: string[], "search"?: string, "sortBy"?: "score" | "newest", "model"?: string, "limit"?: number, "page"?: number }` - `getUserVoice` — one voice by id. input: `{ "userVoiceId": string }` - `getUserVoicesByIds` — batch. input: `{ "ids": string[] }` Each voice object includes: `id`, `name`, `description`, `tags[]`, `isPublic`, `modelInfo` (default model), and `modelInfos[]` (all models the voice supports). Use `voice.id` as `userVoiceId`. A voice must be active to synthesize. List responses are paginated: `{ "items": [...], "page": n, "limit": n, "total": n, "hasNext": bool }`. ## Models A voice can support several TTS engines. Pass `model` to `tts` to select one (it must exist in the voice's `modelInfos`); omit it to use the voice default. Valid values: `lucyV1`, `lucyV2`, `lucyV3`, `lucyV4`, `spoonaiV1`, `siaTTS`, `akiTTS`. ## Account & credits - `getUserInfo` — input `{}` → `{ "user": { ... , "creditsRemaining", "subscriptionTier", ... } }`. Check `creditsRemaining` before long exports. - `tts` previews are free; `ttsLongText`/`exportAudio` deduct credits per character. ## Projects (optional, for reusable multi-block audio) If you want to build and re-render a document rather than one-shot long text: - `createProject` — `{ "name": string, "userVoiceId": string }` → project - `updateProjectBlocks` — set/replace the text blocks of a project - `updateProjectVoice`, `updateProjectBlocksSpeed`, `updateProjectBlocksPauseDuration` - `exportAudio` — `{ "projectId": string }` → `{ "projectExportId", "jobId", "status", "srtUrl"? }` (then poll `getExportStatus`, same as above) - `getProjectExports` — `{ "projectId": string }` → paginated export history (each has `url`, `mp3Url`, `srtUrl`) - `getProject`, `getMyProjects`, `renameProject`, `deleteProject`, `cancelExport`, `regenerateBlockVersion` ## Methods available via API key Only these methods are callable with an API key (whitelist): `createProject`, `getProject`, `getMyProjects`, `renameProject`, `deleteProject`, `updateProjectBlocks`, `updateProjectBlock`, `updateProjectBlocksSpeed`, `updateProjectBlocksPauseDuration`, `updateProjectVoice`, `regenerateBlockVersion`, `tts`, `ttsLongText`, `exportAudio`, `getProjectExports`, `getExportStatus`, `cancelExport`, `getUserVoices`, `getUserVoicesByIds`, `getCommunityVoices`, `getUserVoice`, `getUserInfo`. Blocked from API keys (web UI only): API-key management, voice creation/editing (`createUserVoice`, `updateUserVoice`, activate/deactivate), file uploads, and all admin methods. Calling a blocked/unknown method returns an error. ## Minimal agent recipe 1. `getUserVoices` (or `getCommunityVoices`) → choose a `userVoiceId`. 2. Short text → `tts` → use `result.url`. Long text → `ttsLongText` → poll `getExportStatus` until `completed` → use `result.url`. 3. Handle the `error` envelope on every call.