> ## Documentation Index
> Fetch the complete documentation index at: https://docs.voyager.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Create an image job, wait for it to finish, and download the result in curl, Python or JavaScript.

In a few minutes you'll generate an image with the Voyager API and save it to disk.

> [!NOTE]
>
> **Before you start:** you need a Voyager API key from a paid plan (trials count) in the `VOYAGER_API_KEY` environment variable. See [Authentication](https://docs.voyager.so/api/authentication.md). The job is paid from your workspace's Voyager credits; prices are on [voyager.so/pricing](https://voyager.so/pricing).

## The three calls

1. **Create** a job with `POST /v1/jobs`. Send an `Idempotency-Key` so a retry never buys twice.
2. **Wait** with `GET /v1/jobs/{id}?wait=25`. The server holds the request for up to 25 seconds and answers as soon as the job finishes. Repeat while the status is `queued` or `running`.
3. **Download** `outputs[].url`, a signed link that works for 1 hour without your key.

```bash curl theme={"system"}
# Needs curl, jq and uuidgen.
API=https://api.voyager.so/v1
AUTH="Authorization: Bearer $VOYAGER_API_KEY"

# 1. Create
JOB_ID=$(curl -sf "$API/jobs" \
  -H "$AUTH" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"model": "nano-banana-2.1", "operation": "text-to-image", "input": {"prompt": "a red fox in snow", "aspect_ratio": "16:9"}}' \
  | jq -r .id)
echo "created $JOB_ID"

# 2. Wait
while :; do
  JOB=$(curl -sf "$API/jobs/$JOB_ID?wait=25" -H "$AUTH")
  STATUS=$(echo "$JOB" | jq -r .status)
  [ "$STATUS" = queued ] || [ "$STATUS" = running ] || break
done
echo "status: $STATUS"
[ "$STATUS" = succeeded ] || { echo "$JOB" | jq .error; exit 1; }

# 3. Download
curl -sf -o fox.png "$(echo "$JOB" | jq -r '.outputs[0].url')"
echo "$JOB" | jq .credits
```

```python Python theme={"system"}
# pip install requests
import os
import time
import uuid

import requests

API = "https://api.voyager.so/v1"
AUTH = {"Authorization": f"Bearer {os.environ['VOYAGER_API_KEY']}"}

# 1. Create
response = requests.post(
    f"{API}/jobs",
    headers={**AUTH, "Idempotency-Key": str(uuid.uuid4())},
    json={"model": "nano-banana-2.1", "operation": "text-to-image", "input": {"prompt": "a red fox in snow", "aspect_ratio": "16:9"}},
    timeout=30,
)
response.raise_for_status()
job = response.json()
print("created", job["id"])

# 2. Wait
while job["status"] in ("queued", "running"):
    response = requests.get(f"{API}/jobs/{job['id']}", headers=AUTH, params={"wait": 25}, timeout=60)
    response.raise_for_status()
    job = response.json()
    if job["status"] in ("queued", "running") and job.get("poll_after_ms"):
        time.sleep(job["poll_after_ms"] / 1000)

if job["status"] != "succeeded":
    raise SystemExit(f"{job['status']}: {job['error']}")

# 3. Download
output = job["outputs"][0]
extension = output["mime_type"].split("/")[-1]
download = requests.get(output["url"], timeout=120)
download.raise_for_status()  # fails on an expired link: GET the job again for a fresh URL
with open(f"fox.{extension}", "wb") as file:
    file.write(download.content)

print("saved", f"fox.{extension}", "credits", job["credits"])
```

```javascript JavaScript theme={"system"}
// Node 18+; save as quickstart.mjs and run: node quickstart.mjs
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";

const API = "https://api.voyager.so/v1";
const AUTH = { Authorization: `Bearer ${process.env.VOYAGER_API_KEY}` };

async function call(path, init = {}) {
  const response = await fetch(`${API}${path}`, { ...init, headers: { ...AUTH, ...init.headers } });
  const body = await response.json();
  if (!response.ok) throw new Error(`${response.status} ${body.error?.code}: ${body.error?.message}`);
  return body;
}

// 1. Create
let job = await call("/jobs", {
  method: "POST",
  headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },
  body: JSON.stringify({
    model: "nano-banana-2.1",
    operation: "text-to-image",
    input: { prompt: "a red fox in snow", aspect_ratio: "16:9" },
  }),
});
console.log("created", job.id);

// 2. Wait
while (job.status === "queued" || job.status === "running") {
  job = await call(`/jobs/${job.id}?wait=25`);
  if ((job.status === "queued" || job.status === "running") && job.poll_after_ms) {
    await new Promise((resolve) => setTimeout(resolve, job.poll_after_ms));
  }
}
if (job.status !== "succeeded") throw new Error(`${job.status}: ${JSON.stringify(job.error)}`);

// 3. Download
const output = job.outputs[0];
const file = `fox.${output.mime_type.split("/")[1]}`;
const download = await fetch(output.url);
// Fails on an expired link: GET the job again for a fresh URL.
if (!download.ok) throw new Error(`download failed: ${download.status}`);
await writeFile(file, Buffer.from(await download.arrayBuffer()));
console.log("saved", file, "credits", job.credits);
```

## Check it worked

You have an image file next to your script, and the job printed its `credits`. `credits.charged` is what the job cost. If `credits.state` is still `pending`, the final amount isn't confirmed yet: `credits.estimated` is the estimate, and `GET /v1/jobs/{id}` will show the final charge later. See [Credits](https://docs.voyager.so/api/jobs.md#credits).

> [!TIP]
>
> For a fast model, add the header `Prefer: wait=30` to the create request (up to 60 seconds). If the job finishes in time, the create response is `200 OK` with the finished job and the loop in step 2 ends at once.

## If something went wrong

> [accordion] 401 invalid\_api\_key
>
> The key is missing, mistyped or revoked. Check `echo $VOYAGER_API_KEY` starts with `vy_live_`, and that the key is still listed on [voyager.so/dashboard/api](https://voyager.so/dashboard/api).

> [accordion] 402 paid\_plan\_required or insufficient\_credits
>
> The workspace isn't on a paid plan, or doesn't have enough Voyager credits for this job. Open `error.details.upgrade_url`. See [Errors](https://docs.voyager.so/api/errors.md#insufficient_credits).

> [accordion] 400 invalid\_input
>
> The `input` doesn't match the model's `input_schema`, or has a field the model doesn't take. Read `error.details`, then check the schema with `GET /v1/models/nano-banana-2.1`. See [Models](https://docs.voyager.so/api/models.md).

> [accordion] The job failed
>
> Read `job.error.code` and look it up on [Errors](https://docs.voyager.so/api/errors.md). A failed job releases its held credits unless its type's [settlement rule](https://docs.voyager.so/api/jobs.md#how-each-type-is-charged) says otherwise.

## Next

> [card] [Jobs and polling](https://docs.voyager.so/api/jobs.md)
>
> Statuses, credits, cancel, uploads and idempotency.

> [card] [Models](https://docs.voyager.so/api/models.md)
>
> Find video, speech, music and 3D models and their inputs.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.