voao pro

Quickstart

From a key to a downloaded result in six steps. The samples use $VOAO_API for your API address and $VOAO_API_KEY for a key with the scopes models:read, jobs:write, jobs:read and assets:read (and assets:write for step 7).

export VOAO_API=https://api.example.com   # your deployment's API address
export VOAO_API_KEY=vk_k3x9Qm2a_...       # shown once when the key was created

1. List models

curl -sS "$VOAO_API/v1/models" -H "Authorization: Bearer $VOAO_API_KEY"

You get {"models": [...]}. Each model has a key, a name, a kind (image, video, audio, text, 3d), locked (above your plan; it cannot be used) and input_schema, a JSON Schema of the settings a job of the model takes. Tools are listed the same way at GET /v1/tools.

2. Price a request

curl -sS -X POST "$VOAO_API/v1/price" \
  -H "Authorization: Bearer $VOAO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"fake-image","input":{"prompt":"a red bicycle"}}'
# {"price": 4}

The price is in credits. It runs the same checks and pricing as a submit and reserves nothing, so a job with the same request reserves exactly this.

3. Submit a job

Idempotency-Key is required; see idempotency.

curl -sS -X POST "$VOAO_API/v1/jobs" \
  -H "Authorization: Bearer $VOAO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"model":"fake-image","input":{"prompt":"a red bicycle"}}'

The answer is 201 with the job:

{
  "id": "0b7d8f9e-1c2a-4b3d-8e4f-5a6b7c8d9e0f",
  "model": "fake-image",
  "status": "queued",
  "price": 4,
  "input": { "prompt": "a red bicycle" },
  "queue_position": 1,
  "created_at": "2026-10-24T09:29:40Z",
  "updated_at": "2026-10-24T09:29:40Z"
}

4. Wait for it

Either register a webhook and handle job.succeeded, job.failed, job.blocked and job.cancelled, or poll every few seconds:

curl -sS "$VOAO_API/v1/jobs/$JOB_ID" -H "Authorization: Bearer $VOAO_API_KEY"

Stop when status is succeeded, failed, blocked or cancelled. A succeeded job has result_asset_ids; a failed or blocked one has error_code and error_message. To stop a job: POST /v1/jobs/{jobId}/cancel (needs jobs:write; a queued job is cancelled at once and its credits return).

5. Fetch the asset

curl -sS "$VOAO_API/v1/assets/$ASSET_ID" -H "Authorization: Bearer $VOAO_API_KEY"

returns the asset (kind, content_type, size_bytes, width, height, ...).

6. Download the file

curl -sS "$VOAO_API/v1/assets/$ASSET_ID/download" -H "Authorization: Bearer $VOAO_API_KEY"
# {"url":"https://...","expires_at":"2026-10-24T10:30:00Z","marked":false}
curl -sS -o result.png "<url>"

The link is signed and expires; ask again for a new one. Downloads follow the same delivery rules as the web app: the file carries your organization's copyright notice and a mark id in its metadata, and a visible watermark unless your plan removes it (marked says so).

For a video, the marked copy is made on first request: the answer is 202 with {"state":"preparing"}. Ask again after a few seconds.

7. Upload an input

To use a file of yours as a setting of a job (a reference picture, say), upload it first. Large files go in parts.

  1. Start (assets:write): POST /v1/assets/uploads with {"name","content_type","size_bytes"}. The answer has the new asset_id, a part_size (bytes per part except the last) and a part_count.
  2. Get links: POST /v1/assets/{assetId}/upload/parts with {"parts":[1,2,...]}. The answer is {"parts":[{"number":1,"url":"..."}]}.
  3. Send each part: PUT the part's bytes to its url, and keep the ETag response header.
  4. Complete: POST /v1/assets/{assetId}/complete with {"parts":[{"number":1,"etag":"..."}]}. The answer is the asset.

Then pass the asset id in the field of the model's input_schema that takes a file.

SIZE=$(wc -c < ref.png | tr -d ' ')
PLAN=$(curl -sS -X POST "$VOAO_API/v1/assets/uploads" \
  -H "Authorization: Bearer $VOAO_API_KEY" -H "Content-Type: application/json" \
  -d "{\"name\":\"ref.png\",\"content_type\":\"image/png\",\"size_bytes\":$SIZE}")
