API quickstart

Design a voice, save it, and speak with it from an agent or application.

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.wav

For 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.pcm

Python 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_16000 and pcm_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.
  • mulaw or ulaw_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 adetail message.
  • 404 not_found: voice API id is missing, expired or outside the workspace. Speech lookup errors return HTTP 404 with a detail message.
  • 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's Retry-After seconds.
  • 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-After seconds.
  • 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 use rewrite_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.