Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.oxen.ai/llms.txt

Use this file to discover all available pages before exploring further.

Why Use the Async Queue

The synchronous endpoints (/ai/images/generate, /ai/videos/generate) block until the result is ready. For video generation, this can be 1-10+ minutes. The async queue returns immediately with generation IDs so you can:
  • Run many generations in parallel (up to 4 per request, no limit on total queued)
  • Avoid long-lived HTTP connections
  • Build progress-tracking UIs
  • Query completed generations and their output URLs at any time

Workflow

1. POST /ai/queue                       → Get generation IDs (status: queued)
2. GET  /ai/queue  or  /ai/queue/:id    → Poll until status is succeeded or failed
3. Read result_url from the completed generation
Generations persist after reaching a terminal state (succeeded, failed, or cancelled), so you can retrieve results at any time. A Server-Sent Events stream at GET /api/events can also deliver completion notifications with the output file URL. See Completion Events.

Enqueue

POST /api/ai/queue
Submit an async image or video generation job.

Required Parameters

ParameterTypeDescription
modelstringMust be an image or video model. Text-only models are rejected.

Additional Parameters

ParameterTypeDefaultDescription
num_generationsinteger1How many generations to enqueue per request. Range: 1-4. Call the endpoint multiple times to queue more.
target_namespacestringyour usernameNamespace to store results and bill to.
All other parameters (e.g. prompt, multi_prompt, input_image, input_video, aspect_ratio, duration, seed, generate_audio, response_format) are passed through to the model. Consult the model’s request_schema via GET /api/ai/models/:id for supported parameters and their constraints.

Response

{
  "generations": [
    {"generation_id": "bb8f5eb7-361e-4e13-ab73-67457bc8057e", "status": "queued"},
    {"generation_id": "e9bede09-cd0e-46cf-bbcd-cb1a50099351", "status": "queued"}
  ]
}

Validation

The API validates at enqueue time that:
  • The model exists and has image or video output capability
  • num_generations is 1-4
  • The user is authenticated with sufficient credits
Model-specific parameter validation (prompt content, duration ranges, aspect ratio values) happens when the generation runs, not at enqueue time. If a parameter is invalid, the generation transitions to failed status with an error_message. To validate parameters and get immediate error feedback, use /ai/images/generate or /ai/videos/generate instead.

List Generations

GET /api/ai/queue
Lists generations for the authenticated user’s namespace. By default, only active generations (queued or processing) are returned. Pass an explicit status filter to include terminal states.

Query Parameters

ParameterTypeRequiredDescription
namespacestringnoNamespace to query. Defaults to the authenticated user.
modelstringnoFilter by model name.
statusstringnoFilter by status: queued, processing, succeeded, failed, or cancelled. When omitted, only active generations are returned.
media_typestringnoFilter by image or video.
repostringnoFilter by target repository name.
folderstringnoFilter by target directory.

Response

{
  "count": 2,
  "generations": [
    {
      "generation_id": "7cf9b23a-1234-5678-9abc-def012345678",
      "model_name": "kling-video-o3-pro-reference-to-video",
      "prompt": "An astronaut walking on Mars",
      "media_type": "video",
      "status": "processing",
      "result_url": null,
      "error_message": null,
      "enqueued_at": 1775091431,
      "started_at": 1775091432,
      "completed_at": null,
      "aspect_ratio": "16:9",
      "duration": 10
    },
    {
      "generation_id": "bb8f5eb7-361e-4e13-ab73-67457bc8057e",
      "model_name": "black-forest-labs-flux-2-klein-4b",
      "prompt": "Abstract geometric pattern in blue and gold",
      "media_type": "image",
      "status": "queued",
      "result_url": null,
      "error_message": null,
      "enqueued_at": 1775091440,
      "started_at": null,
      "completed_at": null
    }
  ]
}
FieldAlways PresentDescription
countyesNumber of generations in the response
generation_idyesUnique ID for this generation
model_nameyesModel name
promptyesText prompt (from original request parameters)
media_typeyes"image" or "video"
statusyes"queued", "processing", "succeeded", "failed", or "cancelled"
result_urlyesOutput file URL when succeeded, otherwise null
error_messageyesError details when failed, otherwise null
enqueued_atyesUnix timestamp when the job was enqueued
started_atyesUnix timestamp when processing began, or null
completed_atyesUnix timestamp when the job reached a terminal state, or null
seedif submittedRandom seed
aspect_ratioif submittedAspect ratio
durationif submittedVideo duration
Any additional parameters from the original enqueue request (e.g. seed, aspect_ratio, duration, input_image) are included in the response alongside the fields above.

