> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wondercraft.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Image & Video Generation

> Generate images and videos via API

This guide shows how to use Python to generate images and videos, optionally from your own reference images and clips. For the full request and response fields, see the [Images](/api-reference/endpoint/generate_image) and [Videos](/api-reference/endpoint/generate_video) API reference.

Every generation follows the same steps:

1. Optionally upload reference images or videos. Each upload returns an ID.
2. Start a job with `POST /image` or `POST /video`. The response contains a `job_id` and the credits the job will cost.
3. Poll `GET /image/{job_id}` or `GET /video/{job_id}` until the job has succeeded, then download the result.

## Prerequisites

* Python 3.6+
* `requests` library (`pip install requests`)
* An API key, passed in the `X-API-KEY` header. See [Authentication](/api-reference/introduction#authentication) for how to create one.

```python theme={null}
import time
import requests

API_BASE_URL = "https://api.wondercraft.ai/v1"
HEADERS = {"X-API-KEY": "YOUR_API_KEY"}


def wait_for(job_path):
    """Poll a job every 5 seconds until it succeeds or fails."""
    while True:
        job = requests.get(f"{API_BASE_URL}{job_path}", headers=HEADERS).json()
        if job["status"] in ("succeeded", "failed"):
            return job
        print(f"Status: {job['status']}")
        time.sleep(5)
```

## Choosing a model

List the available models, with the resolutions, aspect ratios and credits each one supports. The list changes as models are added, so read model IDs from here instead of hard-coding them.

```python theme={null}
image_models = requests.get(f"{API_BASE_URL}/image/models", headers=HEADERS).json()["models"]
video_models = requests.get(f"{API_BASE_URL}/video/models", headers=HEADERS).json()["models"]

for model in image_models:
    print(model["id"], model["credits_by_resolution"])  # credits per image at each resolution

for model in video_models:
    print(model["id"], model["modes"], [(r["label"], r["credits_per_second"]) for r in model["resolutions"]])
```

Resolutions are one of `480p`, `720p`, `1080p`, `2k` or `4k`, and each model lists the ones it supports. Video models also return a `label` for each resolution with the size actually rendered. For example, a model whose lowest tier renders at 360p lists it as `480p` with the label `360p`.

When you leave out `resolution`, `aspect_ratio` or `duration`, the model's defaults are used.

## Generating an image

```python theme={null}
response = requests.post(f"{API_BASE_URL}/image", headers=HEADERS, json={
    "model_id": "seedream-5-lite",
    "prompt": "A grey French bulldog swimming, ukiyo-e style, with the Great Wave off Kanagawa behind it",
    "resolution": "2k",
    "aspect_ratio": "16:9",
    "number_of_images": 1,
})
response.raise_for_status()
job = response.json()
print(f"Job {job['job_id']} will cost {job['estimated_credits']} credits")

result = wait_for(f"/image/{job['job_id']}")
if result["status"] == "succeeded":
    image = result["images"][0]
    print(image["url"])  # valid for 24 hours; poll the job again for a fresh link
else:
    print(f"Generation failed: {result['error']}")
```

### Editing and combining images

Upload images first, then pass their IDs in `reference_image_ids`. The first image is the one being edited; the rest are extra references. Refer to them in the prompt as `@Image 1`, `@Image 2` and so on, in the same order. Only models with `supports_reference_images` accept references.

```python theme={null}
def upload_image(path, mimetype):
    with open(path, "rb") as file:
        response = requests.post(f"{API_BASE_URL}/image/upload", headers=HEADERS,
                                 files={"image": (path, file, mimetype)})
    response.raise_for_status()
    return response.json()["image_id"]


dog_id = upload_image("dog.png", "image/png")
response = requests.post(f"{API_BASE_URL}/image", headers=HEADERS, json={
    "model_id": "nano-banana-2",
    "prompt": "Turn @Image 1 into an ukiyo-e woodblock print",
    "reference_image_ids": [dog_id],
})
```

Uploads accept PNG, JPEG or WebP images up to 20 MB. Images generated by an earlier job can be used as references too: use the `image_id` from the job result.

## Generating a video

The kind of video is set by which inputs you send. Each model lists the modes it supports in `modes`.

| Mode | Send | Example use |
| - | - | - |
| `text` | only a prompt | Generate a clip from a description |
| `image` | `start_frame_image_id` | Animate an image |
| `frames` | `start_frame_image_id` and `end_frame_image_id` | Move from one image to another |
| `references` | `reference_image_ids` and/or `reference_video_ids` | Keep a subject or copy a motion or style |
| `video_to_video` | `source_video_id` | Edit or restyle an existing clip |

Send inputs for one mode only; mixing them returns a 422.

### Text to video

```python theme={null}
response = requests.post(f"{API_BASE_URL}/video", headers=HEADERS, json={
    "model_id": "minimax-h3",
    "prompt": "A grey French bulldog swimming through ukiyo-e waves",
    "resolution": "480p",
    "duration": 5,
    "aspect_ratio": "16:9",
})
response.raise_for_status()
job = response.json()

result = wait_for(f"/video/{job['job_id']}")
print(result["url"] if result["status"] == "succeeded" else result["error"])
```

### Animating an image

Pass an uploaded image, or the `image_id` of a generated image, as the start frame. Add `end_frame_image_id` to control where the clip ends.

```python theme={null}
response = requests.post(f"{API_BASE_URL}/video", headers=HEADERS, json={
    "model_id": "minimax-h3",
    "prompt": "The bulldog dives into the wave",
    "start_frame_image_id": image["image_id"],  # from the image job above
})
```

### Using reference images and videos

Upload reference clips with `POST /video/upload`, then refer to every input in the prompt by type and position: `@Image 1`, `@Video 1`.

```python theme={null}
def upload_video(path, mimetype="video/mp4"):
    with open(path, "rb") as file:
        response = requests.post(f"{API_BASE_URL}/video/upload", headers=HEADERS,
                                 files={"video": (path, file, mimetype)})
    response.raise_for_status()
    return response.json()["video_id"]


clip_id = upload_video("swim.mp4")
response = requests.post(f"{API_BASE_URL}/video", headers=HEADERS, json={
    "model_id": "seedance-2",
    "prompt": "The dog from @Image 1 swimming with the motion of @Video 1, in ukiyo-e style",
    "reference_image_ids": [dog_id],
    "reference_video_ids": [clip_id],
})
```

Video uploads accept MP4 or MOV files up to 100 MB and 60 seconds. Each model limits how many references it takes (`max_reference_images`, `max_reference_videos`) and how long reference clips may be. Requests outside a limit return a 400 that names it.

### Editing a video

Pass the clip to edit as `source_video_id`. For models with `output_duration_follows_source`, the result is as long as the source clip, so leave out `duration`. You're charged for the clip's length.

```python theme={null}
response = requests.post(f"{API_BASE_URL}/video", headers=HEADERS, json={
    "model_id": "gemini-omni-flash",
    "prompt": "Restyle this as an ukiyo-e woodblock print with the Great Wave in the background",
    "source_video_id": clip_id,
})
```

## Credits

* Images are charged per generated image, at the price for the chosen resolution (`credits_by_resolution`).
* Videos are charged per second of generated video, at the price for the chosen resolution (`credits_per_second`).
* Every create request returns `estimated_credits`. Credits are checked before the job starts and charged once the result is ready.
* Uploads are free.

## Error handling

| Status | Meaning |
| - | - |
| 400 | The model, mode, resolution, duration or aspect ratio isn't supported, an upload or clip is the wrong type or length, or the prompt refers to inputs the request doesn't include. The `detail` says which. |
| 401 / 403 | The API key is missing or invalid, or the workspace has no active paid plan. |
| 402 | Not enough credits, or the model isn't available on your plan. |
| 404 | An image, video or job doesn't exist, or wasn't created with your API key. |
| 413 | The uploaded file is too large. |
| 422 | The request body is invalid, for example it mixes inputs from different video modes. |

Image and video jobs don't return a 429 when you start many at once: they queue and start as capacity frees up. A job that fails returns `status: "failed"` with a readable `error`, and isn't charged.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.