Zani

API v1

Albanian audio in one call

Send text, get audio. Everything you need to put Zani in your product, or let your agent do it for you.

Quick start

  1. 1Create a key in Dashboard → API. It starts with zn_live_. Store it as an environment variable, for example ZANI_API_KEY.
  2. 2Send a POST to /v1/text-to-speech with the text and the voice.
  3. 3The response is JSON with an audioUrl. Download the file from that link.
# 1. Create the audio
curl -s -X POST https://www.tryzani.com/v1/text-to-speech \
  -H "Authorization: Bearer $ZANI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"Mirë se vini në Zani.","voice":"arben","dialect":"kosovo"}' \
  | tee response.json

# 2. Download the file from audioUrl
curl -s "$(jq -r .audioUrl response.json)" -o zani.mp3

Copy the prompt for your agent

Paste it into Claude Code, Cursor, Codex, or any other agent. It reads the full specification and integrates Zani into your project, with the key in an environment variable.

You are integrating the Zani text-to-speech API (natural Albanian voices) into this project.

## What Zani is
Zani turns text into natural Albanian speech (dialects: standard, kosovo, gheg, tosk). It is a REST API: JSON in, JSON out, Bearer auth.
Base URL: https://www.tryzani.com
Full docs: https://www.tryzani.com/en/docs

## Before you write code
1. Inspect the project: language, framework, where server-side code lives, how secrets/env vars are handled, and any existing audio/TTS code. Follow the project's conventions.
2. If it is not obvious, ask me where audio should be generated and what to do with it (play in the UI, save a file, serve to users).
3. Ask me to create an API key at https://www.tryzani.com/app/developers (it starts with "zn_live_") and store it in the environment variable ZANI_API_KEY. Never hard-code, log or commit the key. Never call the API from browser code: go through a server route or function.

## API
Auth header on every request: Authorization: Bearer $ZANI_API_KEY
Rate limit: 60 requests per minute per key, all endpoints combined. On HTTP 429 wait for the Retry-After header (60 seconds) and retry.

### POST https://www.tryzani.com/v1/text-to-speech
JSON body:
- text (string, required): 1 to 20000 characters. 1 character costs 1 credit, charged only when generation succeeds.
- voice (string, required): voice slug or id, e.g. "vjosa". List them with GET /v1/voices.
- dialect: standard | kosovo | gheg | tosk (default standard)
- emotion: neutral | warm | confident | energetic | calm | empathetic | serious | joyful | solemn (default warm)
- pace: slow | medium | medium-fast | fast (default medium)
- energy: low | medium | high (default medium)
- purpose: advertisement | audiobook | accessibility | social | documentary | meditation | education | news | podcast | narration | character (default narration)
- personality: free text, max 240 characters (optional)
- format: mp3 | wav (default mp3)

Response 200 (JSON): { id, status, queued, audioUrl, durationMs, characterCount, creditsCharged, format, voice: { id, slug, name }, error, createdAt, ... }
- status is one of "completed", "queued", "processing", "failed".
- Texts up to 900 characters finish inside the request: status "completed" and audioUrl set.
- Texts over 900 characters are queued: status "queued", audioUrl null. Poll GET /v1/generations/{id} every 2-3 seconds until status is "completed" (audioUrl set) or "failed" (error set). Stop after about 5 minutes.
- audioUrl is a temporary signed link (about 1 hour). Download the bytes and store them yourself if they are needed later.

### GET https://www.tryzani.com/v1/generations/{id}
Same JSON shape as above. Use it to poll queued generations. 404 if the id is not yours.

### GET https://www.tryzani.com/v1/voices
Returns { voices: [{ id, slug, name, gender, age, dialect, description, tags, badge, custom, sampleUrl }] }. sampleUrl is a ready-made audio sample of the voice.

### GET https://www.tryzani.com/v1/account
Returns { credits, plan, status }. Use it to check the remaining balance.

## Errors
Failures return JSON { "error": string } with these statuses:
- 400 invalid request (an "issues" array explains which field)
- 401 missing or invalid API key
- 402 not enough credits
- 404 voice or generation not found
- 429 rate limit exceeded (honour Retry-After)
- 500 / 502 temporary generation failure (no credits charged; retry once after a few seconds)
Error messages are written in Albanian: map them to your own user-facing messages instead of showing them raw.

## What to build
- A small wrapper module (for example lib/zani.ts or the equivalent for this stack) exposing speak({ text, voice, dialect?, emotion?, pace?, energy?, purpose?, format? }) that returns the audio bytes or a stored file path. It must handle polling, a 60 second request timeout, retry on 429 (Retry-After) and on 5xx once, and throw typed errors.
- Validate text length (max 20000) before calling. Split longer content on sentence boundaries into several requests.
- Cache audio for identical (text, voice, options) so credits are not spent twice.
- Add tests for the wrapper using a mocked fetch, plus a tiny command or script that generates one sentence so I can try it.
- Do not add new dependencies unless they are genuinely needed.

## Done when
An example call in this project turns Albanian text into an audio file, the key is read from ZANI_API_KEY, errors are handled, and you have told me exactly how to run it.

Authentication

Every request must include your key in the Authorization header:

Authorization: Bearer zn_live_xxxxxxxxxxxxxxxx

The key is a secret. Call the API only from your server, never from the browser or a mobile app. If it leaks, revoke it in Dashboard → API and create a new one.

