# LumixAI Image and Video API Implementation Guide

Purpose: concise implementation guide for AI agents integrating LumixAI model lookup, pricing catalogs, image generation, video generation, polling, and download flows.

Base API origin:

```text
https://api.lumixai.live
```

API base URL:

```text
https://api.lumixai.live/v1
```

Server-side env:

```bash
LUMIXAI_API_ORIGIN=https://api.lumixai.live
LUMIXAI_API_KEY=YOUR_LUMIXAI_API_KEY
```

Do not expose `LUMIXAI_API_KEY` in browser code. Call LumixAI from your backend or server-side agent.

## Flow

1. Call `GET /v1/models` or `GET /v1/models/catalog`.
2. Choose exact `model.slug`.
3. Use chosen model `modality`, `aspect_ratios`, `resolutions`, `max_resolution`, and `pricing_tiers`.
4. For image: call `POST /v1/images/jobs` with `X-API-Key`.
5. For video: call `GET /v1/videos/models` for live video capabilities, then call `POST /v1/videos/jobs` with `X-API-Key`.
6. Poll image jobs with `GET /v1/images/jobs/{id}` or video jobs with `GET /v1/videos/jobs/{id}` until `status` is `done` or `error`.
7. Read `data[0].download_url` when done.
8. Download file with `GET /v1/images/results/{id}/download` or `GET /v1/videos/results/{id}/download` using same `X-API-Key`.
9. Save generated binary in your own storage if long-term access is needed.

## Authentication

### Public model endpoint

No auth:

```text
GET /v1/models
GET /v1/models/catalog
GET /v1/models/credits
GET /v1/models/credits/catalog
GET /v1/videos/models
```

### Generate/download endpoints

Use API key header:

```http
X-API-Key: YOUR_LUMIXAI_API_KEY
```

Protected endpoints in this guide:

```text
POST /v1/images/jobs
GET  /v1/images/jobs/{id}
GET  /v1/images/results/{id}/download
POST /v1/images/generations
POST /v1/images/quote
POST /v1/videos/jobs
GET  /v1/videos/jobs/{id}
GET  /v1/videos/results/{id}/download
```

## API Details

### 1. Get Models

```http
GET /v1/models
```

Auth: none.

Use this before every implementation. Do not hard-code model capabilities.

```bash
curl --location 'https://api.lumixai.live/v1/models'
```

Response shape:

```json
{
  "data": [
    {
      "id": "model-id",
      "slug": "imagen-nano-banana-2-flash",
      "display_name": "Nano Banana 2 Flash",
      "modality": "image",
      "aspect_ratios": ["1:1", "2:3", "3:2", "4:3", "3:4", "5:4", "4:5", "16:9", "9:16", "21:9"],
      "aspectRatios": ["1:1", "2:3", "3:2", "4:3", "3:4", "5:4", "4:5", "16:9", "9:16", "21:9"],
      "default_aspect_ratio": "1:1",
      "defaultAspectRatio": "1:1",
      "resolutions": ["1k", "2k", "4k"],
      "default_resolution": "1k",
      "max_resolution": "2k",
      "maxRes": "2k",
      "group": "Google",
      "timing": "~29s",
      "unlimited": true,
      "max_n": 1,
      "image_credit_micros_per_unit": 5000000,
      "pricing_tiers": [
        {
          "name": "Resolution 1k",
          "label": "1k",
          "resolution": "1k",
          "billing_unit": "image",
          "credits": "5",
          "credits_micros": 5000000
        }
      ],
      "public": true,
      "active": true
    }
  ]
}
```

Implementation rules:

- Use `slug` as request `model`.
- Use `modality` to split image and video UI.
- Use `display_name` only for UI labels.
- Use `aspect_ratios` for valid aspect ratio choices.
- Use `default_aspect_ratio` as fallback.
- Use `resolutions` for valid resolution choices.
- Use `default_resolution` as fallback.
- Respect `max_resolution`; backend also caps requests above max before pricing and generation.
- Use `pricing_tiers` for displayed costs. Image tiers are credits per output. Video tiers are credits per second.
- Do not invent or fallback video prices from `image_credit_micros_per_unit`; if video tier is missing, show unconfigured state and block generation.
- Do not invent unsupported pixel sizes such as `1024x1024` unless backend returns them.

