All Documentation

REST API (v1)

The full Tonetta API: projects, characters, voices, saved content, and audio generation — enforced by scoped API keys

Basics

Base URL: https://<host>/api/v1

Authenticate every request with a scoped API key via Authorization: Bearer <key> (or X-API-Key). Requests are rate-limited to 60/minute per key. Responses use a Cloudflare-style envelope:

// Success
{ "success": true, "result": { ... } }

// Failure (401 / 402 / 403 / 404 / 409 / 429 / 500)
{ "success": false, "errors": [{ "code": "missing_scope", "message": "..." }] }

Scopes

Each endpoint below lists its required scope. write implies read. Content endpoints resolve the scope per item: lines need lines:*, sound effects need sfx:*, 3D models need models:*.

Verify

GET/api/v1/verify

No scope required. Returns the key's name, organization, and scopes. Use it in health checks.

Organization

The key's organization and its organization-wide art direction for 3D models — the layer every project inherits (see the 3D Models guide). Scopes organization:read / organization:write.

GET/api/v1/organizationorganization:read

Returns id, name, artDirection (the house style, or empty) and limits — { maxActiveJobs, activeJobs }, how many model jobs this organization may run at once and how many are running now.

PATCH/api/v1/organizationorganization:write
FieldTypeDescription
artDirection*stringThe house style for 3D models, in plain prose — shapes, era, materials, colours, sizes, what to avoid (≤ 8000 chars). Empty string clears it

Text naming a trademarked franchise is refused with 422 prompt_rejected; bad values answer 400 invalid_field.

Projects

A project is the workspace that voices, characters, and saved content belong to. A fresh organization has none — create one before anything else.

GET/api/v1/projectsprojects:read

Lists the organization's projects: id, name, description, defaultVoiceSampleText, artDirection (the 3D-model house style, when set), timestamps.

GET/api/v1/projects/{'{id}'}projects:read
POST/api/v1/projectsprojects:write
FieldTypeDescription
name*stringDisplay name
descriptionstringFree-text description
defaultVoiceSampleTextstringText the audition endpoints speak by default
artDirectionstringArt direction for 3D models, in plain prose (≤ 8000 chars) — see the 3D Models guide
PATCH/api/v1/projects/{'{id}'}projects:write

Partial update of name, description, defaultVoiceSampleText, artDirection (send an empty string to clear it). Only provided fields change. Text that names a trademarked franchise is refused with 422 prompt_rejected.

DELETE/api/v1/projects/{'{id}'}projects:write

Returns 409 in_use while characters, voices, or saved content still belong to the project.

Live Endpoints

A live endpoint is a stored text + character + settings combination that the Public TTS API serves as cached audio at a stable URL. Every item returned here carries publicTtsPath.

GET/api/v1/live-endpoints?projectId={'{id}'}&characterId={'{id}'}live_endpoints:read
GET/api/v1/live-endpoints/{'{id}'}live_endpoints:read
POST/api/v1/live-endpointslive_endpoints:write
FieldTypeDescription
name*stringDisplay name
text*stringThe line the endpoint speaks
characterId*stringCharacter whose voice and persona are used
projectIdstringDefaults to the character’s project
ttlMinutesnumberHow long generated audio stays cached (default 60)
useSSMLbooleanAdd delivery markup when generating (default true)
changeTonebooleanRewrite the text in the character’s voice when generating (default true)
ttsSettingsobjectVoice overrides — same fields as the /tts settings object
PATCH/api/v1/live-endpoints/{'{id}'}live_endpoints:write

Partial update of the same fields; ttsSettings merges into the stored settings. Cached audio is keyed by endpoint id and expires by TTL, so a changed text is served once the current cache entry expires.

DELETE/api/v1/live-endpoints/{'{id}'}live_endpoints:write

Characters

A character binds a voice to a persona (characterDefinition holds tone instructions used for text enhancement). external tells Tonetta which kind of voice voiceId references.

GET/api/v1/characters?projectId={'{id}'}characters:read
GET/api/v1/characters/{'{id}'}characters:read
POST/api/v1/characterscharacters:write
FieldTypeDescription
name*stringDisplay name
projectId*stringProject the character belongs to
voiceId*stringInternal (Voice Studio) voice id — or a project voice id with external: true
externalbooleantrue when voiceId is a project (external provider) voice
characterDefinitionstringPersona/tone instructions used to enhance text before speaking

The referenced voice is validated against the right store before the character is created.

DELETE/api/v1/characters/{'{id}'}characters:write

Returns 409 if live endpoints still use the character.

Project Voices

GET/api/v1/voices?projectId={'{id}'}voices:read
GET/api/v1/voices/{'{id}'}voices:read
POST/api/v1/voicesvoices:write
FieldTypeDescription
name*stringYour name for the voice
projectId*stringProject to add the voice to
voiceProviderId*stringProvider connection id (see External Voices below)
externalVoiceId*stringThe provider-native voice id
DELETE/api/v1/voices/{'{id}'}voices:write

Returns 409 if characters still use the voice.

Internal Voices (Voice Studio)