Generate audio

POST/v1/text-to-speech

The body is JSON. Only the first two fields are required. The rest have sensible defaults.

  • textrequired

    string

    The text to turn into speech. 1 to 20,000 characters. 1 character = 1 credit.

  • voicerequired

    string

    Voice slug or id, for example vjosa. Full list at GET /v1/voices.

  • dialectoptional
    standardkosovoghegtosk

    Dialect. Default: standard.

  • emotionoptional
    neutralwarmconfidentenergeticcalmempatheticseriousjoyfulsolemn

    Speaking emotion. Default: warm.

  • paceoptional
    slowmediummedium-fastfast

    Speed. Default: medium.

  • energyoptional
    lowmediumhigh

    Energy. Default: medium.

  • purposeoptional
    advertisementaudiobookaccessibilitysocialdocumentarymeditationeducationnewspodcastnarrationcharacter

    What the audio is for. It shapes the delivery. Default: narration.

  • personalityoptional

    string

    Free text up to 240 characters, for example “calm and warm”.

  • formatoptional
    mp3wav

    File format. Default: mp3.

{
  "text": "Mirë se vini në Zani.",
  "voice": "arben",
  "dialect": "kosovo",
  "emotion": "confident",
  "purpose": "advertisement",
  "format": "mp3"
}

Response

You get JSON, not the audio file. When status is completed, audioUrl is the file link.

{
  "id": "cmugn72xj0000yq8hn9twxq9u",
  "status": "completed",
  "queued": false,
  "text": "Mirë se vini në Zani.",
  "characterCount": 21,
  "durationMs": 2140,
  "creditsCharged": 21,
  "format": "mp3",
  "dialect": "kosovo",
  "voice": { "id": "…", "slug": "arben", "name": "Arben" },
  "audioUrl": "https://…/audio/cmugn72xj0000yq8hn9twxq9u.mp3?X-Amz-…",
  "error": null,
  "createdAt": "2026-09-25T10:14:03.000Z"
}
  • status: completed, queued, processing, or failed.
  • audioUrl is temporary (about 1 hour). Download the file and store it yourself.
  • creditsCharged is the number of credits that were deducted.

Long texts

Texts up to 900 characters finish inside the same request. Longer ones are processed in the background: the response has status: "queued" and audioUrl: null.

GET/v1/generations/{id}

Ask every 2 to 3 seconds until status becomes completed (it has audioUrl) or failed (it has error). The shape matches the response above.

Voice list

GET/v1/voices

Returns public voices and your custom voices. Use slug in the voice field. sampleUrl is a ready-made sample of the voice.

{
  "voices": [
    {
      "id": "…",
      "slug": "vjosa",
      "name": "Vjosa",
      "gender": "female",
      "age": 32,
      "dialect": "standard",
      "description": "Zëri ynë kryesor…",
      "tags": ["signature", "standard"],
      "badge": "PRO",
      "custom": false,
      "sampleUrl": "https://…/api/voices/…/sample"
    }
  ]
}

Credits

GET/v1/account

Check remaining credits and the plan, for example before starting a large job.

{ "credits": 4820, "plan": "studio", "status": "active" }

Limits and price

  • 60 / minute

    Requests per key, all endpoints together. Past that you get 429 with Retry-After.

  • 1 character = 1 credit

    Charged only when the audio is created successfully. Failed generations are free.

  • 20,000 characters

    Maximum for one request. For longer texts, split at the end of a sentence.

  • 900 characters

    Above this, generation runs in the background and you fetch it with GET /v1/generations/{id}.

Errors

Every error returns JSON with an error field. The messages are in Albanian, so show your own message to the user.

  • 400

    Invalid request

    The body is missing or a field is wrong. The response also includes an issues list.

  • 401

    Missing or invalid key

    Check the Authorization: Bearer zn_live_… header, and that the key has not been revoked.

  • 402

    Not enough credits

    The text needs more characters than the credits you have left. Add credits under Plans.

  • 404

    Not found

    The voice or generation does not exist, or it is not yours.

  • 429

    Too many requests

    60 requests per minute per key. Wait for the Retry-After header and try again.

  • 500

    Generation failed

    Temporary error. Credits are not charged. Try again in a few seconds.

{ "error": "Nuk ke kredi të mjaftueshme." }

Full examples

These functions do the whole job: they send the request, wait if the text is long, and return the audio bytes.

const BASE = "https://www.tryzani.com";
const headers = {
  Authorization: `Bearer ${process.env.ZANI_API_KEY}`,
  "Content-Type": "application/json",
};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

export async function speak(text, voice = "vjosa", options = {}) {
  const res = await fetch(`${BASE}/v1/text-to-speech`, {
    method: "POST",
    headers,
    body: JSON.stringify({ text, voice, ...options }),
  });
  if (!res.ok) throw new Error((await res.json()).error ?? res.statusText);
  let job = await res.json();

  // Texts over 900 characters are processed in the background.
  while (job.status === "queued" || job.status === "processing") {
    await sleep(2500);
    const poll = await fetch(`${BASE}/v1/generations/${job.id}`, { headers });
    job = await poll.json();
  }
  if (job.status !== "completed") throw new Error(job.error ?? "Generation failed");

  const audio = await fetch(job.audioUrl); // temporary link, save the file
  return Buffer.from(await audio.arrayBuffer());
}