> Section "Endpoints" of the AthanLab Developer API documentation (API version 1.0.0). On the web: https://athanlab.com/docs#reference. Whole documentation as Markdown: https://athanlab.com/docs.md # Endpoints This reference is generated from the API's own OpenAPI document: each endpoint lists its parameters, responses, errors and an example. Every path is relative to the base URL `https://api.athanlab.com/api/v1`, and client generators can read this reference as an [OpenAPI 3.1 document](https://api.athanlab.com/api/v1/openapi.json). The response headers listed below leave out `X-Request-Id` and the `X-RateLimit-*` headers, which are covered in [Request IDs](https://athanlab.com/docs#request-ids) and [Rate limits and quotas](https://athanlab.com/docs#limits). ## Endpoints at a glance | Method | Path | Summary | Scope | | --- | --- | --- | --- | | POST | [/speech](https://athanlab.com/docs#endpoint-post-speech) | Create a speech job | `speech:write` | | GET | [/speech](https://athanlab.com/docs#endpoint-get-speech) | List speech jobs | `speech:read` | | GET | [/speech/{id}](https://athanlab.com/docs#endpoint-get-speech-id) | Get a speech job | `speech:read` | | POST | [/speech/{id}/cancel](https://athanlab.com/docs#endpoint-post-speech-id-cancel) | Cancel a speech job | `speech:write` | | GET | [/speech/{id}/audio](https://athanlab.com/docs#endpoint-get-speech-id-audio) | Download a job's audio | `speech:read` | | POST | [/deliveries](https://athanlab.com/docs#endpoint-post-deliveries) | Send to AthanInbox | `speech:write` | | GET | [/deliveries/{id}](https://athanlab.com/docs#endpoint-get-deliveries-id) | Get a delivery | `speech:read` | | GET | [/voices](https://athanlab.com/docs#endpoint-get-voices) | List voices | `voices:read` | | GET | [/voices/{id}/preview](https://athanlab.com/docs#endpoint-get-voices-id-preview) | Get a voice sample | `voices:read` | | GET | [/usage](https://athanlab.com/docs#endpoint-get-usage) | Get the account balance | `usage:read` | | GET | [/openapi.json](https://athanlab.com/docs#endpoint-get-openapi.json) | This OpenAPI document | No key needed | ## Common errors Every endpoint that takes a key can also return these errors: [401 missing_api_key](https://athanlab.com/docs#errors-missing_api_key) [401 invalid_api_key](https://athanlab.com/docs#errors-invalid_api_key) [403 developer_pending](https://athanlab.com/docs#errors-developer_pending) [403 developer_rejected](https://athanlab.com/docs#errors-developer_rejected) [403 developer_suspended](https://athanlab.com/docs#errors-developer_suspended) [403 developer_not_approved](https://athanlab.com/docs#errors-developer_not_approved) [403 account_suspended](https://athanlab.com/docs#errors-account_suspended) [403 scope_missing](https://athanlab.com/docs#errors-scope_missing) [429 rate_limited](https://athanlab.com/docs#errors-rate_limited) [429 auth_blocked](https://athanlab.com/docs#errors-auth_blocked) [500 internal_error](https://athanlab.com/docs#errors-internal_error) [503 api_disabled](https://athanlab.com/docs#errors-api_disabled) ## Speech Asynchronous speech jobs: create, poll, download, cancel. ### Create a speech job POST `https://api.athanlab.com/api/v1/speech` [Scope speech:write](https://athanlab.com/docs#scopes) [Max plan](https://athanlab.com/docs#access) [Paused in read-only mode](https://athanlab.com/docs#errors-api_read_only) Accepts and charges a job, then returns `202 Accepted` at once — the audio is generated in the background. Poll the job at the `Location` header: wait this response's `Retry-After` (2 seconds) before the first poll, then back off to about 10 seconds between polls until it finishes (a successful poll sends no `Retry-After` of its own), then download its audio (https://athanlab.com/docs#polling). The charge is the length of `text` in UTF-16 code units (https://athanlab.com/docs#size-limits). Send an `Idempotency-Key` header so a retry never charges twice (https://athanlab.com/docs#idempotency). With `dry_run: true` the response is a `200` quote, and nothing is created or charged (https://athanlab.com/docs#dry-run). Writing text that sounds natural — sentence punctuation, Burmese and English (made-up names spelled the way they sound in Burmese), and what each `number_mode` reads: https://athanlab.com/docs#writing-text. See: [Polling](https://athanlab.com/docs#polling) · [Safe retries](https://athanlab.com/docs#idempotency) · [Quote first](https://athanlab.com/docs#dry-run) · [Writing text](https://athanlab.com/docs#writing-text) #### Headers - `Idempotency-Key` (`string`, optional): A unique string per request (a UUID works). A retry with the same key and body within 24 hours returns what the first request created — the original job or delivery — without a new charge; the same key with a different body is rejected with 409 `idempotency_conflict`. A key used on `POST /speech` never conflicts with the same value on `POST /deliveries`. Ignored for dry runs. Constraints: 1–255 characters · pattern `^[\x20-\x7E]{1,255}$` #### Body (JSON; unknown fields are rejected) - `text` (`string`, required): The text to speak, 1–5,000 characters, billed at that same count. Characters are UTF-16 code units (JavaScript `String.length`): a Burmese or Latin character counts 1, an emoji or any other character outside the Basic Multilingual Plane counts 2. `maxLength` counts code points, so a client-side check can pass text the API rejects; `dry_run` returns the exact count. Constraints: 1–5,000 characters - `voice_id` (`string | null`, optional): A voice from `GET /voices`. Defaults to the API default voice (`default_voice_id`). Constraints: 1–128 characters - `output_format` (`"mp3" | "wav"`, optional): The audio format the job prepares. Either format can still be downloaded later. Constraints: default `mp3` - `number_mode` (`"smart" | "place" | "digits" | "english" | "english_digits"`, optional): How digits are read: smart (Burmese digits as words, other digits as written), place, digits, english, english_digits. Constraints: default `smart` - `metadata` (`map`, optional): Up to 16 string key/value pairs stored with the job and returned on every read (keys ≤ 40 characters, values ≤ 500, ≤ 8000 bytes in total). Constraints: keys: 1–40 characters, pattern `^[A-Za-z0-9][A-Za-z0-9_.:-]*$` · values: at most 500 characters - `dry_run` (`boolean`, optional): Validate and quote the request without creating a job or charging anything. #### Returns - 202 Accepted [CreateSpeechResponse](https://athanlab.com/docs#schema-CreateSpeechResponse)\ The job was accepted and charged, or an earlier request with the same Idempotency-Key was replayed.\ Headers: `Location`, `Retry-After`, `Idempotent-Replayed` - 200 OK [DryRunQuote](https://athanlab.com/docs#schema-DryRunQuote)\ A quote for a `dry_run` request. Nothing was created or charged. #### Errors [400 validation_error](https://athanlab.com/docs#errors-validation_error) [402 plan_required](https://athanlab.com/docs#errors-plan_required) [402 insufficient_characters](https://athanlab.com/docs#errors-insufficient_characters) [404 voice_not_found](https://athanlab.com/docs#errors-voice_not_found) [409 idempotency_conflict](https://athanlab.com/docs#errors-idempotency_conflict) [413 payload_too_large](https://athanlab.com/docs#errors-payload_too_large) [415 unsupported_media_type](https://athanlab.com/docs#errors-unsupported_media_type) [422 unspeakable_text](https://athanlab.com/docs#errors-unspeakable_text) [422 text_too_long](https://athanlab.com/docs#errors-text_too_long) [429 concurrency_limit](https://athanlab.com/docs#errors-concurrency_limit) [429 key_budget_exceeded](https://athanlab.com/docs#errors-key_budget_exceeded) [503 api_read_only](https://athanlab.com/docs#errors-api_read_only) [503 capacity_unavailable](https://athanlab.com/docs#errors-capacity_unavailable) [503 server_restarting](https://athanlab.com/docs#errors-server_restarting) Plus the [common errors](https://athanlab.com/docs#common-errors). #### Example Request: ```sh 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": "မင်္ဂလာပါ။ အသံလက်ဘ်မှ ကြိုဆိုပါတယ်။", "voice_id": "athanlab-default-female-v1", "output_format": "mp3", "metadata": { "order_id": "A-1042" } }' ``` 202 Accepted · CreateSpeechResponse: ```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 } } ``` 200 OK · DryRunQuote: ```json { "object": "speech.quote", "characters": 35, "dispatches": 1, "spendable": 1950000, "sufficient": true } ``` ### List speech jobs GET `https://api.athanlab.com/api/v1/speech` [Scope speech:read](https://athanlab.com/docs#scopes) [Works in read-only mode](https://athanlab.com/docs#errors-api_read_only) Jobs created by any of the account's keys, newest first. Page with `cursor` (the previous page's `next_cursor`, unchanged) until `has_more` is false. #### Query parameters - `status` (`"processing" | "succeeded" | "failed" | "cancelled"`, optional): Only jobs in this status. - `limit` (`integer`, optional): Jobs per page, 1–100. Constraints: 1–100 · default `20` - `cursor` (`string`, optional): The `next_cursor` of the previous page. #### Returns - 200 OK [SpeechList](https://athanlab.com/docs#schema-SpeechList)\ A page of jobs. #### Errors [400 validation_error](https://athanlab.com/docs#errors-validation_error) [400 invalid_cursor](https://athanlab.com/docs#errors-invalid_cursor) Plus the [common errors](https://athanlab.com/docs#common-errors). #### Example Request: ```sh curl -sS https://api.athanlab.com/api/v1/speech \ -H "X-API-Key: $ATHANLAB_API_KEY" ``` 200 OK: ```json { "object": "list", "data": [ { "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" } } ], "has_more": false, "next_cursor": null } ``` ### Get a speech job GET `https://api.athanlab.com/api/v1/speech/{id}` [Scope speech:read](https://athanlab.com/docs#scopes) [Works in read-only mode](https://athanlab.com/docs#errors-api_read_only) The job's current state. Poll it while `status` is `processing`, as https://athanlab.com/docs#polling describes. A job of another account returns 404. See: [Polling](https://athanlab.com/docs#polling) #### Path parameters - `id` (`string`, required): The job id from the create response. Constraints: pattern `^[0-9a-f]{32}$` #### Returns - 200 OK [SpeechJob](https://athanlab.com/docs#schema-SpeechJob)\ The job. #### Errors [404 not_found](https://athanlab.com/docs#errors-not_found) Plus the [common errors](https://athanlab.com/docs#common-errors). #### Example Request: ```sh curl -sS https://api.athanlab.com/api/v1/speech/$JOB_ID \ -H "X-API-Key: $ATHANLAB_API_KEY" ``` 200 OK: ```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" } } ``` ### Cancel a speech job POST `https://api.athanlab.com/api/v1/speech/{id}/cancel` [Scope speech:write](https://athanlab.com/docs#scopes) [Works in read-only mode](https://athanlab.com/docs#errors-api_read_only) Stops a job that is still processing. The share already generated stays charged; the rest is refunded (`usage.characters_refunded`). Cancelling a cancelled job returns it again. Works in read-only mode and after your Max plan ends, because it only ever returns characters. No request body. See: [Cancel](https://athanlab.com/docs#cancel) #### Path parameters - `id` (`string`, required): The job id from the create response. Constraints: pattern `^[0-9a-f]{32}$` #### Returns - 200 OK [SpeechJob](https://athanlab.com/docs#schema-SpeechJob)\ The cancelled job. #### Errors [404 not_found](https://athanlab.com/docs#errors-not_found) [409 job_finished](https://athanlab.com/docs#errors-job_finished) [413 payload_too_large](https://athanlab.com/docs#errors-payload_too_large) [415 unsupported_media_type](https://athanlab.com/docs#errors-unsupported_media_type) Plus the [common errors](https://athanlab.com/docs#common-errors). #### Example Request: ```sh curl -sS -X POST https://api.athanlab.com/api/v1/speech/$JOB_ID/cancel \ -H "X-API-Key: $ATHANLAB_API_KEY" ``` 200 OK: ```json { "object": "speech", "id": "8f14e45fceea167a5a36dedd4bea2543", "status": "cancelled", "created_at": "2026-10-01T08:30:00.000Z", "completed_at": "2026-10-01T08:30:03.000Z", "progress": null, "input": { "characters": 35, "voice_id": "athanlab-default-female-v1", "output_format": "mp3", "number_mode": "smart" }, "usage": { "characters_charged": 35, "characters_refunded": 21 }, "audio": null, "error": null, "metadata": { "order_id": "A-1042" } } ``` ### Download a job's audio GET `https://api.athanlab.com/api/v1/speech/{id}/audio` [Scope speech:read](https://athanlab.com/docs#scopes) [Works in read-only mode](https://athanlab.com/docs#errors-api_read_only) Streams the finished audio. Defaults to the job's `output_format`; `?format=` downloads the other one (MP3 is encoded on first request, so the first WAV→MP3 download of a job can take a few seconds). `HEAD` returns the same status and headers as `GET` — `Content-Length` and `ETag` included — without the body, so it also returns 410 once the audio is gone. Until an MP3 has been encoded it has no size or ETag yet, so `HEAD` of a `wav` job's first MP3 omits both. Audio is kept for 30 days (`audio.expires_at`). See: [Retention](https://athanlab.com/docs#retention) #### Path parameters - `id` (`string`, required): The job id from the create response. Constraints: pattern `^[0-9a-f]{32}$` #### Query parameters - `format` (`"mp3" | "wav"`, optional): Download in this format instead of the job's `output_format`. #### Returns - 200 OK audio/mpeg · audio/wav\ The audio file (sent as an attachment named `athanlab-speech-{id}.mp3` or `.wav`).\ Headers: `Content-Disposition`, `Content-Length`, `ETag` #### Errors [400 validation_error](https://athanlab.com/docs#errors-validation_error) [404 not_found](https://athanlab.com/docs#errors-not_found) [409 job_not_finished](https://athanlab.com/docs#errors-job_not_finished) [409 job_failed](https://athanlab.com/docs#errors-job_failed) [410 audio_expired](https://athanlab.com/docs#errors-audio_expired) [503 server_busy](https://athanlab.com/docs#errors-server_busy) Plus the [common errors](https://athanlab.com/docs#common-errors). #### Example 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" ``` ## Deliveries Send audio to AthanInbox, the key owner's inbox on athanlab.com: a titled text or up to 50 chapters, made in the background and kept in AthanInbox until the owner deletes it. ### Send to AthanInbox POST `https://api.athanlab.com/api/v1/deliveries` [Scope speech:write](https://athanlab.com/docs#scopes) [Max plan](https://athanlab.com/docs#access) [Paused in read-only mode](https://athanlab.com/docs#errors-api_read_only) Sends audio to AthanInbox, the inbox on athanlab.com of the account that owns the key — never anyone else's. Accepts and charges the whole delivery, every chapter, then returns `202 Accepted` at once: the chapters are made in the background, one at a time, and the owner follows their progress and listens in AthanInbox at https://athanlab.com/dashboard/inbox. A chapter that cannot be made is refunded to the balance it was charged from (a monthly allowance that has reset since is not carried over; `usage.characters_refunded` shows what came back) and the rest still arrive (`partially_succeeded`). The delivery stays in AthanInbox until the owner deletes it. Polling is optional. To poll, GET the delivery at the `Location` header no more often than this response's `Retry-After` (30 seconds). Send the content as `text` (one chapter) or as `chapters` — up to 50 of at most 5,000 characters each, 100,000 in total — each checked like a speech job's text before anything is charged (an error names the chapter, as in `chapters.3.text`). The charge is the total length in UTF-16 code units. Send an `Idempotency-Key` header so a retry never charges twice (https://athanlab.com/docs#idempotency). With `dry_run: true` the response is a `200` quote, and nothing is created or charged. Guide: https://athanlab.com/docs#athaninbox. See: [AthanInbox](https://athanlab.com/docs#athaninbox) · [Safe retries](https://athanlab.com/docs#idempotency) · [Writing text](https://athanlab.com/docs#writing-text) #### Headers - `Idempotency-Key` (`string`, optional): A unique string per request (a UUID works). A retry with the same key and body within 24 hours returns what the first request created — the original job or delivery — without a new charge; the same key with a different body is rejected with 409 `idempotency_conflict`. A key used on `POST /speech` never conflicts with the same value on `POST /deliveries`. Ignored for dry runs. Constraints: 1–255 characters · pattern `^[\x20-\x7E]{1,255}$` #### Body (JSON; unknown fields are rejected) - `title` (`string`, required): What the listener sees in AthanInbox, 1–120 characters. Leading and trailing spaces are removed; line breaks and other control characters are refused. Constraints: 1–120 characters - `text` (`string`, optional): The text of a one-chapter delivery, 1–5,000 characters. Send exactly one of `text` and `chapters`. Constraints: 1–5,000 characters - `chapters` (`object[]`, optional): 1–50 chapters, played in this order, totalling at most 100,000 characters. Send exactly one of `text` and `chapters`. - `voice_id` (`string | null`, optional): A voice from `GET /voices`, used for every chapter. Defaults to the API default voice (`default_voice_id`). Constraints: 1–128 characters - `number_mode` (`"smart" | "place" | "digits" | "english" | "english_digits"`, optional): How digits are read in every chapter: smart (Burmese digits as words, other digits as written), place, digits, english, english_digits. Constraints: default `smart` - `kind` (`"article" | "book" | "lesson" | "briefing" | "other"`, optional): What the delivery is: article, book, lesson, briefing, other. AthanInbox files and labels it by kind. Constraints: default `other` - `sources` (`object[]`, optional): Up to 20 links the listener can open from the delivery (≤ 16,000 bytes in total). AthanLab never fetches them. - `metadata` (`map`, optional): Up to 16 string key/value pairs stored with the delivery and returned on every read (keys ≤ 40 characters, values ≤ 500, ≤ 8000 bytes in total). Constraints: keys: 1–40 characters, pattern `^[A-Za-z0-9][A-Za-z0-9_.:-]*$` · values: at most 500 characters - `dry_run` (`boolean`, optional): Validate and quote the delivery without creating it or charging anything. #### Returns - 202 Accepted [CreateDeliveryResponse](https://athanlab.com/docs#schema-CreateDeliveryResponse)\ The delivery was accepted and charged, or an earlier request with the same Idempotency-Key was replayed.\ Headers: `Location`, `Retry-After`, `Idempotent-Replayed` - 200 OK [DeliveryQuote](https://athanlab.com/docs#schema-DeliveryQuote)\ A quote for a `dry_run` request. Nothing was created or charged. #### Errors [400 validation_error](https://athanlab.com/docs#errors-validation_error) [402 plan_required](https://athanlab.com/docs#errors-plan_required) [402 insufficient_characters](https://athanlab.com/docs#errors-insufficient_characters) [404 not_found](https://athanlab.com/docs#errors-not_found) [404 voice_not_found](https://athanlab.com/docs#errors-voice_not_found) [409 idempotency_conflict](https://athanlab.com/docs#errors-idempotency_conflict) [413 payload_too_large](https://athanlab.com/docs#errors-payload_too_large) [415 unsupported_media_type](https://athanlab.com/docs#errors-unsupported_media_type) [422 unspeakable_text](https://athanlab.com/docs#errors-unspeakable_text) [422 text_too_long](https://athanlab.com/docs#errors-text_too_long) [429 concurrency_limit](https://athanlab.com/docs#errors-concurrency_limit) [429 key_budget_exceeded](https://athanlab.com/docs#errors-key_budget_exceeded) [503 api_read_only](https://athanlab.com/docs#errors-api_read_only) [503 server_restarting](https://athanlab.com/docs#errors-server_restarting) Plus the [common errors](https://athanlab.com/docs#common-errors). #### Example Request: ```sh curl -sS https://api.athanlab.com/api/v1/deliveries \ -H "X-API-Key: $ATHANLAB_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(python3 -c 'import uuid; print(uuid.uuid4())')" \ -d '{ "title": "ယနေ့ သတင်းအကျဉ်း", "kind": "briefing", "chapters": [ { "title": "ရာသီဥတု", "text": "ရန်ကုန်မြို့တွင် မိုးအနည်းငယ် ရွာနိုင်ပါသည်။ ထီးဆောင်ထားပါ။" }, { "title": "ဈေးကွက်", "text": "ဈေးကွက်များ တည်ငြိမ်နေပါသည်။ အသေးစိတ်ကို မနက်ဖြန် ပြောပြပါမယ်။" } ], "sources": [ { "title": "မိုးလေဝသ ခန့်မှန်းချက်", "url": "https://example.com/weather" } ], "voice_id": "athanlab-default-female-v1" }' ``` 202 Accepted · CreateDeliveryResponse: ```json { "object": "delivery", "id": "1679091c5a880faf6fb5e6087eb1b2dc", "status": "queued", "title": "ယနေ့ သတင်းအကျဉ်း", "kind": "briefing", "created_at": "2026-10-06T00:30:00.000Z", "completed_at": null, "progress": 0, "input": { "characters": 121, "chapters": 2, "voice_id": "athanlab-default-female-v1", "number_mode": "smart" }, "chapters": [ { "index": 0, "title": "ရာသီဥတု", "characters": 59, "status": "queued", "duration_seconds": null, "error": null }, { "index": 1, "title": "ဈေးကွက်", "characters": 62, "status": "queued", "duration_seconds": null, "error": null } ], "sources": [ { "title": "မိုးလေဝသ ခန့်မှန်းချက်", "url": "https://example.com/weather" } ], "usage": { "characters_charged": 121, "characters_refunded": 0 }, "app_url": "https://athanlab.com/app/inbox/1679091c5a880faf6fb5e6087eb1b2dc", "notified": false, "metadata": {}, "balance": { "spendable_chars": 1949879 } } ``` 200 OK · DeliveryQuote: ```json { "object": "delivery.quote", "characters": 121, "chapters": 2, "dispatches": 2, "spendable": 1950000, "sufficient": true } ``` ### Get a delivery GET `https://api.athanlab.com/api/v1/deliveries/{id}` [Scope speech:read](https://athanlab.com/docs#scopes) [Works in read-only mode](https://athanlab.com/docs#errors-api_read_only) The delivery's current state, its chapters and what it cost. The owner follows its progress in AthanInbox, so polling is optional; to poll, wait at least 30 seconds between requests. After the owner deletes it in AthanInbox it reads as `deleted`, keeping only its charge and refunds. A delivery of another account returns 404. See: [AthanInbox](https://athanlab.com/docs#athaninbox) #### Path parameters - `id` (`string`, required): The delivery id from the create response. Constraints: pattern `^[0-9a-f]{32}$` #### Returns - 200 OK [Delivery](https://athanlab.com/docs#schema-Delivery)\ The delivery. #### Errors [404 not_found](https://athanlab.com/docs#errors-not_found) Plus the [common errors](https://athanlab.com/docs#common-errors). #### Example Request: ```sh curl -sS https://api.athanlab.com/api/v1/deliveries/$DELIVERY_ID \ -H "X-API-Key: $ATHANLAB_API_KEY" ``` 200 OK: ```json { "object": "delivery", "id": "1679091c5a880faf6fb5e6087eb1b2dc", "status": "succeeded", "title": "ယနေ့ သတင်းအကျဉ်း", "kind": "briefing", "created_at": "2026-10-06T00:30:00.000Z", "completed_at": "2026-10-06T00:31:24.000Z", "progress": 1, "input": { "characters": 121, "chapters": 2, "voice_id": "athanlab-default-female-v1", "number_mode": "smart" }, "chapters": [ { "index": 0, "title": "ရာသီဥတု", "characters": 59, "status": "succeeded", "duration_seconds": 6.48, "error": null }, { "index": 1, "title": "ဈေးကွက်", "characters": 62, "status": "succeeded", "duration_seconds": 6.91, "error": null } ], "sources": [ { "title": "မိုးလေဝသ ခန့်မှန်းချက်", "url": "https://example.com/weather" } ], "usage": { "characters_charged": 121, "characters_refunded": 0 }, "app_url": "https://athanlab.com/app/inbox/1679091c5a880faf6fb5e6087eb1b2dc", "notified": true, "metadata": {} } ``` ## Voices The voices a key can speak with. ### List voices GET `https://api.athanlab.com/api/v1/voices` [Scope voices:read](https://athanlab.com/docs#scopes) [Works in read-only mode](https://athanlab.com/docs#errors-api_read_only) AthanLab's official voices, then the voices in your own library. `default_voice_id` is the voice a job uses when it names none. Voices are added to your library in the AthanLab app, under the same consent rules as every other voice. See: [Voices](https://athanlab.com/docs#voices) #### Returns - 200 OK [VoiceList](https://athanlab.com/docs#schema-VoiceList)\ The voices this account can use. #### Example Request: ```sh curl -sS https://api.athanlab.com/api/v1/voices \ -H "X-API-Key: $ATHANLAB_API_KEY" ``` 200 OK: ```json { "data": [ { "id": "athanlab-default-female-v1", "name": "AthanLab Female", "category": "default", "source": "athanlab", "is_default": true }, { "id": "athanlab-default-male-v1", "name": "AthanLab Male", "category": "default", "source": "athanlab", "is_default": false }, { "id": "3c59dc048e8850243be8079a5c74d079", "name": "My narrator", "category": "custom", "source": "user", "is_default": false } ], "default_voice_id": "athanlab-default-female-v1" } ``` ### Get a voice sample GET `https://api.athanlab.com/api/v1/voices/{id}/preview` [Scope voices:read](https://athanlab.com/docs#scopes) [Works in read-only mode](https://athanlab.com/docs#errors-api_read_only) A short-lived signed URL of the voice's reference sample, to let people hear a voice before choosing it. See: [Voices](https://athanlab.com/docs#voices) #### Path parameters - `id` (`string`, required): A voice id from `GET /voices`. Constraints: 1–128 characters #### Returns - 200 OK [VoicePreview](https://athanlab.com/docs#schema-VoicePreview)\ The sample URL. #### Errors [404 voice_not_found](https://athanlab.com/docs#errors-voice_not_found) Plus the [common errors](https://athanlab.com/docs#common-errors). #### Example Request: ```sh curl -sS https://api.athanlab.com/api/v1/voices/$VOICE_ID/preview \ -H "X-API-Key: $ATHANLAB_API_KEY" ``` 200 OK: ```json { "id": "athanlab-default-female-v1", "name": "AthanLab Female", "url": "https://storage.example/athanlab-default-female-v1.wav?X-Amz-Signature=…", "expires_in": 3600 } ``` ## Account The balance your keys draw on. ### Get the account balance GET `https://api.athanlab.com/api/v1/usage` [Scope usage:read](https://athanlab.com/docs#scopes) [Works in read-only mode](https://athanlab.com/docs#errors-api_read_only) The balance your keys draw on — the same one the AthanLab app uses: the Max plan's monthly allowance first, then Athan Tokens. `entitled` says whether keys may create jobs right now. Also the two limits a create is checked against besides the balance: the account's jobs processing (`active_jobs` of `max_concurrent_jobs`) and the calling key's monthly budget (`key`). Works after your Max plan ends. See: [Billing](https://athanlab.com/docs#billing) #### Returns - 200 OK [Usage](https://athanlab.com/docs#schema-Usage)\ The balance. #### Example Request: ```sh curl -sS https://api.athanlab.com/api/v1/usage \ -H "X-API-Key: $ATHANLAB_API_KEY" ``` 200 OK: ```json { "plan": "max", "entitled": true, "upgrade_url": null, "monthly": { "limit": 2000000, "used": 50000, "remaining": 1950000, "resets_at": "2026-10-21T00:00:00.000Z" }, "tokens": { "balance": 0 }, "spendable": 1950000, "active_jobs": 1, "max_concurrent_jobs": 2, "key": { "id": "c4ca4238a0b923820dcc509a6f75849b", "monthly_char_budget": 500000, "used_this_month": 120000, "remaining": 380000, "resets_at": "2026-11-01T00:00:00.000Z" } } ``` ## Meta This document. ### This OpenAPI document GET `https://api.athanlab.com/api/v1/openapi.json` No key needed The OpenAPI 3.1 description of this API. Public: no key needed, and served even while the API is switched off. #### Returns - 200 OK application/json\ The document.\ Headers: `ETag` - 304 Not Modified\ Not modified: the If-None-Match header matched the current ETag.\ Headers: `ETag` #### Errors [429 rate_limited](https://athanlab.com/docs#errors-rate_limited) [500 internal_error](https://athanlab.com/docs#errors-internal_error) #### Example Request: ```sh curl -sS https://api.athanlab.com/api/v1/openapi.json ``` ## Objects The objects the endpoints return. Ignore fields your code does not know, as [Versioning](https://athanlab.com/docs#versioning) explains. ### SpeechJob - `object` (`"speech"`): Always `speech`. - `id` (`string`): The job id: 32 lowercase hex characters. - `status` (`"processing" | "succeeded" | "failed" | "cancelled"`): `processing` until the job finishes: `succeeded`, `failed` or `cancelled`. - `created_at` (`string (ISO 8601)`): When the job was accepted and charged. - `completed_at` (`string (ISO 8601) | null`): When the job finished; null while it is processing. - `progress` (`number | null`): Share of the work done: 0 to 0.99 while processing, 1 once succeeded, null once failed or cancelled. Constraints: 0–1 - `input` (`object`): What was requested. The input text itself is never returned. - `input.characters` (`integer`): Characters billed: the length of `text` in UTF-16 code units (an emoji counts 2). Constraints: greater than 0 - `input.voice_id` (`string | null`): The voice the job speaks with (the API default voice when the request named none). - `input.output_format` (`"mp3" | "wav"`): The format the job prepared. Either format can be downloaded. - `input.number_mode` (`"smart" | "place" | "digits" | "english" | "english_digits"`): How digits were read. - `usage` (`object`): What the job cost. - `usage.characters_charged` (`integer`): Characters charged when the job was accepted. Constraints: at least 0 - `usage.characters_refunded` (`integer`): Characters returned since: all of them when the job failed, the unprocessed share when it was cancelled. Constraints: at least 0 - `audio` ([SpeechAudio](https://athanlab.com/docs#schema-SpeechAudio) | null): Present while the job has downloadable audio: succeeded and inside the retention period. Null otherwise. - `error` ([SpeechJobError](https://athanlab.com/docs#schema-SpeechJobError) | null): Why the job failed; null unless status is `failed`. - `metadata` (`map`): The metadata sent with the request, unchanged. ### SpeechJobError - `code` (`"input_rejected" | "capacity_unavailable" | "generation_failed" | "interrupted"`): Machine-readable reason, explained under "Job failure codes" at https://athanlab.com/docs#jobs. Not an HTTP error code: do not look it up as `#errors-` — `capacity_unavailable` is also an HTTP 503, which charged nothing and carries Retry-After, while a failed job was charged, then refunded. - `message` (`string`): A human-readable explanation. Do not parse it. - `retryable` (`boolean`): True when submitting the same request again may succeed. ### SpeechAudio - `format` (`"mp3" | "wav"`): The job's output format. GET the audio URL with `?format=` for the other one. - `duration_seconds` (`number`): Length of the audio in seconds. Constraints: at least 0 - `url` (`string`): Download path, relative to https://api.athanlab.com. Send your API key with the request. - `expires_at` (`string (ISO 8601)`): When the audio (and the job's input text) is deleted. Downloads return 410 `audio_expired` after this. ### CreateSpeechResponse Every field of [SpeechJob](https://athanlab.com/docs#schema-SpeechJob), plus: - `balance` (`object`): What the account can still spend after this job's charge. - `balance.spendable_chars` (`integer`): Monthly allowance remaining plus Athan Tokens. Constraints: at least 0 ### SpeechList - `object` (`"list"`): Always `list`. - `data` ([SpeechJob\[\]](https://athanlab.com/docs#schema-SpeechJob)): Jobs, newest first. - `has_more` (`boolean`): True when older jobs exist beyond this page. - `next_cursor` (`string | null`): Pass as `cursor` to fetch the next (older) page; null on the last page. ### DryRunQuote - `object` (`"speech.quote"`): Always `speech.quote`. - `characters` (`integer`): What the job would be charged: the length of `text` in UTF-16 code units (an emoji counts 2). Constraints: greater than 0 - `dispatches` (`integer`): How many pieces the text is split into for synthesis. Constraints: greater than 0 - `spendable` (`integer`): Monthly allowance remaining plus Athan Tokens, right now. Constraints: at least 0 - `sufficient` (`boolean`): True when `spendable` covers `characters`. ### Delivery - `object` (`"delivery"`): Always `delivery`. - `id` (`string`): The delivery id: 32 lowercase hex characters. - `status` (`"queued" | "processing" | "succeeded" | "partially_succeeded" | "failed" | "deleted"`): `queued` until its first chapter starts, `processing` while chapters are made, then `succeeded` (every chapter), `partially_succeeded` (some chapters failed and were refunded) or `failed` (none could be made; all refunded). `deleted` once the owner deleted it in AthanInbox. - `title` (`string | null`): The title the owner sees in AthanInbox; null once the delivery is deleted. - `kind` (`"article" | "book" | "lesson" | "briefing" | "other"`): What the delivery is, as sent. - `created_at` (`string (ISO 8601)`): When the delivery was accepted and charged. - `completed_at` (`string (ISO 8601) | null`): When its last chapter finished; null while queued or processing. - `progress` (`number | null`): Share of the delivery made, by characters: 0 to 0.99 while queued or processing, 1 once it succeeded or partially succeeded, null once it failed or was deleted. Constraints: 0–1 - `input` (`object`): What was sent. The text itself is never returned. - `input.characters` (`integer`): Characters charged: the total length of the chapters in UTF-16 code units (an emoji counts 2). Constraints: greater than 0 - `input.chapters` (`integer`): How many chapters were sent. Constraints: greater than 0 - `input.voice_id` (`string | null`): The voice every chapter speaks with (the API default voice when the request named none). - `input.number_mode` (`"smart" | "place" | "digits" | "english" | "english_digits"`): How digits are read. - `chapters` ([DeliveryChapter\[\]](https://athanlab.com/docs#schema-DeliveryChapter)): The chapters, in play order; empty once the delivery is deleted. - `sources` ([DeliverySource\[\]](https://athanlab.com/docs#schema-DeliverySource)): The links sent with the delivery, as AthanInbox shows them; empty once it is deleted. - `usage` (`object`): What the delivery cost. - `usage.characters_charged` (`integer`): Characters charged when the delivery was accepted: every chapter, up front. Constraints: at least 0 - `usage.characters_refunded` (`integer`): Characters returned to the balance since: each failed chapter in full and, when the owner deleted the delivery while it was being made, every chapter not yet made plus the unmade share of the one in progress. A share charged to a monthly allowance that has reset since is not returned (the reset gave a fresh allowance). Constraints: at least 0 - `app_url` (`string | null`): The delivery in AthanInbox on athanlab.com; it opens only for the signed-in owner. Open it on a computer for now: the AthanLab mobile app gets AthanInbox in a later update. Null once deleted. - `notified` (`boolean`): True once AthanLab has recorded its notice to the owner that the delivery finished. It is not a read receipt. - `metadata` (`map`): The metadata sent with the request, unchanged; empty once the delivery is deleted. ### DeliveryChapter - `index` (`integer`): The chapter's position, from 0, in the order sent. Constraints: at least 0 - `title` (`string | null`): The chapter's title as sent; null when none was sent. - `characters` (`integer`): Characters this chapter was charged. Constraints: greater than 0 - `status` (`"queued" | "processing" | "succeeded" | "failed" | "cancelled"`): `queued`, then `processing` while it is made, then `succeeded` or `failed` (refunded). `cancelled` when AthanLab stopped the chapter while it was being made; the share not made is refunded. - `duration_seconds` (`number | null`): Length of the chapter's audio in seconds; null until it succeeded. Constraints: at least 0 - `error` ([DeliveryChapterError](https://athanlab.com/docs#schema-DeliveryChapterError) | null): Why the chapter failed; null unless status is `failed`. ### DeliveryChapterError - `code` (`"input_rejected" | "capacity_unavailable" | "generation_failed" | "interrupted" | "voice_unavailable" | "expired"`): Machine-readable reason: the job failure codes explained at https://athanlab.com/docs#jobs, plus `voice_unavailable` (the voice was removed before the chapter ran) and `expired` (it waited past the queue limit). Not an HTTP error code. - `message` (`string`): A human-readable explanation. Do not parse it. - `retryable` (`boolean`): True when sending the chapter again may succeed. ### DeliverySource - `title` (`string | null`): What the link is; null when none was sent. - `url` (`string`): The https link, normalised (punycode host, percent-encoded path). AthanLab never fetches it. ### CreateDeliveryResponse Every field of [Delivery](https://athanlab.com/docs#schema-Delivery), plus: - `balance` (`object`): What the account can still spend after this delivery's charge. - `balance.spendable_chars` (`integer`): Monthly allowance remaining plus Athan Tokens. Constraints: at least 0 ### DeliveryQuote - `object` (`"delivery.quote"`): Always `delivery.quote`. - `characters` (`integer`): What the delivery would be charged: the total length of the chapters in UTF-16 code units (an emoji counts 2). Constraints: greater than 0 - `chapters` (`integer`): How many chapters the delivery has. Constraints: greater than 0 - `dispatches` (`integer`): How many pieces the chapters are split into for synthesis, in total. Constraints: greater than 0 - `spendable` (`integer`): Monthly allowance remaining plus Athan Tokens, right now. Constraints: at least 0 - `sufficient` (`boolean`): True when `spendable` covers `characters`. ### Voice - `id` (`string`): Pass as `voice_id` when creating a job. - `name` (`string`): Display name. - `category` (`string`): Voice category. - `source` (`"athanlab" | "user"`): `athanlab` for official voices, `user` for voices in your own library. - `is_default` (`boolean`): True for the voice a job uses when it names none. ### VoiceList - `data` ([Voice\[\]](https://athanlab.com/docs#schema-Voice)): Official voices first, then your own. - `default_voice_id` (`string | null`): The voice a job uses when it names none; null when that voice is temporarily unavailable (send `voice_id`). ### VoicePreview - `id` (`string`): The voice id. - `name` (`string`): Display name. - `url` (`string`): A signed URL of the voice sample. No API key needed; it expires. - `expires_in` (`integer`): Seconds until `url` stops working. Constraints: greater than 0 ### Usage - `plan` (`string`): The account's plan. - `entitled` (`boolean`): True when keys may create jobs (the account is on Max). False means creating a job returns 402 `plan_required`. - `upgrade_url` (`string | null`): Where to subscribe to Max; null while entitled. - `monthly` (`object`): The plan's monthly character allowance. - `monthly.limit` (`integer`): Characters per 30-day period. Constraints: at least 0 - `monthly.used` (`integer`): Characters used this period. Constraints: at least 0 - `monthly.remaining` (`integer`): Characters left this period. Constraints: at least 0 - `monthly.resets_at` (`string (ISO 8601) | null`): When the period resets; null without a monthly allowance. - `tokens` (`object`): Athan Tokens: purchased characters, spent after the monthly allowance. - `tokens.balance` (`integer`): Athan Tokens held. Constraints: at least 0 - `spendable` (`integer`): monthly.remaining + tokens.balance. Constraints: at least 0 - `active_jobs` (`integer`): The account's jobs processing right now, across all its keys. Constraints: at least 0 - `max_concurrent_jobs` (`integer`): How many jobs the account may have processing at once. A create beyond it returns 429 `concurrency_limit`. Constraints: greater than 0 - `key` (`object`): The monthly character budget of the key making this request. - `key.id` (`string`): The id of the key that made this request. A rotation keeps it, so the budget carries over. - `key.monthly_char_budget` (`integer | null`): The most this key may spend in a UTC calendar month, set by the account owner; null when the key has no budget. Constraints: greater than 0 - `key.used_this_month` (`integer | null`): Characters this key has spent since the month began, net of refunds; null without a budget. Constraints: at least 0 - `key.remaining` (`integer | null`): Characters left in the budget; null without a budget. A job needs both this and `spendable`: beyond it a create returns 429 `key_budget_exceeded`. Constraints: at least 0 - `key.resets_at` (`string (ISO 8601) | null`): When the budget starts over (the start of the next UTC calendar month); null without a budget.