GET/api/v1/internal-voicesinternal_voices:read
POST/api/v1/internal-voices/designinternal_voices:write
FieldTypeDescription
prompt*stringDescription of the desired voice

Returns requestId and preview files with temporary URLs (~1 hour). Spends the voice-creation credit cost; refunded on failure.

POST/api/v1/internal-voicesinternal_voices:write
FieldTypeDescription
name*stringVoice name
requestId*stringFrom the design call
audioFileUrl*stringThe chosen preview URL from the design call
prompt*stringThe prompt the voice was designed from
descriptionstringOptional description
projectIdstringOptional project association
DELETE/api/v1/internal-voices/{'{id}'}internal_voices:write

Returns 409 if characters still use the voice.

No cloning via API

Voice cloning from arbitrary audio is dashboard-only by design.

External Voice Providers

GET/api/v1/voice-providersexternal_voices:read

The organization's provider connections (credentials are never returned).

GET/api/v1/external-voices?voiceProviderId={'{id}'}external_voices:read

Voices available from that provider account, with voice_id values usable in POST /voices.

Saved Content

Lines, sound effects, and 3D models with full version history. Top-level fields always mirror the active version. Audio items carry audioUrl; models carry files[] (each with role, name, url, sizeBytes), model (the metadata), thumbnailUrl, and glbUrl.

GET/api/v1/content?contentType=&projectId=&characterId=lines:read | sfx:read | models:read | music:read

Unfiltered calls return only the types the key can read. contentType is line, sfx, or model.

GET/api/v1/content/{'{id}'}read (by item type)
POST/api/v1/contentwrite (by contentType)

multipart/form-data: audio (WAV/MP3/OGG/FLAC/M4A/AAC/WMA, ≤50MB), name, contentType, projectId?, characterId?, textToScreen?, tags?, source?, durationMs?. Lines require project, character, and text.

POST/api/v1/content/{'{id}'}/versionswrite (by item type)

multipart/form-data: audio + optional fields. The new version becomes active.

PATCH/api/v1/content/{'{id}'}write (by item type)
FieldTypeDescription
activeVersionintegerVersion number to make active (send this alone)
namestringNew display name (1-100 characters; a 3D model name is its asset name: letters, digits, underscores, hyphens, starts with a letter, ≤ 48)
tagsstring[]Replaces the tag list (≤ 20)

Either pin a version (activeVersion) or edit details (name and/or tags); the project an item belongs to cannot be changed.

DELETE/api/v1/content/{'{id}'}write (by item type)

Removes the record and every version's files. Cannot be undone. A model with a running job answers 409 job_in_progress.

POST/api/v1/content/{'{id}'}/refinemodels:write

3D models only: { instructions, referenceImages?, budgetUsd? } starts a job that produces version n+1 from the active version's build script and .blend — see 3D Models. Returns 202 with the job, like /models/generate below.

Generation

Generation spends credits, charged up front and refunded automatically on failure. Results are uploaded to storage and returned as URLs.

POST/api/v1/ttstts:generate
FieldTypeDescription
characterId*stringThe character to speak as (internal or external voice)
text*stringWhat to say
settingsobjectOptional overrides: stability, similarityBoost, useSpeakerBoost, speed, model, pitch, volumeGain, exaggeration
saveobjectOptional { name, textToScreen? } — also save the result as a line, or { contentId, textToScreen? } — save it as the next active version of that line (same character). Requires lines:write
POST/api/v1/sfx/generatesfx:write
FieldTypeDescription
text*stringWhat the effect should sound like
durationSeconds*numberLength in seconds (provider max applies)
promptInfluencenumber 0-1Prompt adherence vs variety (default 0.3)
loopbooleanSeamless loop
saveobjectOptional { name, projectId?, tags? } — also save to Saved Content, or { contentId } — save it as the next active version of that item
GET/api/v1/music/catalogmusic:read
FieldTypeDescription
—stringNo parameters. Returns the music models cheapest first: model, name, maxDurationSeconds, credits.
POST/api/v1/music/generatemusic:write
FieldTypeDescription
prompt*stringGenre, instruments, mood and tempo
durationSeconds*numberLength in seconds (model max applies). Charged per generation, so length does not affect the price.
modelstringA model id from /music/catalog (omit for the cheapest)
negativePromptstringQualities to avoid, e.g. "vocals"
seednumberReproduce a previous track
expandPromptbooleanExpand a terse brief before generating
saveobjectOptional { name, projectId?, tags? } — also save to Saved Content, or { contentId } — save it as the next active version of that item

Generate a line and save it in one call:

curl -s https://<host>/api/v1/tts \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "characterId": "<characterId>",
    "text": "Welcome back, adventurer!",
    "save": { "name": "Welcome Back" }
  }'

Response:

{
  "success": true,
  "result": {
    "audioUrl": "https://<r2-public>/lines/<org>/<project>/<uuid>.mp3",
    "audioKey": "lines/<org>/<project>/<uuid>.mp3",
    "contentType": "audio/mpeg",
    "characterId": "…",
    "characterName": "Narrator",
    "internalVoice": false,
    "creditsCharged": 5,
    "tokensCharged": 5,
    "savedContent": { "id": "…", "name": "Welcome Back", ... }
  }
}

