> ## Documentation Index
> Fetch the complete documentation index at: https://oxenai-eric-revamp-getting-started.mintlify.site/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:

* Generate up to 4 images or videos in parallel
* Avoid long-lived HTTP connections
* Build progress-tracking UIs

## Workflow

```
1. POST /ai/queue                       → Get generation IDs
2. GET  /ai/queue  or  /ai/queue/:id    → Poll until done
3. Results accessible via URLs in your account
```

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 run in parallel. Range: 1-4. |
| `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": "processing"},
    {"generation_id": "e9bede09-cd0e-46cf-bbcd-cb1a50099351", "status": "processing"}
  ]
}
```

### 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 fails silently (it disappears from the queue without producing a result). To validate parameters and get immediate error feedback, use `/ai/images/generate` or `/ai/videos/generate` instead.

***

## List Generations

```
GET /api/ai/queue
```

Lists all in-progress generations. Only actively-processing generations are returned; completed, failed, or cancelled generations are not retained.

### Query Parameters

| Parameter   | Type   | Required | Description                                             |
| ----------- | ------ | -------- | ------------------------------------------------------- |
| `namespace` | string | no       | Namespace to query. Defaults to the authenticated user. |
| `model`     | string | no       | Filter by model name.                                   |

### Response (jobs in progress)

```json theme={null}
{
  "count": 2,
  "generations": [
    {
      "generation_id": "7cf9b23a-1234-5678-9abc-def012345678",
      "model": "kling-video-o3-pro-reference-to-video",
      "prompt": "An astronaut walking on Mars",
      "media_type": "video",
      "enqueued_at": 1775091431,
      "aspect_ratio": "16:9",
      "duration": 10
    },
    {
      "generation_id": "bb8f5eb7-361e-4e13-ab73-67457bc8057e",
      "model": "black-forest-labs-flux-2-klein-4b",
      "prompt": "Abstract geometric pattern in blue and gold",
      "media_type": "image",
      "enqueued_at": 1775091440
    }
  ]
}
```

| Field           | Always Present | Description                              |
| --------------- | -------------- | ---------------------------------------- |
| `count`         | yes            | Number of in-progress generations        |
| `generation_id` | yes            | Unique ID for this generation            |
| `model`         | yes            | Model name                               |
| `prompt`        | yes            | Text prompt                              |
| `media_type`    | yes            | `"image"` or `"video"`                   |
| `enqueued_at`   | yes            | Unix timestamp when the job was enqueued |
| `seed`          | if submitted   | Random seed                              |
| `aspect_ratio`  | if submitted   | Aspect ratio                             |
| `duration`      | if submitted   | Video duration                           |

### Response (all jobs complete)

```json theme={null}
{
  "count": 0,
  "generations": []
}
```

### How Completion Works

When a generation finishes, it is removed from the queue. There is no "completed" status on this endpoint. Generations are either in the list (still processing) or gone (done). Poll until `count` reaches 0.

The output file URL is not returned by this endpoint. See [Completion Events](#completion-events) for receiving URLs via SSE.

### 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.
* Generations expire after 24 hours regardless of completion.

***

## Get Generation

```
GET /api/ai/queue/:generation_id
```

Retrieves metadata for a single in-progress generation.

### 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": "kling-video-o3-pro-reference-to-video",
  "prompt": "An astronaut walking on Mars",
  "media_type": "video",
  "enqueued_at": 1775091431,
  "aspect_ratio": "16:9",
  "duration": 10
}
```

### Response (completed or not found)

Returns 404 when the generation has reached a terminal state (completed, failed, cancelled, or expired after 24h):

```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 (already completed or not found)

Returns 404:

```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 processing. Once a generation completes and leaves the queue, the DELETE endpoint returns 404.

***

## 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`)
* Expired generations (24h TTL with no worker pickup)

***

## 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,
      },
  )
  generations = response.json()["generations"]
  for gen in generations:
      print(f"  generation_id: {gen['generation_id']}")

  # Poll until done
  start = time.time()
  while True:
      status = requests.get(
          "https://hub.oxen.ai/api/ai/queue",
          headers=HEADERS,
          params={"model": model},
      ).json()
      elapsed = time.time() - start
      remaining = status["count"]
      completed = len(generations) - remaining
      print(f"  [{elapsed:5.1f}s] {completed}/{len(generations)} complete, {remaining} remaining...")
      if remaining == 0:
          break
      time.sleep(5)

  elapsed = time.time() - start
  print(f"\nAll {len(generations)} images generated in {elapsed:.1f}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 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 "Remaining: $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}")

  start = time.time()
  poll_count = 0
  while True:
      poll_count += 1
      resp = requests.get(
          f"https://hub.oxen.ai/api/ai/queue/{generation_id}",
          headers=HEADERS,
      )
      elapsed = time.time() - start
      if resp.status_code == 404:
          print(f"  [{elapsed:5.1f}s] Generation complete (or cancelled/expired)")
          break
      data = resp.json()
      print(f"  [{elapsed:5.1f}s] Poll #{poll_count} — still processing ({data['model']}, {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 completed, cancelled, expired, or doesn't exist |
