Text to speech

Send text, receive audio. Compatible with the OpenAI speech API, with a few extra controls.

POST/v1/audio/speech

The response body is raw audio bytes, not JSON. Write it straight to a file or pipe it to a player. Audio starts arriving before the whole clip is rendered, so playback can begin early.

curl https://api.xkiro.com/v1/audio/speech \
  -H "Authorization: Bearer $XKIRO_API_KEY" \
  -H "Content-Type: application/json" \
  -o speech.mp3 \
  -d '{
    "model": "xkiro-voice",
    "input": "The quick brown fox jumps over the lazy dog.",
    "voice": "mexican-female",
    "response_format": "mp3"
  }'

Request body#

Standard fields
FieldTypeDescription
modelrequiredstringA speech model ID — currently xkiro-voice.
inputrequiredstringThe text to speak. Billing is per character of this field, so trim boilerplate.
voicestringVoice ID. See Voices below. Omit for the model default.
response_formatstringmp3 (default), opus, aac, flac, wav or pcm. Support varies by model; unsupported values return a clear 400.
speednumber0.25–4.0. 1.0 is the natural rate.
xKiro extensions
FieldTypeDescription
pitchnumberShifts the voice higher or lower. Ignored by models that cannot change pitch.
volumenumberOutput gain.
emotionstringDelivery style, e.g. 'happy', 'sad', 'angry'. Available values depend on the voice.
streambooleanSend audio in chunks as it is produced, for a lower time to first byte. The bytes are the same either way.

Unknown fields are ignored, not rejected

A strict OpenAI client can send this request unchanged, and an xKiro-aware client can add the extensions above. Neither breaks the other.

Voices#

GET/v1/audio/voices

Public, cached for five minutes. Filter by `locale`, `languageKey`, `gender`, `isVip` or a free-text `q`.

curl "https://api.xkiro.com/v1/audio/voices?gender=female&limit=20"
200 OK
{
  "total": 145,
  "returned": 145,
  "voices": [
    {
      "id": "mexican-female",
      "name": "Mexican Female",
      "locale": null,
      "languageKey": null,
      "gender": null,
      "isVip": false,
      "categories": []
    },
    {
      "id": "female-voice-german",
      "name": "Female Voice (German)",
      "locale": null,
      "languageKey": null,
      "gender": null,
      "isVip": false,
      "categories": []
    }
  ]
}

Billing#

  • Speech is billed per character of input, not per token.
  • Your plan's daily allowance covers a number of characters; past that, usage is charged to your wallet. See Pricing & billing.
  • A failed request is not charged. A request you cancel part-way is charged for the audio already produced.

Concurrency limit#

Speech is synchronous and holds an upstream connection for its whole duration, so each account has a cap on simultaneous speech requests — a separate limit from requests per minute. Exceeding it returns 429; wait for one to finish rather than retrying immediately.

Practical notes

  • Split long text into paragraphs and request them separately. You get audio sooner and a failure costs you one paragraph, not the whole chapter.
  • Cache generated audio keyed by the exact input, voice and settings. Speech for unchanged text is the easiest cost to eliminate.
  • Punctuation drives pacing. Adding commas and full stops does more for naturalness than adjusting speed.