Polling Strategy

  • Image generation (FLUX, etc.): typically completes in 5-30 seconds. Poll every 2-5 seconds.
  • Video generation (Kling, etc.): typically takes 1-5 minutes. Poll every 10-30 seconds.
Poll until every generation’s status is a terminal value (succeeded, failed, or cancelled), or until count reaches 0 when using the default active-only filter.

Get Generation

GET /api/ai/queue/:generation_id
Retrieves metadata for a single generation. Includes result_url when the generation has succeeded and error_message when it has failed.

Path Parameters

ParameterTypeRequiredDescription
generation_idstring (UUID)yesThe generation ID returned by the enqueue endpoint.

Response (in progress)

{
  "generation_id": "7cf9b23a-1234-5678-9abc-def012345678",
  "model_name": "kling-video-o3-pro-reference-to-video",
  "prompt": "An astronaut walking on Mars",
  "media_type": "video",
  "status": "processing",
  "result_url": null,
  "error_message": null,
  "enqueued_at": 1775091431,
  "started_at": 1775091432,
  "completed_at": null,
  "aspect_ratio": "16:9",
  "duration": 10
}

Response (succeeded)

{
  "generation_id": "7cf9b23a-1234-5678-9abc-def012345678",
  "model_name": "kling-video-o3-pro-reference-to-video",
  "prompt": "An astronaut walking on Mars",
  "media_type": "video",
  "status": "succeeded",
  "result_url": "https://hub.oxen.ai/api/repos/...",
  "error_message": null,
  "enqueued_at": 1775091431,
  "started_at": 1775091432,
  "completed_at": 1775091590,
  "aspect_ratio": "16:9",
  "duration": 10
}

Response (failed)

{
  "generation_id": "7cf9b23a-1234-5678-9abc-def012345678",
  "model_name": "kling-video-o3-pro-reference-to-video",
  "prompt": "An astronaut walking on Mars",
  "media_type": "video",
  "status": "failed",
  "result_url": null,
  "error_message": "Insufficient credits",
  "enqueued_at": 1775091431,
  "started_at": 1775091432,
  "completed_at": 1775091435
}

Response (not found)

Returns 404 when the generation ID does not exist:
{
  "error": {
    "type": "resource_not_found",
    "title": "The requested resource could not be found"
  },
  "status": "error",
  "status_message": "resource_not_found"
}

Cancel Generation

DELETE /api/ai/queue/:generation_id
Cancels a queued or in-progress generation.

Path Parameters

ParameterTypeRequiredDescription
generation_idstring (UUID)yesThe generation ID to cancel.

Response (success)

{
  "status": "success",
  "generation_id": "bb8f5eb7-361e-4e13-ab73-67457bc8057e"
}

Response (not found)

Returns 404 when the generation ID does not exist:
{
  "error": {
    "type": "resource_not_found",
    "title": "The requested resource could not be found"
  },
  "status": "error",
  "status_message": "resource_not_found"
}
You can only cancel generations that are still active (queued or processing). Cancelling a generation that has already reached a terminal state (succeeded, failed, or cancelled) has no effect.

Completion Events

GET /api/events
Server-Sent Events stream that emits media_generation_completed events when generations reach a terminal state.

Connect

GET /api/events
Authorization: Bearer $OXEN_API_KEY
Response is Content-Type: text/event-stream. The server sends : keep-alive\n\n every 15 seconds when idle. Events are broadcast to currently-connected subscribers with no buffering. Anything that fires before you connect is lost. Subscribe before calling POST /ai/queue to avoid missing events.

Event: media_generation_completed

