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

# Video generation and tasks

> Create, retrieve, download, and remix video tasks.

Video generation uses asynchronous tasks. Save the gateway `id` after creation, query it with credentials in the same user/project scope, and download when complete. Do not substitute a private task ID from the provider dashboard.

As of 2026-10-14, [OpenAI's official guide](https://developers.openai.com/api/docs/guides/video-generation) states that Sora 2 / the Videos API shut down on 2026-09-24. This page documents the compatibility endpoints retained by this project. Confirm an actually available channel or plugin supporting `openai_video` before use. The presence of a Sora plugin or SDK method does not establish upstream availability.

## Task lifecycle

| Status | Application behavior |
| - | - |
| `queued` / `in_progress` | Wait before querying again; bound attempts and total time |
| `completed` | Request content; expiry or download failures can still occur |
| `failed` | Stop polling and inspect task errors/logs |
| `unknown` or an unrecognized value | Stop automation and investigate; never treat it as success |

`progress` is a hint. A value of 100 does not replace `status: completed`. Retrieval may return a snapshot saved by background polling; frequent queries do not guarantee fresher data.

## Python: create, resume, and download

Configure the [Python SDK environment](/en/guides/python). `API_MODEL` must identify a video model your administrator has confirmed is available. Save as `videos.py`. The pinned SDK retains compatibility methods but may emit Sora deprecation notices. It targets your configured gateway and cannot restore a discontinued upstream.

Run `python videos.py create` and save the returned `id`. Replace `task_example` below with that ID. Each `create` makes a new task: never put creation inside a polling loop.

```sh theme={null}
python videos.py status task_example
python videos.py wait task_example
python videos.py download task_example
```

`wait` queries at most 60 times with 5-second intervals by default. Each request also has a 120-second timeout, so this is not a five-minute wall-clock guarantee. Set `--max-polls` to 1–120 attempts. Reaching the limit, exiting, or a network failure does not cancel the server task; resume later with the same ID.

```python theme={null}
import argparse
import json
import os
import re
import time
from contextlib import ExitStack
from pathlib import Path
from openai import OpenAI, APIConnectionError, APIStatusError

parser = argparse.ArgumentParser()
parser.add_argument("action", choices=["create", "status", "wait", "download", "remix"])
parser.add_argument("task_id", nargs="?")
parser.add_argument("--max-polls", type=int, default=60)
parser.add_argument("--image", help="Reference image to upload when creating a task")
args = parser.parse_args()
if args.action != "create" and (not args.task_id or not re.fullmatch(r"[A-Za-z0-9_-]+", args.task_id)):
    parser.error("Provide the public gateway task ID (letters, digits, underscores, or hyphens).")
if not 1 <= args.max_polls <= 120:
    parser.error("--max-polls must be between 1 and 120.")
if args.image and args.action != "create":
    parser.error("--image is only valid with create.")

with OpenAI(base_url=os.environ["API_BASE_URL"].rstrip("/"),
            api_key=os.environ["API_KEY"], timeout=120.0, max_retries=0) as client:
    try:
        if args.action == "create":
            # SDK sends multipart; the gateway also accepts JSON for compatible plugins.
            with ExitStack() as stack:
                extra = {}
                if args.image:
                    extra["input_reference"] = stack.enter_context(Path(args.image).open("rb"))
                result = client.videos.create(
                    model=os.environ["API_MODEL"],
                    prompt="A paper boat floating on a calm pond, static camera.", **extra,
                )
        elif args.action == "remix":
            result = client.videos.remix(args.task_id, prompt="Change the scene to sunset.")
        elif args.action == "download":
            task = client.videos.retrieve(args.task_id)
            if task.status != "completed":
                raise ValueError("Video is not completed.")
            # A failed transfer may leave a partial file; remove it before retrying download.
            output = Path("video.mp4")
            with client.videos.with_streaming_response.download_content(args.task_id) as response:
                media_type = response.headers.get("content-type", "").split(";")[0].lower()
                if media_type not in ("video/mp4", "application/octet-stream"):
                    raise ValueError("Expected MP4 bytes for this example.")
                with output.open("xb") as target:
                    size = 0
                    for chunk in response.iter_bytes():
                        size += len(chunk)
                        if size > 256 * 1024 * 1024:
                            raise ValueError("Example download exceeds 256 MiB.")
                        target.write(chunk)
                if size == 0:
                    raise ValueError("Empty video.")
            print("Saved video.mp4")
            raise SystemExit(0)
        else:
            for attempt in range(args.max_polls if args.action == "wait" else 1):
                result = client.videos.retrieve(args.task_id)
                if result.status not in ("queued", "in_progress") or args.action == "status":
                    break
                if attempt + 1 == args.max_polls:
                    raise SystemExit("Polling limit reached; resume with the same task ID. The task was not cancelled.")
                time.sleep(5)
        if not result.id or result.status not in ("queued", "in_progress", "completed", "failed"):
            raise ValueError("Unrecognized video response.")
        print(json.dumps({"id": result.id, "status": result.status, "progress": getattr(result, "progress", None)}))
        if result.status == "failed":
            raise SystemExit("Video task failed. Check the task logs before submitting again.")
    except APIStatusError as error:
        raise SystemExit(f"Request failed (HTTP {error.status_code}). Keep any task ID; do not automatically create another task.")
    except APIConnectionError:
        raise SystemExit("Connection failed. Keep any task ID; do not automatically create another task.")
    except (ValueError, OSError):
        raise SystemExit("Invalid video response or output file. Check status, content type, and any partial download.")
```

## Image-to-video and Remix

The [create endpoint](/en/api-reference/videos-create) accepts JSON or multipart, but `input_reference`, duration, and dimensions depend on the executing plugin. Upload a reference image in the multipart `input_reference` field and confirm supported encoding, dimensions, and size. Run `python videos.py create --image /path/to/reference.png` to upload the image to the configured gateway and upstream. A prompt alone requests text-to-video; an image reference is not a video upload for transcription.

Use [Remix](/en/api-reference/videos-remix) with an existing task: `python videos.py remix task_example`. The original task's channel and plugin must support it. The gateway resolves the original task and pins submission to its channel. The result is a new task submission summary: save the new `id` and query it. Remix is not universal across video plugins.

## Download and recover

[Download content](/en/api-reference/videos-content) returns bytes; [HEAD](/en/api-reference/videos-content-head) inspects headers only. Use the gateway content endpoint with Bearer authentication. Never send the gateway key to arbitrary provider URLs, or put a long-lived API key into a browser `<video>` URL.

This example accepts MP4 / octet-stream, caps downloads at 256 MiB, and never overwrites `video.mp4`; these are example constraints. A failure can leave a partial file. Remove that partial file or choose another output name before downloading again. Successful transfer does not mean media decoding was validated. Range support, cache headers, file size, and other content types depend on the source and can vary across plugins. Completed tasks do not guarantee permanent content retention. `410` means content is unavailable; `502` can indicate a proxy failure.

## Compatibility boundaries

Legacy `/v1/video/generations` and `/v1/videos` have different response shapes; do not mix parsers. This guide uses `/v1/videos`. It does not promise support for SDK list, delete, extend, character, or separate edit endpoints, nor for webhooks or task cancellation.

If submission times out without returning an ID, inspect request logs before creating again: another POST can duplicate work. Preserve IDs after retrieval/download failures and recover using the [error guide](/en/guides/errors).

[简体中文](/zh/guides/video-generation)

[Related guide: Video understanding](/en/guides/video-understanding)


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