API v1
Audio shqip me një thirrje
Dërgo tekst, merr audio. Këtu ke gjithçka për ta futur Zanin në produktin tënd, ose e le agjentin tënd ta bëjë për ty.
Fillimi i shpejtë
- 1Krijo një çelës te Paneli → API. Fillon me
zn_live_. Ruaje si variabël mjedisi, p.sh.ZANI_API_KEY. - 2Dërgo një
POSTte/v1/text-to-speechme tekstin dhe zërin. - 3Përgjigja është JSON me një
audioUrl. Shkarko skedarin nga ajo lidhje.
# 1. Krijo audion
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. Shkarko skedarin nga audioUrl
curl -s "$(jq -r .audioUrl response.json)" -o zani.mp3Kopjo prompt-in për agjentin tënd
Ngjite në Claude Code, Cursor, Codex ose çdo agjent tjetër. Ai lexon specifikimin e plotë dhe integron Zanin në projektin tënd, me çelësin në variabël mjedisi.
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/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.Autentifikimi
Çdo kërkesë duhet të ketë çelësin tënd në kokën Authorization:
Authorization: Bearer zn_live_xxxxxxxxxxxxxxxxÇelësi është sekret. Thirre API-n vetëm nga serveri yt, kurrë nga shfletuesi ose aplikacioni mobil. Nëse del në publik, revoko atë te Paneli → API dhe krijo një të ri.
Gjenero audio
Trupi është JSON. Vetëm dy fushat e para janë të detyrueshme, të tjerat kanë vlera të mira paraprake.
- texti detyrueshëm
string
Teksti për t'u shndërruar në zë. 1 deri 20 000 karaktere. 1 karakter = 1 kredi.
- voicei detyrueshëm
string
Slug-u ose ID e zërit, p.sh. vjosa. Lista e plotë në GET /v1/voices.
- dialectopsionalstandardkosovoghegtosk
Dialekti. Parazgjedhja: standard.
- emotionopsionalneutralwarmconfidentenergeticcalmempatheticseriousjoyfulsolemn
Emocioni i të folurit. Parazgjedhja: warm.
- paceopsionalslowmediummedium-fastfast
Shpejtësia. Parazgjedhja: medium.
- energyopsionallowmediumhigh
Energjia. Parazgjedhja: medium.
- purposeopsionaladvertisementaudiobookaccessibilitysocialdocumentarymeditationeducationnewspodcastnarrationcharacter
Qëllimi i audios; rregullon stilin e tregimit. Parazgjedhja: narration.
- personalityopsional
string
Tekst i lirë deri në 240 karaktere, p.sh. «i qetë dhe i ngrohtë».
- formatopsionalmp3wav
Formati i skedarit. Parazgjedhja: mp3.
{
"text": "Mirë se vini në Zani.",
"voice": "arben",
"dialect": "kosovo",
"emotion": "confident",
"purpose": "advertisement",
"format": "mp3"
}Përgjigja
Kthehet JSON, jo skedari i audios. Kur status është completed, audioUrl është lidhja e skedarit.
{
"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,processingosefailed.audioUrlështë e përkohshme (rreth 1 orë). Shkarko skedarin dhe ruaje vetë.creditsChargedështë numri i kredive që u zbritën.
Tekste të gjata
Tekstet deri në 900 karaktere përfundojnë brenda të njëjtës kërkesë. Ato më të gjata përpunohen në sfond: përgjigja ka status: "queued" dhe audioUrl: null.
Pyete çdo 2 deri 3 sekonda derisa status të bëhet completed (ka audioUrl) ose failed (ka error). Ka të njëjtën formë si përgjigja e mësipërme.
Lista e zërave
Kthen zërat publikë dhe zërat e tu të personalizuar. Përdor slug në fushën voice. sampleUrl është një prezantim i gatshëm i zërit.
{
"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"
}
]
}Kredite
Shiko kreditet e mbetura dhe planin, p.sh. para se të nisësh një punë të madhe.
{ "credits": 4820, "plan": "studio", "status": "active" }Kufij dhe çmim
60 / minutë
Kërkesa për çelës, të gjitha pikat së bashku. Kur e kalon merr 429 me Retry-After.
1 karakter = 1 kredi
Zbritet vetëm kur audioja krijohet me sukses. Gjenerimet e dështuara nuk kushtojnë.
20 000 karaktere
Maksimumi për një kërkesë. Për tekste më të gjata, ndaji te fundi i fjalisë.
900 karaktere
Mbi këtë prag gjenerimi bëhet në sfond dhe e merr me GET /v1/generations/{id}.
Gabimet
Çdo gabim kthen JSON me fushën error. Mesazhet janë në shqip, prandaj shfaq mesazhin tënd te përdoruesi.
- 400
Kërkesa nuk është e vlefshme
Trupi mungon ose një fushë është gabim. Përgjigja ka edhe listën issues.
- 401
Çelës i munguar ose i pavlefshëm
Kontrollo kokën Authorization: Bearer zn_live_…, dhe që çelësi nuk është revokuar.
- 402
Kredi të pamjaftueshme
Teksti kërkon më shumë karaktere se kreditet e mbetura. Shto kredi te Planet.
- 404
Nuk u gjet
Zëri ose gjenerimi nuk ekziston, ose nuk është i yti.
- 429
Shumë kërkesa
60 kërkesa në minutë për çelës. Prit sipas kokës Retry-After dhe provo sërish.
- 500
Gjenerimi dështoi
Gabim i përkohshëm. Kredite nuk zbriten. Provo sërish pas pak sekondash.
{ "error": "Nuk ke kredi të mjaftueshme." }Shembuj të plotë
Këto funksione bëjnë gjithçka: dërgojnë kërkesën, presin nëse teksti është i gjatë dhe kthejnë bajtet e audios.
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();
// Tekstet mbi 900 karaktere përpunohen në sfond.
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 ?? "Gjenerimi dështoi");
const audio = await fetch(job.audioUrl); // lidhje e përkohshme, ruaje skedarin
return Buffer.from(await audio.arrayBuffer());
}
