> ## 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.

# Async Queue

> Enqueue image and video generation jobs that process in the background

## 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](#completion-events).

***

## Enqueue

```
POST /api/ai/queue
```

Submit an async image or video generation job.

### Required Parameters

| Parameter | Type   | Description                                                     |
| --------- | ------ | --------------------------------------------------------------- |
| `model`   | string | Must be an image or video model. Text-only models are rejected. |

### Additional Parameters

| Parameter          | Type    | Default       | Description                                                                                              |
| ------------------ | ------- | ------------- | -------------------------------------------------------------------------------------------------------- |
| `num_generations`  | integer | `1`           | How many generations to enqueue per request. Range: 1-4. Call the endpoint multiple times to queue more. |
| `target_namespace` | string  | your username | Namespace 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

```json theme={null}
{
  "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

| Parameter    | Type   | Required | Description                                                                                                                          |
| ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `namespace`  | string | no       | Namespace to query. Defaults to the authenticated user.                                                                              |
| `model`      | string | no       | Filter by model name.                                                                                                                |
| `status`     | string | no       | Filter by status: `queued`, `processing`, `succeeded`, `failed`, or `cancelled`. When omitted, only active generations are returned. |
| `media_type` | string | no       | Filter by `image` or `video`.                                                                                                        |
| `repo`       | string | no       | Filter by target repository name.                                                                                                    |
| `folder`     | string | no       | Filter by target directory.                                                                                                          |

### Response

```json theme={null}
{
  "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
    }
  ]
}
```

| Field           | Always Present | Description                                                             |
| --------------- | -------------- | ----------------------------------------------------------------------- |
| `count`         | yes            | Number of generations in the response                                   |
| `generation_id` | yes            | Unique ID for this generation                                           |
| `model_name`    | yes            | Model name                                                              |
| `prompt`        | yes            | Text prompt (from original request parameters)                          |
| `media_type`    | yes            | `"image"` or `"video"`                                                  |
| `status`        | yes            | `"queued"`, `"processing"`, `"succeeded"`, `"failed"`, or `"cancelled"` |
| `result_url`    | yes            | Output file URL when succeeded, otherwise `null`                        |
| `error_message` | yes            | Error details when failed, otherwise `null`                             |
| `enqueued_at`   | yes            | Unix timestamp when the job was enqueued                                |
| `started_at`    | yes            | Unix timestamp when processing began, or `null`                         |
| `completed_at`  | yes            | Unix timestamp when the job reached a terminal state, or `null`         |
| `seed`          | if submitted   | Random seed                                                             |
| `aspect_ratio`  | if submitted   | Aspect ratio                                                            |
| `duration`      | if submitted   | Video 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

| Parameter       | Type          | Required | Description                                         |
| --------------- | ------------- | -------- | --------------------------------------------------- |
| `generation_id` | string (UUID) | **yes**  | The generation ID returned by the enqueue endpoint. |

### Response (in progress)

```json theme={null}
{
  "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)

```json theme={null}
{
  "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)

```json theme={null}
{
  "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:

```json theme={null}
{
  "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

| Parameter       | Type          | Required | Description                  |
| --------------- | ------------- | -------- | ---------------------------- |
| `generation_id` | string (UUID) | **yes**  | The generation ID to cancel. |

### Response (success)

```json theme={null}
{
  "status": "success",
  "generation_id": "bb8f5eb7-361e-4e13-ab73-67457bc8057e"
}
```

### Response (not found)

Returns 404 when the generation ID does not exist:

```json theme={null}
{
  "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

| Field           | succeeded     | failed     | Description                                                     |
| --------------- | ------------- | ---------- | --------------------------------------------------------------- |
| `generation_id` | yes           | yes        | Matches the ID returned by `POST /ai/queue`                     |
| `status`        | `"succeeded"` | `"failed"` | Only these two values appear                                    |
| `media_type`    | yes           | yes        | `"image"` or `"video"`                                          |
| `model`         | yes           | no         | Model name                                                      |
| `url`           | yes           | no         | Presigned URL to the output file. Expires after a limited time. |
| `error`         | no            | yes        | Human-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

<CodeGroup>
  ```python Python theme={null}
  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']}")
  ```

  ```bash cURL theme={null}
  curl -N -H "Authorization: Bearer $OXEN_API_KEY" \
    https://hub.oxen.ai/api/events
  ```
</CodeGroup>

### 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

<CodeGroup>
  ```python Python theme={null}
  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}")
  ```

  ```bash cURL theme={null}
  # Enqueue 4 images
  curl -s -X POST https://hub.oxen.ai/api/ai/queue \
    -H "Authorization: Bearer $OXEN_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "black-forest-labs-flux-2-klein-4b",
      "prompt": "Abstract geometric pattern in blue and gold",
      "num_generations": 4
    }'

  # Poll until all active generations are done
  while true; do
    STATUS=$(curl -s -H "Authorization: Bearer $OXEN_API_KEY" \
      "https://hub.oxen.ai/api/ai/queue?model=black-forest-labs-flux-2-klein-4b")
    COUNT=$(echo "$STATUS" | python3 -c "import json,sys; print(json.load(sys.stdin)['count'])")
    echo "Active: $COUNT"
    [ "$COUNT" -eq 0 ] && break
    sleep 5
  done

  echo "All images generated!"
  ```
</CodeGroup>

### Async video generation

<CodeGroup>
  ```python Python theme={null}
  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())
  ```

  ```bash cURL theme={null}
  curl -X POST https://hub.oxen.ai/api/ai/queue \
    -H "Authorization: Bearer $OXEN_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "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"
    }'
  ```
</CodeGroup>

### Poll a single generation by ID

<CodeGroup>
  ```python Python theme={null}
  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)
  ```

  ```bash cURL theme={null}
  # Check a specific generation
  curl -H "Authorization: Bearer $OXEN_API_KEY" \
    "https://hub.oxen.ai/api/ai/queue/$GENERATION_ID"

  # Cancel a generation
  curl -X DELETE -H "Authorization: Bearer $OXEN_API_KEY" \
    "https://hub.oxen.ai/api/ai/queue/$GENERATION_ID"
  ```
</CodeGroup>

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

```python Python theme={null}
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

| Condition                      | Error                                                  |
| ------------------------------ | ------------------------------------------------------ |
| `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/DELETE              | Generation ID does not exist                           |
