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 created1. 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.
- Start (
assets:write):POST /v1/assets/uploadswith{"name","content_type","size_bytes"}. The answer has the newasset_id, apart_size(bytes per part except the last) and apart_count. - Get links:
POST /v1/assets/{assetId}/upload/partswith{"parts":[1,2,...]}. The answer is{"parts":[{"number":1,"url":"..."}]}. - Send each part:
PUTthe part's bytes to itsurl, and keep theETagresponse header. - Complete:
POST /v1/assets/{assetId}/completewith{"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)