Fires once per generation on terminal state. Success wire format:
event: media_generation_completed
data: {"generation_id":"bb8f5eb7-...","status":"succeeded","media_type":"video","model":"kling-video-o3-pro-reference-to-video","url":"https://hub.oxen.ai/api/repos/..."}

Failure:
event: media_generation_completed
data: {"generation_id":"bb8f5eb7-...","status":"failed","media_type":"video","error":"Insufficient credits"}

Fields

FieldsucceededfailedDescription
generation_idyesyesMatches the ID returned by POST /ai/queue
status"succeeded""failed"Only these two values appear
media_typeyesyes"image" or "video"
modelyesnoModel name
urlyesnoPresigned URL to the output file. Expires after a limited time.
errornoyesHuman-readable failure reason

Other events on this stream

GET /api/events is a user-scoped stream that may carry unrelated event types (e.g. deployment events). Filter on the event: line and ignore anything other than media_generation_completed.

Example

import json
import requests
import threading
import time

API_KEY = "YOUR_API_KEY"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

def listen(results):
    with requests.get(
        "https://hub.oxen.ai/api/events",
        headers=HEADERS,
        stream=True,
    ) as resp:
        event_name = None
        for line in resp.iter_lines(decode_unicode=True):
            if line is None or line == "":
                event_name = None
                continue
            if line.startswith(":"):
                continue  # keep-alive comment
            if line.startswith("event:"):
                event_name = line[6:].strip()
            elif line.startswith("data:") and event_name == "media_generation_completed":
                payload = json.loads(line[5:].strip())
                results[payload["generation_id"]] = payload

# Start listening BEFORE enqueuing
results = {}
threading.Thread(target=listen, args=(results,), daemon=True).start()
print("SSE listener connected, waiting for events...")

# Enqueue
prompt = "A red cube"
model = "black-forest-labs-flux-2-klein-4b"
print(f"\nEnqueuing {model}")
print(f"  prompt: \"{prompt}\"")
resp = requests.post(
    "https://hub.oxen.ai/api/ai/queue",
    headers={**HEADERS, "Content-Type": "application/json"},
    json={
        "model": model,
        "prompt": prompt,
    },
)
gen_id = resp.json()["generations"][0]["generation_id"]
print(f"  generation_id: {gen_id}")

# Wait for completion event
start = time.time()
while gen_id not in results:
    elapsed = time.time() - start
    print(f"  Waiting for SSE completion event... ({elapsed:.1f}s)")
    time.sleep(2)

elapsed = time.time() - start
event = results[gen_id]
if event["status"] == "succeeded":
    print(f"\nGeneration succeeded in {elapsed:.1f}s")
    print(f"  URL: {event['url']}")
else:
    print(f"\nGeneration failed after {elapsed:.1f}s")
    print(f"  Error: {event['error']}")

Terminal states without events

media_generation_completed does not fire for cancelled generations (you called DELETE /ai/queue/:id). You can still retrieve the final status of any generation via GET /ai/queue/:id.

Examples

Batch image generation with polling

import requests
import time

API_KEY = "YOUR_API_KEY"
HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

# Enqueue 4 images
model = "black-forest-labs-flux-2-klein-4b"
prompt = "Abstract geometric pattern in blue and gold"
print(f"Enqueuing 4 generations of {model}")
print(f"  prompt: \"{prompt}\"")
response = requests.post(
    "https://hub.oxen.ai/api/ai/queue",
    headers=HEADERS,
    json={
        "model": model,
        "prompt": prompt,
        "num_generations": 4,
    },
)
gen_ids = [g["generation_id"] for g in response.json()["generations"]]
for gid in gen_ids:
    print(f"  generation_id: {gid}")

# Poll individual generations until all reach a terminal status
terminal = {"succeeded", "failed", "cancelled"}
start = time.time()
while True:
    statuses = {}
    for gid in gen_ids:
        resp = requests.get(
            f"https://hub.oxen.ai/api/ai/queue/{gid}",
            headers=HEADERS,
        ).json()
        statuses[gid] = resp["status"]
    done = sum(1 for s in statuses.values() if s in terminal)
    elapsed = time.time() - start
    print(f"  [{elapsed:5.1f}s] {done}/{len(gen_ids)} complete")
    if done == len(gen_ids):
        break
    time.sleep(5)