Optional filters and grouped catalog:

```http
GET /v1/models?category=image
GET /v1/models?category=video
GET /v1/models?category=all
GET /v1/models/catalog
```

Grouped response shape:

```json
{
  "data": {
    "image": [
      {
        "slug": "imagen-nano-banana-2-flash",
        "display_name": "Nano Banana 2 Flash",
        "modality": "image",
        "resolutions": ["1k", "2k", "4k"],
        "pricing_tiers": [
          { "label": "1k", "resolution": "1k", "billing_unit": "image", "credits": "5", "credits_micros": 5000000 }
        ]
      }
    ],
    "video": [
      {
        "slug": "google-veo3_1-lite",
        "display_name": "Google Veo 3.1 Lite",
        "modality": "video",
        "resolutions": ["720p", "1080p"],
        "pricing_tiers": [
          { "label": "720p", "resolution": "720p", "billing_unit": "second", "credits": "25", "credits_micros": 25000000 }
        ]
      }
    ]
  }
}
```

Credit catalogs:

```http
GET /v1/models/credits
GET /v1/models/credits?category=image
GET /v1/models/credits?category=video
GET /v1/models/credits/catalog
```

Use credit catalogs for pricing tables when you only need model cost summaries.

### 1b. Get Video Model Capabilities

```http
GET /v1/videos/models
```

Auth: none.

Use this endpoint before creating video jobs. It exposes live provider capabilities that may change independently from pricing metadata.

```bash
curl --location 'https://api.lumixai.live/v1/videos/models'
```

Response shape:

```json
{
  "data": [
    {
      "slug": "google-veo3_1-lite",
      "name": "Google Veo 3.1 Lite",
      "api": "google",
      "model": "veo3_1",
      "mode": "veo3_1_lite",
      "resolutions": ["720p", "1080p"],
      "durations": [4, 6, 8],
      "aspectRatios": ["16:9", "9:16"],
      "sound": true,
      "i2v": true,
      "refs": ["startFrame"]
    }
  ]
}
```

Implementation rules:

- Match `slug` with `/v1/models?category=video` pricing data.
- Use `aspectRatios` and `resolutions` to build valid controls.
- Do not list duration option chips from this endpoint; accept a duration in seconds and calculate cost from `/v1/models?category=video` `pricing_tiers` as `credits_micros * duration`.
- Only send `withSoundEffects` when `sound` is true.
- Only send `image`, `imageUrl`, or `refCreationId` when `i2v` or `refs` indicates reference support.

### 2. Create Async Image Job

```http
POST /v1/images/jobs
```

Auth: `X-API-Key` required.

Use this endpoint for production apps and AI agents.

Request body:

```json
{
  "model": "imagen-nano-banana-2-flash",
  "prompt": "premium product render on glass, studio lighting",
  "aspectRatio": "16:9",
  "resolution": "2k"
}
```

Fields:

| Field | Type | Required | Notes |
|---|---:|---:|---|
| `model` | string | yes | Exact slug from `GET /v1/models`. |
| `prompt` | string | yes | Text prompt. For reference image flow, describe the desired output normally. |
| `aspectRatio` | string | no | Must be supported by selected model. |
| `resolution` | string | no | `1k`, `2k`, `4k`; backend caps to selected model max. |
| `references` | array | no | Up to 5 inline reference objects. Sent directly to Spaces img2img as `images[]` / `imageUrls[]`. |
| `seed` | integer | no | Repeatable seed when supported. |
| `timeoutMs` | integer | no | Upstream timeout in milliseconds. |

Request:

```bash
curl --location 'https://api.lumixai.live/v1/images/jobs' \
  -H 'X-API-Key: YOUR_LUMIXAI_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "imagen-nano-banana-2-flash",
    "prompt": "premium product render on glass, studio lighting",
    "aspectRatio": "16:9",
    "resolution": "2k"
  }'
```

Success: HTTP `202`.

Response shape:

```json
{
  "id": "9f5b5e70-839b-46f5-a31c-6cf7d7e92a30",
  "status": "pending",
  "created": 1778956800,
  "usage": {
    "image_count": 1
  }
}
```

### 3. Create Async Image Job With Inline Reference

Still uses:

```http
POST /v1/images/jobs
```

Max references in v1: 5.

Reference object:

```json
{
  "image": "data:image/png;base64,BASE64_BYTES"
}
```

Example request:

```bash
curl --location 'https://api.lumixai.live/v1/images/jobs' \
  -H 'X-API-Key: YOUR_LUMIXAI_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "flux-kontext",
    "prompt": "a cute cat catching a mouse",
    "aspectRatio": "1:1",
    "resolution": "2k",
    "references": [
      {
        "image": "data:image/png;base64,BASE64_BYTES"
      }
    ]
  }'
```

Reference rules:

- Prefer data URI base64 for compatibility.
- Prompt should describe the desired output normally, for example `a cute cat` or `a cat catching a mouse`.
- Send up to 5 references in v1. Data URI references are forwarded as `images[]`; URL references are forwarded as `imageUrls[]`.
- Each reference object must include only one of `image` or `imageUrl`.
- If using URL form, make sure URL is public and fetchable:

```json
{
  "imageUrl": "https://example.com/source.png"
}
```

### 4. Poll Image Job

```http
GET /v1/images/jobs/{id}
```

Auth: `X-API-Key` required.

Request:

```bash
curl --location 'https://api.lumixai.live/v1/images/jobs/9f5b5e70-839b-46f5-a31c-6cf7d7e92a30' \
  -H 'X-API-Key: YOUR_LUMIXAI_API_KEY'
```

Pending/running response:

```json
{
  "id": "9f5b5e70-839b-46f5-a31c-6cf7d7e92a30",
  "status": "running",
  "created": 1778956800,
  "usage": {
    "image_count": 1
  }
}
```

Done response:

```json
{
  "id": "9f5b5e70-839b-46f5-a31c-6cf7d7e92a30",
  "status": "done",
  "created": 1778956800,
  "completed": 1778956842,
  "data": [
    {
      "download_url": "/v1/images/results/9f5b5e70-839b-46f5-a31c-6cf7d7e92a30/download"
    }
  ],
  "usage": {
    "image_count": 1
  }
}
```

Error response:

```json
{
  "id": "9f5b5e70-839b-46f5-a31c-6cf7d7e92a30",
  "status": "error",
  "created": 1778956800,
  "completed": 1778956842,
  "error": "Generate failed. Any reserved credits will be refunded automatically. The prompt may contain sensitive content that did not pass safety checks.",
  "usage": {
    "image_count": 1
  }
}
```

Polling rules:

- Poll every 2 seconds.
- Continue on `pending` and `running`.
- Stop on `done` or `error`.
- Use a local timeout, commonly 3 to 5 minutes.
- On `done`, read `data[0].download_url`.

### 5. Download Generated Image

```http
GET /v1/images/results/{id}/download
```

Auth: `X-API-Key` required.

Usually use `download_url` returned by polling. If `download_url` is relative, prefix API origin:

```text
https://api.lumixai.live + download_url
```

Request:

```bash
curl --location 'https://api.lumixai.live/v1/images/results/9f5b5e70-839b-46f5-a31c-6cf7d7e92a30/download' \
  -H 'X-API-Key: YOUR_LUMIXAI_API_KEY' \
  -L \
  -o lumixai-result.png
```

Success response:

```http
HTTP/1.1 200 OK
Content-Type: image/png
Content-Disposition: attachment; filename="lumixai-9f5b5e70-839b-46f5-a31c-6cf7d7e92a30.png"

<binary image stream>
```

Download rules:

- Send same `X-API-Key` used to create/poll job.
- Preserve `Content-Type`.
- Use `Content-Disposition` filename when available.
- Store binary in your own storage if user needs permanent access.

### 6. Quote Image Cost

```http
POST /v1/images/quote
```

Auth: `X-API-Key` required.

Use this endpoint before creating an image job when you need an exact price for the selected model, resolution, aspect ratio, and reference setup.

Request body matches `POST /v1/images/jobs`.

```bash
curl --location 'https://api.lumixai.live/v1/images/quote' \
  -H 'X-API-Key: YOUR_LUMIXAI_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "imagen-nano-banana-2-flash",
    "prompt": "premium product render on glass, studio lighting",
    "aspectRatio": "16:9",
    "resolution": "2k"
  }'
```

### 7. Create Async Video Job

```http
POST /v1/videos/jobs
```

Auth: `X-API-Key` required.

Video generation is async only. Use `GET /v1/videos/models` and `/v1/models?category=video` before selecting controls and pricing.

Text-to-video request body:

```json
{
  "model": "google-veo3_1-lite",
  "prompt": "a cinematic product reveal with soft camera motion",
  "aspectRatio": "16:9",
  "resolution": "720p",
  "duration": 6,
  "withSoundEffects": true
}
```

Fields:

| Field | Type | Required | Notes |
|---|---:|---:|---|
| `model` | string | yes | Exact video slug from `/v1/models?category=video` or `/v1/videos/models`. |
| `prompt` | string | conditional | Required for text-to-video. Optional only if using a supported reference-only workflow. |
| `aspectRatio` | string | no | Must be supported by selected video model. |
| `resolution` | string | no | For example `720p` or `1080p` when supported. |
| `duration` | integer | no | Seconds. Charged as configured credits per second for the selected model and resolution. |
| `withSoundEffects` | boolean | no | Only send when model `sound` is true. |
| `image` | string | no | Data URI or base64 start frame for image-to-video. |
| `imageUrl` | string | no | Public start frame URL for image-to-video. |
| `refCreationId` | string | no | Existing upstream creation id to use as reference/start frame. |
| `images` | string[] | no | Multiple data URI start/reference frames for image-to-video. Max 5 total references. |
| `imageUrls` | string[] | no | Multiple public start/reference frame URLs. Max 5 total references. |
| `refCreationIds` | string[] | no | Multiple existing upstream creation ids. Max 5 total references. |
| `endImage` | string | no | Data URI or base64 end frame. Only send when model `refs` includes `endFrame`. |
| `endImageUrl` | string | no | Public end frame URL. Prefer data URI because upstream CORS can block remote URLs. |
| `endRefCreationId` | string | no | Existing upstream creation id to use as the end frame. |
| `maxRefs` | integer | no | Start/reference cap. Must be between 0 and 5; end frame is separate. |
| `spaceId` | string | no | Magnific Space UUID. |
| `folderRef` | string | no | Folder reference in Magnific. |
| `timeoutMs` | integer | no | Upstream timeout in milliseconds. |

Request:

```bash
curl --location 'https://api.lumixai.live/v1/videos/jobs' \
  -H 'X-API-Key: YOUR_LUMIXAI_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "google-veo3_1-lite",
    "prompt": "a cinematic product reveal with soft camera motion",
    "aspectRatio": "16:9",
    "resolution": "720p",
    "duration": 6,
    "withSoundEffects": true
  }'
```

Image-to-video request:

```bash
curl --location 'https://api.lumixai.live/v1/videos/jobs' \
  -H 'X-API-Key: YOUR_LUMIXAI_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "google-veo3_1-lite",
    "prompt": "animate these product frames with a slow dolly-in",
    "images": [
      "data:image/png;base64,BASE64_BYTES_1",
      "data:image/png;base64,BASE64_BYTES_2"
    ],
    "maxRefs": 5,
    "aspectRatio": "16:9",
    "resolution": "720p",
    "duration": 6
  }'
```

Start/end frame request:

```bash
curl --location 'https://api.lumixai.live/v1/videos/jobs' \
  -H 'X-API-Key: YOUR_LUMIXAI_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "google-veo3_1-lite",
    "prompt": "smooth transition between the two frames",
    "image": "data:image/png;base64,START_FRAME_BYTES",
    "endImage": "data:image/png;base64,END_FRAME_BYTES",
    "aspectRatio": "16:9",
    "resolution": "720p",
    "duration": 6
  }'
```

Reference rules:

- Use `images`, `imageUrls`, or `refCreationIds` when the video model supports image-to-video/reference input.
- Total start/reference inputs across `image`, `imageUrl`, `refCreationId`, `images`, `imageUrls`, and `refCreationIds` must be 5 or fewer.
- Use `endImage`, `endImageUrl`, or `endRefCreationId` only when model `refs` includes `endFrame`; it maps to upstream `last-frame` and does not count against `maxRefs`.
- Single fields remain supported for backward compatibility; prefer arrays for multi-frame workflows.

Success: HTTP `202`.

Response shape:

```json
{
  "id": "2ff3e6c3-6b6f-4b47-b9f2-d64691712e2a",
  "status": "pending",
  "created": 1778956800,
  "usage": {
    "video_count": 6
  }
}
```

### 8. Poll Video Job

```http
GET /v1/videos/jobs/{id}
```

Auth: `X-API-Key` required.

Request:

```bash
curl --location 'https://api.lumixai.live/v1/videos/jobs/2ff3e6c3-6b6f-4b47-b9f2-d64691712e2a' \
  -H 'X-API-Key: YOUR_LUMIXAI_API_KEY'
```

Done response:

```json
{
  "id": "2ff3e6c3-6b6f-4b47-b9f2-d64691712e2a",
  "status": "done",
  "created": 1778956800,
  "completed": 1778956902,
  "data": [
    {
      "download_url": "/v1/videos/results/2ff3e6c3-6b6f-4b47-b9f2-d64691712e2a/download",
      "web_url": "https://magnific.com/video/example"
    }
  ],
  "usage": {
    "video_count": 6
  }
}
```

Error response:

```json
{
  "id": "2ff3e6c3-6b6f-4b47-b9f2-d64691712e2a",
  "status": "error",
  "created": 1778956800,
  "completed": 1778956902,
  "error": "Generate failed. Any reserved credits will be refunded automatically. The prompt may contain sensitive content that did not pass safety checks.",
  "usage": {
    "video_count": 6
  }
}
```

Video polling rules:

- Poll every 2 seconds.
- Continue on `pending` and `running`.
- Stop on `done` or `error`.
- Use a longer local timeout than images, commonly 5 to 10 minutes.
- On `done`, read `data[0].download_url`.
- On `error`, show the error message. Reserved credits are refunded automatically for failed generation.

### 9. Download Generated Video

```http
GET /v1/videos/results/{id}/download
```

Auth: `X-API-Key` required.

Request:

```bash
curl --location 'https://api.lumixai.live/v1/videos/results/2ff3e6c3-6b6f-4b47-b9f2-d64691712e2a/download' \
  -H 'X-API-Key: YOUR_LUMIXAI_API_KEY' \
  -L \
  -o lumixai-result.mp4
```

Success response:

```http
HTTP/1.1 200 OK
Content-Type: video/mp4
Content-Disposition: attachment; filename="lumixai-2ff3e6c3-6b6f-4b47-b9f2-d64691712e2a.mp4"

<binary video stream>
```

### 10. Optional Synchronous Image Generate Endpoint

```http
POST /v1/images/generations
```

Auth: `X-API-Key` required.

This endpoint blocks until generation finishes. Prefer async jobs for apps and AI agents.

Request:

```bash
curl --location 'https://api.lumixai.live/v1/images/generations' \
  -H 'X-API-Key: YOUR_LUMIXAI_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "imagen-nano-banana-2-flash",
    "prompt": "premium product render on glass",
    "aspectRatio": "1:1",
    "resolution": "1k"
  }'
```

