1. Create a key
An owner bootstraps the first key with the key runbook. With an administrative key, create a scoped key for voice design and speech. The secret appears once; keep it in an environment variable. Browser sign-in does not authenticate API calls.
curl -sS https://api.voice.a2nada.com/v1/keys \
-H "Authorization: Bearer $A2NADA_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"scopes":["speech","voices:read","voices:write"]}'Save the returned api_key as A2NADA_API_KEY.
2. Design, refine and save a voice
Design returns candidate voices with audio links. Listen to a candidate before choosing it.
curl -sS https://api.voice.a2nada.com/v1/voices/design \
-H "Authorization: Bearer $A2NADA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: first-voice-001" \
-d '{"description":"Warm, measured narrator with a clear low register.","sample_text":"Welcome to the north gallery.","n":2}'Use a candidate id from the response to create alternatives from feedback:
curl -sS https://api.voice.a2nada.com/v1/voices/VOICE_ID/refine \
-H "Authorization: Bearer $A2NADA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"feedback":"A little brighter, keep the calm pace.","n":2}'Lock the candidate you chose. Locking keeps its reference audio and makes the voice available to speech.
curl -sS https://api.voice.a2nada.com/v1/voices/VOICE_ID/lock \
-H "Authorization: Bearer $A2NADA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Gallery narrator","mode":"icl"}'3. Speak over HTTP
Whole-file HTTP returns a complete WAV file. Replace VOICE_ID with the selected voice ID.
curl -sS https://api.voice.a2nada.com/v1/audio/speech \
-H "Authorization: Bearer $A2NADA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input":"Welcome to the north gallery.","voice":"VOICE_ID","response_format":"wav"}' \
--output welcome.wavFor chunked audio, add "stream": true. Streamed WAV has placeholder size fields; use PCM for a raw chunk stream, or use a streaming WAV parser.
curl --no-buffer -sS https://api.voice.a2nada.com/v1/audio/speech \
-H "Authorization: Bearer $A2NADA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input":"Welcome to the north gallery.","voice":"VOICE_ID","stream":true,"response_format":"pcm"}' \
--output welcome.pcmPython with httpx:
import httpx
import os
response = httpx.post(
"https://api.voice.a2nada.com/v1/audio/speech",
headers={"Authorization": f"Bearer {os.environ['A2NADA_API_KEY']}"},
json={"input": "Welcome to the north gallery.", "voice": "VOICE_ID", "response_format": "wav"},
)
response.raise_for_status()
open("welcome.wav", "wb").write(response.content)4. Speak over WebSocket
Connect to wss://api.voice.a2nada.com/v1/audio/speech/stream, send one JSON text message, collect binary audio frames, then read the final JSON text message. Authenticate with the WebSocket Authorization: Bearer header. The default format is 24 kHz mono signed 16-bit little-endian PCM.
import asyncio
import json
import os
import websockets
async def speak():
async with websockets.connect(
"wss://api.voice.a2nada.com/v1/audio/speech/stream",
additional_headers={"Authorization": f"Bearer {os.environ['A2NADA_API_KEY']}"},
) as ws:
await ws.send(json.dumps({"input": "Welcome to the north gallery.", "voice": "VOICE_ID"}))
audio = bytearray()
async for message in ws:
if isinstance(message, bytes):
audio.extend(message)
else:
print(json.loads(message)) # done and timing fields
return bytes(audio)
pcm = asyncio.run(speak())Formats
HTTP defaults to wav; WebSocket defaults to pcm.
pcm: 24 kHz mono s16le.pcm_16000andpcm_8000: s16le at the named rate.wav: 24 kHz PCM WAV; streamed HTTP uses placeholder sizes.opus: Ogg/Opus, 32 kbps, 20 ms frames.mp3: raw MPEG-2 Layer III frames, 64 kbps at 24 kHz.mulaworulaw_8000: headerless G.711 mu-law at 8 kHz.
Errors
HTTP errors use a non-2xx status. Voice endpoints return{"error":{"code":"...","message":"..."}}; speech validation and upstream failures use {"detail":"..."}. Quota refusals return HTTP 429 with {"detail":"...","error":{"code":"...","message":"..."}} and a Retry-After header.
401: missing, invalid or revoked key.403: missing scope or voice permission.400 invalid_request: invalid voice API body. Speech input/format errors return HTTP 400 with adetailmessage.404 not_found: voice API id is missing, expired or outside the workspace. Speech lookup errors return HTTP 404 with adetailmessage.409 not_ready: the voice is still being built. Speech and lock wait for a new voice (up to 10 s) before answering this, so a first request normally just takes longer; refine answers it at once. Retry after the response'sRetry-Afterseconds.422 voice_failed: the voice's audio or speaker profile failed to build. Retrying won't help; make or upload the voice again.429 key_concurrency_limit,key_quota_exceeded,quota_pending,workspace_quota_exceeded: admission or character quota refusal. Retry after the response'sRetry-Afterseconds.502 codec_eos_missing: the model stream ended before codec EOS. HTTP streaming ends as a transport error; WebSocket sends an error frame and closes with code 1011.503: service unavailable; voice rewrite errors userewrite_unavailable.
Host stream-capacity 429s may have no error.code. WebSocket errors are JSON text frames with error and status; quota frames also include code and retry_after, then close with 1013. Full error schemas and routes are in the OpenAPI document.