A media job runs one speech or transcription request with a model's own parameters and keeps the result.
POST /v1/audio/speech and POST /v1/audio/transcriptions create media jobs too. POST /v1/media/jobs needs the
media:generate scope; the GET routes need media:read.
Errors on these routes have the shape {"ok": false, "error": "bad-param", "code": "bad-param", "message": "...", "param": "text"}. Branch on code. Key, scope and rate-limit errors:
Errors.
The job object
| Field | Type | Value |
|---|---|---|
id | string | mj_ and 22 random characters |
kind | string | speech or transcription |
model | string | The model id, such as kokoro |
label | string | The caller's label; openai/speech and openai/transcription for jobs of the audio routes |
state | string | queued, waiting_device, running, done, failed, cancelled or expired |
waiting | object or null | With waiting_device: reason (offline or busy), note and since |
position | integer or null | Place in the queue while queued, from 1 |
priority | string | interactive or batch |
params | object | The parameters as the job runs them, defaults filled in |
results | array | The output files once done |
error | object or null | code, message and retryable of a job that did not finish |
created_at, started_at, finished_at | string or null | RFC 3339 times |
gen_s | number or null | Seconds the model took |
queue_timeout_s | integer or null | Seconds the job may wait in the queue; null: as long as it takes |
waiting_device is a queued job whose model cannot start now; the job runs when the model is back. A result entry
has index, url, content_type and bytes; a speech result adds duration_s and sample_rate, and a
transcription result adds the whole transcript in text and duration_s. url fetches the file without a key.
Fields not listed above are for image and video jobs.
Waiting and queue timeouts
A job waits in the queue up to its queue_timeout_s. A job still queued at its timeout ends expired with
error.code queue-timeout. Only time in the queue counts: a running job never expires.
| Kind | Queue timeout |
|---|---|
speech | 300 s |
transcription | 60 s |
The values above are the defaults. queue_timeout_s in the job body, or X-Queue-Timeout on the audio routes, asks
for a shorter wait; a longer value is lowered to the kind's queue timeout. The job object shows the value in effect.
?wait=<s> on POST /v1/media/jobs and GET /v1/media/jobs/{id} holds the request up to s seconds, 90 at most,
and at most 85 s from the request's arrival. The answer is 200 once the job is done, failed, cancelled or
expired, else 202 with the job as it is. An account holds 16 waiting requests at most; past that, ?wait
answers 202 at once.
Create a job
POST /v1/media/jobs queues a speech or transcription job.
Parameters
A JSON body. A top-level field outside the list fails with 400 bad-request.
| Name | Type | Description |
|---|---|---|
model required | string | A model id from GET /v1/media/models. Optional for a speech job whose params.voice is a saved voice vo_…: the voice's default_model runs |
params | object | The model's parameters (tables below); a parameter outside the model's schema fails with 400 bad-param |
kind | string | speech or transcription; must match the model's kind |
priority | string | interactive (default for speech and transcription) or batch |
label | string | Free text on the job, 200 characters at most |
idempotency_key | string | 1 to 200 printable ASCII characters; the Idempotency-Key header does the same. A second request with the key returns the first job for 7 days |
queue_timeout_s | integer | Seconds the job may wait in the queue, 1 or more |
wait | number | Query parameter: seconds to hold the request, 0 to 90 |
params of a speech job | Type | Description |
|---|---|---|
text required | string | The text to speak; 5,000 characters at most, 20,000 on kokoro |
voice | string | A built-in voice, or a voice reference vo_…, up_…, job:<id>/<i> or https://…; required on cloning models other than omnivoice |
reference_text | string | The words of the voice clip, 2,000 characters at most; cloning models |
instruct | string | A voice description or delivery, 500 characters at most; omnivoice without voice, and breeze2 |
language | string | A language code, or auto; values per model |
speed | number | 0.5 to 2.0; kokoro and omnivoice |
seed | integer | 0 to 4294967295; the same seed and parameters give the same audio; cloning models |
format | string | mp3 (default), wav or opus |
params of a transcription job | Type | Description |
|---|---|---|
audio required | string | An upload up_…, a job result job:<id>/<i>, a saved voice vo_… or an https:// URL |
language | string | A language code as a hint, or auto (default) |
timestamps | boolean | true adds words with start and end times to the transcript; default false |
Each model's exact schema: List models.
Request
curl
curl "https://api.albastov.com/v1/media/jobs?wait=30" \
-H "Authorization: Bearer $ALBASTOV_API_KEY" \
-H "Content-Type: application/json" \
-d '{"kind": "speech", "model": "kokoro", "params": {"text": "The river bends past the old mill.", "voice": "bm_george", "format": "wav"}}'Python
import json
import os
import urllib.request
req = urllib.request.Request(
"https://api.albastov.com/v1/media/jobs?wait=30",
data=json.dumps({"kind": "speech", "model": "kokoro",
"params": {"text": "The river bends past the old mill.", "voice": "bm_george", "format": "wav"}}).encode(),
headers={"User-Agent": "my-app/1.0", "Authorization": f"Bearer {os.environ['ALBASTOV_API_KEY']}", "Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=120) as res:
job = json.load(res)
print(job["id"], job["state"], [r["url"] for r in job["results"]])JavaScript
const res = await fetch("https://api.albastov.com/v1/media/jobs?wait=30", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ALBASTOV_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
kind: "speech",
model: "kokoro",
params: { text: "The river bends past the old mill.", voice: "bm_george", format: "wav" },
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const job = await res.json();
console.log(job.id, job.state, job.results.map((r) => r.url));Response
202 with the job while it is queued or running; 200 once the job is settled within wait, and for a repeated
idempotency_key.
{
"id": "mj_HSRJJxSiza6v7xZULYpwgh",
"kind": "speech",
"model": "kokoro",
"label": "",
"state": "done",
"waiting": null,
"position": null,
"priority": "interactive",
"params": {"format": "wav", "language": "auto", "speed": 1, "text": "The river bends past the old mill.", "voice": "bm_george"},
"results": [
{
"index": 0,
"url": "https://api.albastov.com/v1/media/files/mj_HSRJJxSiza6v7xZULYpwgh/0.wav",
"content_type": "audio/wav",
"bytes": 113324,
"duration_s": 2.36,
"sample_rate": 24000
}
],
"error": null,
"created_at": "2026-11-03T02:16:32.602Z",
"started_at": "2026-11-03T02:16:32.611Z",
"finished_at": "2026-11-03T02:16:32.792Z",
"gen_s": 0.18,
"queue_timeout_s": 300
}The response above leaves out the fields that are null for speech results and the fields for image and video jobs.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | bad-request | A top-level field is unknown or has the wrong type, or priority, label or idempotency_key is not valid; param names the field |
| 400 | bad-param | model, kind, queue_timeout_s, wait or a parameter fails the model's schema; param names the field |
| 400 | bad-json | The body is not a JSON object |
| 404 | voice_not_found | No saved voice with the id, or a voice the key may not use |
| 409 | voice_not_ready | A designed voice still pending, or failed |
| 409 | idempotency-mismatch | The idempotency key was used before with another request |
| 413 | too-large | The body is over 4 MB |
| 422 | voice_required | A cloning model that needs voice, without one |
| 422 | voice_not_supported | The model does not speak saved voices; models lists the models that do |
| 429 | budget-exhausted | The account's daily media time is used up; resets_at is the next 00:00 UTC |
| 429 | too-many-open | The key has 1,000 jobs queued or running |
| 503 | voice_model_unavailable | No model that speaks saved voices is online for a job without model |
Get a job
GET /v1/media/jobs/{id} returns a job of the key's account.
Parameters
| Name | Type | Description |
|---|---|---|
id required | string | The job id mj_…, in the path; also X-Job-Id and job_id of the audio routes |
wait | number | Query parameter: seconds to hold the request until the job settles, 0 to 90 |
Request
The example queues a Kokoro job without waiting, then reads the job with wait=30.
curl
JOB=$(curl https://api.albastov.com/v1/media/jobs \
-H "Authorization: Bearer $ALBASTOV_API_KEY" \
-H "Content-Type: application/json" \
-d '{"kind": "speech", "model": "kokoro", "params": {"text": "The river bends past the old mill."}}' \
| python3 -c 'import json, sys; print(json.load(sys.stdin)["id"])')
curl "https://api.albastov.com/v1/media/jobs/$JOB?wait=30" \
-H "Authorization: Bearer $ALBASTOV_API_KEY"Python
import json
import os
import urllib.request
BASE = "https://api.albastov.com/v1"
HEADERS = {"User-Agent": "my-app/1.0", "Authorization": f"Bearer {os.environ['ALBASTOV_API_KEY']}", "Content-Type": "application/json"}
body = {"kind": "speech", "model": "kokoro", "params": {"text": "The river bends past the old mill."}}
req = urllib.request.Request(f"{BASE}/media/jobs", data=json.dumps(body).encode(), headers=HEADERS)
with urllib.request.urlopen(req, timeout=30) as res:
job = json.load(res)
req = urllib.request.Request(f"{BASE}/media/jobs/{job['id']}?wait=30", headers=HEADERS)
with urllib.request.urlopen(req, timeout=120) as res:
job = json.load(res)
print(job["id"], job["state"])JavaScript
const base = "https://api.albastov.com/v1";
const headers = { Authorization: `Bearer ${process.env.ALBASTOV_API_KEY}`, "Content-Type": "application/json" };
const created = await fetch(`${base}/media/jobs`, {
method: "POST",
headers,
body: JSON.stringify({ kind: "speech", model: "kokoro", params: { text: "The river bends past the old mill." } }),
});
if (!created.ok) throw new Error(`${created.status} ${await created.text()}`);
const { id } = await created.json();
const res = await fetch(`${base}/media/jobs/${id}?wait=30`, { headers });
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const job = await res.json();
console.log(job.id, job.state);Response
200 with the job object, as in Create a job;
202 when wait passes before the job settles.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | bad-param | wait is not a number of seconds |
| 404 | not-found | No job with the id on the key's account |
List models
GET /v1/media/models returns the models the key may use, each with its parameter schema. Within a kind,
recommended models come first.
Parameters
| Name | Type | Description |
|---|---|---|
kind | string | Query parameter: speech or transcription; without kind, every kind. An unknown kind returns an empty list |
Request
curl
curl "https://api.albastov.com/v1/media/models?kind=speech" \
-H "Authorization: Bearer $ALBASTOV_API_KEY"Python
import json
import os
import urllib.request
req = urllib.request.Request(
"https://api.albastov.com/v1/media/models?kind=speech",
headers={"User-Agent": "my-app/1.0", "Authorization": f"Bearer {os.environ['ALBASTOV_API_KEY']}"},
)
with urllib.request.urlopen(req, timeout=30) as res:
for m in json.load(res)["models"]:
print(m["id"], m["recommended"], m["available"], sorted(m["params"]))JavaScript
const res = await fetch("https://api.albastov.com/v1/media/models?kind=speech", {
headers: { Authorization: `Bearer ${process.env.ALBASTOV_API_KEY}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
for (const m of (await res.json()).models) console.log(m.id, m.recommended, m.available, Object.keys(m.params));Response
{
"models": [
{
"id": "kokoro",
"kind": "speech",
"label": "Kokoro 82M",
"family": "kokoro",
"notes": "Fast default: 54 built-in voices in 8 languages. No cloning.",
"params": {
"text": {"type": "string", "required": true, "max_length": 20000},
"voice": {"type": "enum", "values": ["af_heart", "af_alloy", "bf_emma"], "default": "af_heart", "description": "A built-in voice; its prefix is its language."},
"speed": {"type": "float", "min": 0.5, "max": 2.0, "step": 0.05, "default": 1.0, "description": "Speaking rate multiplier."},
"format": {"type": "enum", "values": ["mp3", "wav", "opus"], "default": "mp3", "description": "The output file: mp3, wav or opus (Ogg)."}
},
"available": true,
"queueable": true,
"recommended": true
}
]
}The response above is shortened to one model, three of its voices and four of its parameters.
| Field | Value |
|---|---|
id, kind, label, family | The model id, its kind, its display name and its model family |
notes | One line on what the model does |
params | The parameter schema: per name, type (string, enum, float, int, bool, seed, audio), and required, default, min, max, step, values, max_length, description where set. An audio parameter takes a voice reference |
available | true when a job for the model starts without waiting |
queueable | true when the model accepts jobs; a job waits in the queue while available is false |
recommended | true for the recommended models of the kind |
Other fields of the response are for albastov.com's own pages.