Response shape:

```json
{
  "created": 1778956800,
  "data": [
    {
      "download_url": "/v1/images/results/9f5b5e70-839b-46f5-a31c-6cf7d7e92a30/download"
    }
  ],
  "usage": {
    "image_count": 1
  }
}
```

## Full cURL Flow

Image flow:

```bash
export LUMIXAI_API_ORIGIN='https://api.lumixai.live'
export LUMIXAI_API_KEY='YOUR_LUMIXAI_API_KEY'

curl --location "$LUMIXAI_API_ORIGIN/v1/models"

JOB_ID=$(curl --silent --location "$LUMIXAI_API_ORIGIN/v1/images/jobs" \
  -H "X-API-Key: $LUMIXAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "imagen-nano-banana-2-flash",
    "prompt": "premium product render on glass, studio lighting",
    "aspectRatio": "16:9",
    "resolution": "2k"
  }' | jq -r '.id')

echo "JOB_ID=$JOB_ID"

while true; do
  BODY=$(curl --silent --location "$LUMIXAI_API_ORIGIN/v1/images/jobs/$JOB_ID" \
    -H "X-API-Key: $LUMIXAI_API_KEY")
  STATUS=$(echo "$BODY" | jq -r '.status')
  echo "status=$STATUS"

  if [ "$STATUS" = "done" ]; then
    DOWNLOAD_URL=$(echo "$BODY" | jq -r '.data[0].download_url')
    break
  fi

  if [ "$STATUS" = "error" ]; then
    echo "$BODY"
    exit 1
  fi

  sleep 2
done

curl --location "$LUMIXAI_API_ORIGIN$DOWNLOAD_URL" \
  -H "X-API-Key: $LUMIXAI_API_KEY" \
  -L \
  -o lumixai-result.png
```

Video flow:

```bash
export LUMIXAI_API_ORIGIN='https://api.lumixai.live'
export LUMIXAI_API_KEY='YOUR_LUMIXAI_API_KEY'

curl --location "$LUMIXAI_API_ORIGIN/v1/models?category=video"
curl --location "$LUMIXAI_API_ORIGIN/v1/videos/models"

JOB_ID=$(curl --silent --location "$LUMIXAI_API_ORIGIN/v1/videos/jobs" \
  -H "X-API-Key: $LUMIXAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "google-veo3_1-lite",
    "prompt": "a cinematic product reveal with soft camera motion",
    "aspectRatio": "16:9",
    "resolution": "720p",
    "duration": 6,
    "withSoundEffects": true
  }' | jq -r '.id')

echo "VIDEO_JOB_ID=$JOB_ID"

while true; do
  BODY=$(curl --silent --location "$LUMIXAI_API_ORIGIN/v1/videos/jobs/$JOB_ID" \
    -H "X-API-Key: $LUMIXAI_API_KEY")
  STATUS=$(echo "$BODY" | jq -r '.status')
  echo "status=$STATUS"

  if [ "$STATUS" = "done" ]; then
    DOWNLOAD_URL=$(echo "$BODY" | jq -r '.data[0].download_url')
    break
  fi

  if [ "$STATUS" = "error" ]; then
    echo "$BODY"
    exit 1
  fi

  sleep 2
done

curl --location "$LUMIXAI_API_ORIGIN$DOWNLOAD_URL" \
  -H "X-API-Key: $LUMIXAI_API_KEY" \
  -L \
  -o lumixai-result.mp4
```

## Node.js Server-Side Example