# single part when SIZE <= part_size:
ASSET=$(echo "$PLAN" | jq -r .asset_id)
URL=$(curl -sS -X POST "$VOAO_API/v1/assets/$ASSET/upload/parts" \
  -H "Authorization: Bearer $VOAO_API_KEY" -H "Content-Type: application/json" \
  -d '{"parts":[1]}' | jq -r '.parts[0].url')
ETAG=$(curl -sS -D - -o /dev/null -X PUT --data-binary @ref.png "$URL" | awk -F': ' 'tolower($1)=="etag"{print $2}' | tr -d '\r')
curl -sS -X POST "$VOAO_API/v1/assets/$ASSET/complete" \
  -H "Authorization: Bearer $VOAO_API_KEY" -H "Content-Type: application/json" \
  -d "{\"parts\":[{\"number\":1,\"etag\":$ETAG}]}"

(The ETag is usually already quoted, so it goes into the JSON as is.)

Node.js

import { writeFile } from "node:fs/promises";

const API = process.env.VOAO_API;
const headers = { Authorization: `Bearer ${process.env.VOAO_API_KEY}`, "Content-Type": "application/json" };

async function call(path, init = {}) {
  const res = await fetch(`${API}${path}`, { ...init, headers: { ...headers, ...init.headers } });
  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`${res.status} ${error.code}: ${error.message} (request ${res.headers.get("X-Request-ID")})`);
  }
  return res.json();
}

const request = { model: "fake-image", input: { prompt: "a red bicycle" } };

const { price } = await call("/v1/price", { method: "POST", body: JSON.stringify(request) });
console.log("will cost", price, "credits");

// Make the key once per logical job and reuse it on every retry.
let job = await call("/v1/jobs", {
  method: "POST",
  headers: { "Idempotency-Key": crypto.randomUUID() },
  body: JSON.stringify(request),
});

const done = new Set(["succeeded", "failed", "blocked", "cancelled"]);
while (!done.has(job.status)) {
  await new Promise((r) => setTimeout(r, 3000));
  job = await call(`/v1/jobs/${job.id}`);
}
if (job.status !== "succeeded") throw new Error(`${job.status}: ${job.error_code} ${job.error_message}`);

const link = await call(`/v1/assets/${job.result_asset_ids[0]}/download`);
const file = await fetch(link.url);
await writeFile("result.png", Buffer.from(await file.arrayBuffer()));

Python

import os
import time
import uuid

import requests

API = os.environ["VOAO_API"]
S = requests.Session()
S.headers["Authorization"] = f"Bearer {os.environ['VOAO_API_KEY']}"


def call(method, path, **kw):
    r = S.request(method, API + path, timeout=30, **kw)
    if not r.ok:
        err = r.json()["error"]
        raise RuntimeError(f"{r.status_code} {err['code']}: {err['message']} (request {r.headers.get('X-Request-ID')})")
    return r.json()


request = {"model": "fake-image", "input": {"prompt": "a red bicycle"}}

print("will cost", call("POST", "/v1/price", json=request)["price"], "credits")

# Make the key once per logical job and reuse it on every retry.
job = call("POST", "/v1/jobs", json=request, headers={"Idempotency-Key": str(uuid.uuid4())})

while job["status"] not in ("succeeded", "failed", "blocked", "cancelled"):
    time.sleep(3)
    job = call("GET", f"/v1/jobs/{job['id']}")
if job["status"] != "succeeded":
    raise RuntimeError(f"{job['status']}: {job.get('error_code')} {job.get('error_message')}")

link = call("GET", f"/v1/assets/{job['result_asset_ids'][0]}/download")
with open("result.png", "wb") as f:
    f.write(requests.get(link["url"], timeout=60).content)

Next

  • Handle 429 by waiting Retry-After seconds (limits).
  • Replace polling with a webhook.
  • Read your usage with GET /v1/usage?from=2026-10-01&to=2026-10-31 (needs usage:read; at most one year).
Quickstart - voao pro developers