> Section "Quickstart" of the AthanLab Developer API documentation (API version 1.0.0). On the web: https://athanlab.com/docs#quickstart. Whole documentation as Markdown: https://athanlab.com/docs.md # Quickstart Three requests: create a job, poll it, download the audio. > [!NOTE] > **Before you start** > > You need an API key with the `speech:write` and `speech:read` scopes (see [Get access](https://athanlab.com/docs#access)). Set it as `ATHANLAB_API_KEY` in the environment of a server-side process. **1. Create a job** Request: ```sh curl -sS https://api.athanlab.com/api/v1/speech \ -H "X-API-Key: $ATHANLAB_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "မင်္ဂလာပါ။ အသံလက်ဘ်မှ ကြိုဆိုပါတယ်။", "output_format": "mp3", "metadata": {"order_id": "A-1042"}}' ``` 202 Accepted (example): ```json { "object": "speech", "id": "8f14e45fceea167a5a36dedd4bea2543", "status": "processing", "created_at": "2026-10-01T08:30:00.000Z", "completed_at": null, "progress": 0, "input": { "characters": 35, "voice_id": "athanlab-default-female-v1", "output_format": "mp3", "number_mode": "smart" }, "usage": { "characters_charged": 35, "characters_refunded": 0 }, "audio": null, "error": null, "metadata": { "order_id": "A-1042" }, "balance": { "spendable_chars": 1949965 } } ``` The response also carries `Location`, the path of the job, and `Retry-After: 2`. In production, send an `Idempotency-Key` with every create so a retry never charges twice (see [Safe retries](https://athanlab.com/docs#idempotency)). **2. Poll the job** Put the `id` from the response in `JOB_ID`, then fetch the job. Request: ```sh curl -sS https://api.athanlab.com/api/v1/speech/$JOB_ID \ -H "X-API-Key: $ATHANLAB_API_KEY" ``` 200 OK (example): ```json { "object": "speech", "id": "8f14e45fceea167a5a36dedd4bea2543", "status": "succeeded", "created_at": "2026-10-01T08:30:00.000Z", "completed_at": "2026-10-01T08:30:07.412Z", "progress": 1, "input": { "characters": 35, "voice_id": "athanlab-default-female-v1", "output_format": "mp3", "number_mode": "smart" }, "usage": { "characters_charged": 35, "characters_refunded": 0 }, "audio": { "format": "mp3", "duration_seconds": 3.84, "url": "/api/v1/speech/8f14e45fceea167a5a36dedd4bea2543/audio", "expires_at": "2026-10-31T08:30:00.000Z" }, "error": null, "metadata": { "order_id": "A-1042" } } ``` Repeat until `status` is no longer `processing`, at the pace described in [Polling](https://athanlab.com/docs#polling). **3. Download the audio** Request: ```sh curl -sS --fail -o speech.mp3 \ https://api.athanlab.com/api/v1/speech/$JOB_ID/audio \ -H "X-API-Key: $ATHANLAB_API_KEY" ``` With `--fail`, curl saves nothing when the request fails, so an error body never ends up in `speech.mp3`. ## Complete script The same three steps as one program, with an Idempotency-Key and back-off between polls: **curl** (terminal) ```sh # Set ATHANLAB_API_KEY to your key from the Developer API page first. Keep it server-side. # 1. Create a job. The response is 202 Accepted with the job. # Keep its id for the next steps. If the API rejects the request, the error is printed instead. JOB_ID=$(curl -sS https://api.athanlab.com/api/v1/speech \ -H "X-API-Key: $ATHANLAB_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(python3 -c 'import uuid; print(uuid.uuid4())')" \ -d '{"text": "မင်္ဂလာပါ။ အသံလက်ဘ်မှ ကြိုဆိုပါတယ်။", "output_format": "mp3"}' \ | python3 -c 'import json, sys; job = json.load(sys.stdin); print(job.get("id") or sys.exit(json.dumps(job)))') echo "$JOB_ID" # 2. Poll until the job is no longer processing. # Wait 2 seconds first, the Retry-After of the create response, then about 10 seconds between polls. sleep 2 while :; do JOB=$(curl -sS https://api.athanlab.com/api/v1/speech/$JOB_ID -H "X-API-Key: $ATHANLAB_API_KEY") STATUS=$(printf '%s' "$JOB" | python3 -c 'import json, sys; print(json.load(sys.stdin).get("status", "error"))') echo "$STATUS" [ "$STATUS" = "processing" ] || break sleep 10 done echo "$JOB" # 3. Download the audio when the job succeeded. # With --fail, curl writes no file on an HTTP error, since an error body is not audio. [ "$STATUS" = "succeeded" ] && curl -sS --fail -o speech.mp3 https://api.athanlab.com/api/v1/speech/$JOB_ID/audio \ -H "X-API-Key: $ATHANLAB_API_KEY" ``` **Python** (quickstart.py) ```python import os, time, uuid import requests # pip install requests BASE = "https://api.athanlab.com/api/v1" HEADERS = {"X-API-Key": os.environ["ATHANLAB_API_KEY"]} # 1. Create the job. The Idempotency-Key makes retrying this POST safe: # a repeat returns the same job instead of charging twice. res = requests.post( f"{BASE}/speech", headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())}, json={"text": "မင်္ဂလာပါ။ အသံလက်ဘ်မှ ကြိုဆိုပါတယ်။", "output_format": "mp3"}, timeout=30, ) res.raise_for_status() job = res.json() # 2. Poll until the job finishes: first after the create response's Retry-After, # then backing off to 10 s (a successful poll sends no Retry-After). delay = float(res.headers.get("Retry-After", 2)) while job["status"] == "processing": time.sleep(delay) res = requests.get(f"{BASE}/speech/{job['id']}", headers=HEADERS, timeout=30) if res.status_code == 429: delay = float(res.headers.get("Retry-After", delay)) continue res.raise_for_status() job = res.json() delay = min(delay * 1.5, 10) if job["status"] != "succeeded": raise RuntimeError(f"job {job['status']}: {job['error']}") # 3. Download the audio. audio = requests.get(f"{BASE}/speech/{job['id']}/audio", headers=HEADERS, timeout=120) audio.raise_for_status() with open("speech.mp3", "wb") as f: f.write(audio.content) ``` **Node.js** (quickstart.mjs) ```js // Node.js 18+ — save as quickstart.mjs and run: node quickstart.mjs import { randomUUID } from 'node:crypto' import { writeFile } from 'node:fs/promises' const BASE = 'https://api.athanlab.com/api/v1' // An unset key would go out as the header "undefined": rejected, and counted // toward the per-network limit on failed key checks. const apiKey = process.env.ATHANLAB_API_KEY if (!apiKey) throw new Error('Set ATHANLAB_API_KEY to your API key first.') const headers = { 'X-API-Key': apiKey } const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000)) async function check(res) { if (!res.ok) throw new Error(JSON.stringify(await res.json())) return res } // 1. Create the job. The Idempotency-Key makes retrying this POST safe. let res = await check(await fetch(`${BASE}/speech`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json', 'Idempotency-Key': randomUUID() }, body: JSON.stringify({ text: 'မင်္ဂလာပါ။ အသံလက်ဘ်မှ ကြိုဆိုပါတယ်။', output_format: 'mp3' }), })) let job = await res.json() // 2. Poll until the job finishes: first after the create response's Retry-After, // then backing off to 10 s (a successful poll sends no Retry-After). let delay = Number(res.headers.get('Retry-After') ?? 2) while (job.status === 'processing') { await sleep(delay) res = await fetch(`${BASE}/speech/${job.id}`, { headers }) if (res.status === 429) { delay = Number(res.headers.get('Retry-After') ?? delay) continue } job = await (await check(res)).json() delay = Math.min(delay * 1.5, 10) } if (job.status !== 'succeeded') throw new Error(`job ${job.status}: ${job.error?.code}`) // 3. Download the audio. res = await check(await fetch(`${BASE}/speech/${job.id}/audio`, { headers })) await writeFile('speech.mp3', Buffer.from(await res.arrayBuffer())) ``` Sending your own text? Read [Writing text for speech](https://athanlab.com/docs#writing-text) first: sentence punctuation, made-up names, and how numbers are read. ## Next steps - [Writing text for speech](https://athanlab.com/docs#writing-text) - [Job lifecycle](https://athanlab.com/docs#jobs) - [Voices](https://athanlab.com/docs#voices) - [Errors](https://athanlab.com/docs#errors) - [AI agents (MCP)](https://athanlab.com/docs#agents)