```ts
const API_ORIGIN = process.env.LUMIXAI_API_ORIGIN ?? "https://api.lumixai.live";
const API_KEY = process.env.LUMIXAI_API_KEY;

if (!API_KEY) throw new Error("Missing LUMIXAI_API_KEY");

type CreateJobResponse = {
  id: string;
  status: string;
  created: number;
  usage: { image_count: number };
};

type JobStatusResponse = {
  id: string;
  status: "pending" | "running" | "done" | "error";
  created: number;
  completed?: number;
  data?: Array<{ download_url: string }>;
  error?: string;
  usage: { image_count: number };
};

async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
  const res = await fetch(`${API_ORIGIN}${path}`, {
    ...init,
    headers: {
      "X-API-Key": API_KEY,
      "Content-Type": "application/json",
      ...(init.headers ?? {})
    },
    cache: "no-store"
  });

  if (!res.ok) {
    const body = await res.json().catch(() => ({}));
    const type = body?.error?.type ?? "unknown_error";
    const message = body?.error?.message ?? `LumixAI request failed: ${res.status}`;
    throw new Error(`${type}: ${message}`);
  }

  return res.json() as Promise<T>;
}

export async function generateAndDownload() {
  const created = await api<CreateJobResponse>("/v1/images/jobs", {
    method: "POST",
    body: JSON.stringify({
      model: "imagen-nano-banana-2-flash",
      prompt: "premium product render on glass, studio lighting",
      aspectRatio: "16:9",
      resolution: "2k"
    })
  });

  const started = Date.now();
  let job: JobStatusResponse;

  while (true) {
    if (Date.now() - started > 300_000) throw new Error("LumixAI job timed out");

    job = await api<JobStatusResponse>(`/v1/images/jobs/${created.id}`, {
      method: "GET"
    });

    if (job.status === "done") break;
    if (job.status === "error") throw new Error(job.error || "LumixAI job failed");

    await new Promise((resolve) => setTimeout(resolve, 2000));
  }

  const downloadUrl = job.data?.[0]?.download_url;
  if (!downloadUrl) throw new Error("Missing LumixAI download_url");

  const url = downloadUrl.startsWith("http") ? downloadUrl : `${API_ORIGIN}${downloadUrl}`;
  const file = await fetch(url, {
    headers: { "X-API-Key": API_KEY },
    cache: "no-store"
  });

  if (!file.ok) throw new Error(`LumixAI download failed: ${file.status}`);

  return {
    bytes: Buffer.from(await file.arrayBuffer()),
    contentType: file.headers.get("Content-Type") ?? "application/octet-stream",
    filename: filenameFromContentDisposition(file.headers.get("Content-Disposition"))
  };
}

function filenameFromContentDisposition(value: string | null) {
  if (!value) return "lumixai-result.png";
  const match = value.match(/filename="([^"]+)"/i) ?? value.match(/filename=([^;]+)/i);
  return match?.[1]?.trim() ?? "lumixai-result.png";
}
```

## Error Handling

Common response:

```json
{
  "error": {
    "type": "insufficient_credits_error",
    "message": "insufficient credits"
  }
}
```

| HTTP | Type | Action |
|---:|---|---|
| 400 | `validation_error` | Fix payload. Do not retry same payload. |
| 401 | `authentication_error` | Ask for valid API key. |
| 402 | `insufficient_credits_error` | Stop and ask user to add credits. |
| 403 | `permission_denied_error` | Check that the key owner account is active. |
| 404 | `resource_not_found_error` | Check job id, result id, workspace, and key. |
| 429 | `rate_limit_error` | Back off and retry later. |
| 503 | `service_unavailable` | Show generation failed. Reserved credits are refunded automatically; prompt or safety filtering may be involved. Retry with a safer prompt or backoff. |

## Agent Checklist

- Fetch `/v1/models` or `/v1/models/catalog` first.
- For video, also fetch `/v1/videos/models`.
- Use exact model `slug`.
- Split UI and pricing by `modality` (`image` or `video`).
- Use supported `aspectRatio` and `resolution`.
- For video, use supported `duration`, `sound`, and reference capabilities.
- Keep `LUMIXAI_API_KEY` server-side only.
- Use async job flow by default.
- Poll every 2 seconds until terminal state.
- Prefix relative `download_url` with `https://api.lumixai.live`.
- Send `X-API-Key` on create, poll, and download.
- Store downloaded binary yourself if persistent access is needed.
- Do not invent upstream CDN URLs or unsupported pixel sizes.
