# Quickstart

> Create an API key, turn text into speech, then queue an image and poll it until it is ready.

Three steps: a key, a request that answers with audio, and a request that is queued and polled.

## 1. Get an API key

Sign in to the console at https://app.lumavox.ai and open **API keys**. Your organisation's master key is there, and you can create keys limited to the capabilities they need. A scoped key is what you want in production — see [authentication](https://docs.lumavox.ai/authentication.md).

Keep the key on your server and out of source control. The examples read it from an environment variable:

```bash title="Shell"
export API_KEY="paste-your-key-here"
```

:::caution
Never put an API key in browser JavaScript or a mobile app. Anyone can read it there, and it spends your credit until you revoke it. To let a user's device talk to an agent, mint them an [ephemeral key](https://docs.lumavox.ai/agents/end-users.md).
:::

## 2. Turn text into speech

`POST /v1/tts` answers with the audio itself — a WAV file unless you ask for something else.

```bash title="curl"
curl https://api.lumavox.ai/v1/tts \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Hello from the API."}' \
  --output hello.wav
```

```js title="JavaScript"
// Node 18 or later: fetch is built in.
import { writeFile } from "node:fs/promises";

const response = await fetch("https://api.lumavox.ai/v1/tts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ text: "Hello from the API." }),
});
if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${response.status} ${error.code}: ${error.message}`);
}
await writeFile("hello.wav", Buffer.from(await response.arrayBuffer()));
console.log("saved hello.wav, request", response.headers.get("X-Request-Id"));
```

```python title="Python"
import os

import requests

response = requests.post(
    "https://api.lumavox.ai/v1/tts",
    headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
    json={"text": "Hello from the API."},
)
response.raise_for_status()
with open("hello.wav", "wb") as f:
    f.write(response.content)
print("saved hello.wav, request", response.headers["X-Request-Id"])
```

Play `hello.wav` and you have your first generation.

With no `voice_id` you get the built-in voice. `GET /v1/voices` lists every voice your key may use; pass one's `id` as `voice_id`:

```bash title="curl"
curl https://api.lumavox.ai/v1/voices -H "Authorization: Bearer $API_KEY"
```

Every response carries an `X-Request-Id` header. Keep it: it is what to quote when something goes wrong. Speech responses also say how long the audio is (`X-Audio-Seconds`), which model spoke (`X-Model`) and the rate it was produced at (`X-Sample-Rate`). [Text to speech](https://docs.lumavox.ai/speech/text-to-speech.md) covers models, formats and streaming.

## 3. Queue an image and poll it

Images, video, stored audio, skyboxes and long transcriptions take longer than a request should wait. They answer `202 Accepted` at once with a `gen_…` id, and you poll that id until it finishes.

```bash title="curl"
curl https://api.lumavox.ai/v1/images \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "A weathered brass diving helmet on wet slate, low winter light"}'
```

The response describes the job — what you asked for, what it cost, and where it stands:

```json title="Response — 202 (abridged)"
{
  "id": "gen_a1B2c3D4e5F6g7H8",
  "status": "queued",
  "capability": "image",
  "model": "flux-2-max",
  "prompt": "A weathered brass diving helmet on wet slate, low winter light",
  "aspect_ratio": "1:1",
  "width": 992,
  "height": 992,
  "megapixels": 1,
  "output_format": "jpeg",
  "credits_micros": 4000000,
  "image": null,
  "error": null,
  "created_at": "2026-10-24T09:30:00.000Z",
  "finished_at": null
}
```

Every response writes values the same way: times in UTC with milliseconds, credits as integers of micro-credits (`4000000` is four credits), and `null` for a field with nothing in it. [Conventions](https://docs.lumavox.ai/conventions.md) has the rules.

Poll it every couple of seconds:

```bash title="curl"
curl https://api.lumavox.ai/v1/images/gen_a1B2c3D4e5F6g7H8 \
  -H "Authorization: Bearer $API_KEY"
```

`queued`, `submitted` and `running` mean keep polling. `succeeded`, `failed` and `moderated` are final. Once it has succeeded, the image is on the response:

```json title="Response — 200 (abridged)"
{
  "id": "gen_a1B2c3D4e5F6g7H8",
  "status": "succeeded",
  "capability": "image",
  "image": {
    "asset_id": "ast_x9Y8z7W6v5U4t3S2",
    "url": "https://…",
    "width": 992,
    "height": 992,
    "content_type": "image/jpeg",
    "size_bytes": 284113
  },
  "error": null,
  "created_at": "2026-10-24T09:30:00.000Z",
  "finished_at": "2026-10-24T09:30:07.412Z"
}
```

`image.url` is a short-lived link — download the file promptly. The image is also kept in your [library](https://docs.lumavox.ai/library/assets.md) under `image.asset_id`, which is where to fetch it from later.

The same loop in code:

```js title="JavaScript"
const headers = {
  Authorization: `Bearer ${process.env.API_KEY}`,
  "Content-Type": "application/json",
};

const submitted = await fetch("https://api.lumavox.ai/v1/images", {
  method: "POST",
  headers,
  body: JSON.stringify({ prompt: "A lighthouse in fog, oil painting" }),
}).then((r) => r.json());

let job = submitted;
while (["queued", "submitted", "running"].includes(job.status)) {
  await new Promise((resolve) => setTimeout(resolve, 2000));
  job = await fetch(`https://api.lumavox.ai/v1/images/${submitted.id}`, { headers }).then(
    (r) => r.json(),
  );
}
console.log(job.status, job.image?.url ?? job.error);
```

```python title="Python"
import os
import time

import requests

headers = {"Authorization": f"Bearer {os.environ['API_KEY']}"}

job = requests.post(
    "https://api.lumavox.ai/v1/images",
    headers=headers,
    json={"prompt": "A lighthouse in fog, oil painting"},
).json()

while job["status"] in ("queued", "submitted", "running"):
    time.sleep(2)
    job = requests.get(f"https://api.lumavox.ai/v1/images/{job['id']}", headers=headers).json()

print(job["status"], job["image"]["url"] if job["image"] else job["error"])
```

A queued job is charged when it is accepted and refunded in full if it fails. [Queued jobs](https://docs.lumavox.ai/jobs.md) has the details.

## Next steps

- [Authentication](https://docs.lumavox.ai/authentication.md) — scoped keys, and ephemeral keys for your users' devices.
- [Conventions](https://docs.lumavox.ai/conventions.md) — how every endpoint writes times, credits and lists, and how it pages.
- [Errors and limits](https://docs.lumavox.ai/errors.md) — the error envelope and what each status means.
- [Text to speech](https://docs.lumavox.ai/speech/text-to-speech.md) — models, formats, sample rates and streaming.
- [Agents](https://docs.lumavox.ai/agents.md) — characters that hold a spoken conversation.
- [API reference](https://docs.lumavox.ai/reference.md) — every endpoint and field.