elapsed = time.time() - start
print(f"\nAll {len(gen_ids)} images generated in {elapsed:.1f}s!")
for gid, s in statuses.items():
    print(f"  {gid}: {s}")

Async video generation

import requests

response = requests.post(
    "https://hub.oxen.ai/api/ai/queue",
    headers={
        "Authorization": "Bearer YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={
        "model": "kling-video-o3-pro-reference-to-video",
        "multi_prompt": [
            {"prompt": "Aerial view of waves crashing on a rocky shore", "duration": 5},
            {"prompt": "Camera pulls back to reveal the full coastline", "duration": 5},
        ],
        "aspect_ratio": "16:9",
    },
)

print(response.json())

Poll a single generation by ID

import requests
import time

API_KEY = "YOUR_API_KEY"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

# After enqueuing, grab a generation ID
generation_id = "YOUR_GENERATION_ID"
print(f"Polling generation {generation_id}")

terminal = {"succeeded", "failed", "cancelled"}
start = time.time()
poll_count = 0
while True:
    poll_count += 1
    data = requests.get(
        f"https://hub.oxen.ai/api/ai/queue/{generation_id}",
        headers=HEADERS,
    ).json()
    elapsed = time.time() - start
    status = data["status"]
    if status in terminal:
        print(f"  [{elapsed:5.1f}s] Generation {status}")
        if status == "succeeded":
            print(f"  result_url: {data['result_url']}")
        elif status == "failed":
            print(f"  error: {data['error_message']}")
        break
    print(f"  [{elapsed:5.1f}s] Poll #{poll_count}{status} ({data['model_name']}, {data['media_type']})")
    time.sleep(10)

End-to-end: enqueue, wait for SSE, download

Python
import json
import queue
import requests
import threading
import time

API_KEY = "YOUR_API_KEY"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

def listen(events):
    with requests.get(
        "https://hub.oxen.ai/api/events",
        headers=HEADERS,
        stream=True,
    ) as resp:
        event_name = None
        for line in resp.iter_lines(decode_unicode=True):
            if not line:
                event_name = None
                continue
            if line.startswith(":"):
                continue
            if line.startswith("event:"):
                event_name = line[6:].strip()
            elif line.startswith("data:") and event_name == "media_generation_completed":
                events.put(json.loads(line[5:]))

# Subscribe before enqueuing so no events are missed
events = queue.Queue()
threading.Thread(target=listen, args=(events,), daemon=True).start()
print("SSE listener connected, waiting for events...")

# Enqueue
model = "black-forest-labs-flux-2-klein-4b"
prompt = "A cathedral in the clouds"
print(f"\nEnqueuing {model}")
print(f"  prompt: \"{prompt}\"")
resp = requests.post(
    "https://hub.oxen.ai/api/ai/queue",
    headers={**HEADERS, "Content-Type": "application/json"},
    json={
        "model": model,
        "prompt": prompt,
    },
).json()
gen_id = resp["generations"][0]["generation_id"]
print(f"  generation_id: {gen_id}")

# Wait for the matching completion event
start = time.time()
print(f"\nWaiting for SSE completion event...")
while True:
    try:
        event = events.get(timeout=3)
        if event["generation_id"] == gen_id:
            break
        print(f"  [{time.time() - start:5.1f}s] Received event for different generation, skipping...")
    except queue.Empty:
        print(f"  [{time.time() - start:5.1f}s] Still waiting...")

elapsed = time.time() - start
if event["status"] == "succeeded":
    print(f"\nGeneration succeeded in {elapsed:.1f}s")
    print(f"  Downloading {event['url']}")
    with open("output.png", "wb") as f:
        f.write(requests.get(event["url"]).content)
    print("  Saved output.png")
else:
    print(f"\nGeneration failed after {elapsed:.1f}s")
    print(f"  Error: {event['error']}")

Errors

ConditionError
num_generations out of range"num_generations must be an integer between 1 and 4"
Model not found"Model not found: <name>"
Text-only model":unsupported_media_type"
404 on GET/DELETEGeneration ID does not exist