Live-endpoint TTS

The cached, live-endpoint-driven TTS API remains at GET /api/public/tts?liveEndpointId=… (scope tts:generate) — see Public TTS API.

3D models (asynchronous jobs)

Model generation takes minutes, so it is the one operation that runs as a job: submit, then poll (or long-poll) until the status is SUCCEEDED, FAILED, or CANCELLED. Credits are reserved at submit and settled to the actual cost on completion. Presets, tiers, and the model catalog are described in 3D Models.

GET/api/v1/models/catalogmodels:read

Models (key, vision, estimatedTokens per tier — what a job typically settles to — and reserveTokens — what is held at submit), tiers with their caps and base reserve, presets with bones/sockets/clips, styles (flat / stylized / realistic with what each builds), targets, texture maps. The build agent (Anvil) chooses its own vendors; no request field selects one. LLM prices are in Tonetta credits only.

POST/api/v1/models/generatemodels:write
FieldTypeDescription
prompt*stringThe whole specification (≤ 2000 characters) — the build agent reads it and decides the asset name, the rig, the style, the weathering, the clips and the maps itself. Named franchises are refused with 422 prompt_rejected
projectIdstringOptional project — its art direction then applies
modelstringCatalog key from /models/catalog (omit for the default)
presetstringRig preset to build to, a name from /models/catalog presets (pass ?projectId= to include the project’s own). Omit it and the agent chooses; name it when game code or animations already depend on that contract
seedFirstbooleanImport a generated mesh before the agent starts building (default true for a new model). false builds from primitives — cheaper, flatter on sculpted shapes
referenceImagesstring[]Up to 4 URLs from POST /v1/models/references — images the agent looks at before it plans
budgetUsdnumberSpend ceiling in USD of provider cost, capped at 50; a premium model needs more room than the default

202 → { jobId, contentId, status: "queued", creditsReserved, model, tier, job }. Supports an Idempotency-Key header (a replay returns the original job with 200). Errors: 402 insufficient_tokens, 409 too_many_active_jobs, 422 prompt_rejected.

POST/api/v1/models/sessionsmodels:write

Start a conversation with the build agent instead of a fire-and-forget job: { message, contentId?, projectId?, preset?, referenceImages? }. The message is the brief and a new model is created, or contentId continues an existing model from its active version. Each message is one step: the agent builds, renders or researches, shows the result and asks where to go next; it proposes paid vendor calls with their price and waits for a yes unless the message asked for them. Answers 202 with{ session, job } — poll the job, then read the session. Each turn is billed to what it spent.

GET/api/v1/models/sessions/{'{contentId}'}models:read

The conversation: every message, what each turn did (tools, results), its renders, the version each export saved, cost, and runningJob while a turn runs. null when none exists.

POST/api/v1/models/sessions/{'{contentId}'}/messagesmodels:write

{ message, approvedTools? } — the next step. 409 turn_in_progress while the last turn is still running. approvedTools lets a paid tool the agent proposed run this turn.

POST/api/v1/models/sessions/{'{contentId}'}/exportmodels:write

{ message? } — an export turn: the agent saves what exists as a new version of the model.

GET/api/v1/models/jobs/{'{jobId}'}?wait={'{seconds}'}models:read

The job: status, stage, progress (iteration, the turns taken, against budgetUsd), previews[] with render URLs, creditsReserved / creditsCharged, and on success contentId + versionNumber. With wait (≤ 240 s) the call returns as soon as the job reaches a terminal status; the response adds done.

GET/api/v1/models/jobs?status=&projectId=&contentId=models:read

status accepts a comma-separated list, e.g. QUEUED,RUNNING,FINISHING.

POST/api/v1/models/jobs/{'{jobId}'}/cancelmodels:write

A queued job is cancelled and refunded at once; a running job stops after its current step and is charged for the cost so far.

Submit and long-poll:

curl -s https://<host>/api/v1/models/generate \
  -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
  -d '{ "prompt": "twin-engine attack submarine, angular riveted hull, forward laser cannons, missile pods under the stub wings, salt-worn navy paint over bare steel" }'

curl -s "https://<host>/api/v1/models/jobs/<jobId>?wait=240" -H "Authorization: Bearer $API_KEY"

Credit Costs

Amounts are in credits (1 credit = $0.01 of pack price). Responses name them with credits… fields (creditsReserved, creditsCharged, estimatedCredits, reserveCredits, creditCost); the older tokens… spellings are still sent as deprecated aliases with the same values, and the insufficient_tokens error code keeps its name.

OperationCost (credits)
TTS — external voice5
TTS — internal voice10
SFX generationper second (provider pricing)
Voice design / save (each)25
3D model generation — quick / detailed25 / 90 reserved, settled to actual LLM + compute cost
3D model refine — quick / detailed20 / 70 reserved, settled to actual cost

Model jobs are the only reservation-based charge: the flat amount is taken at submit, reconciled on completion (the difference is returned), refunded entirely on failure, and charged only for work done on cancel.