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

# Image-to-Video API Reference

> Sora 2 image-to-video API reference and live playground — multipart upload of input_reference to animate a static image.

<Info>
  The interactive Playground on the right supports live debugging. Set your API Key in the **Authorization** field (format: `Bearer sk-xxx`), upload a reference image, enter a prompt, choose model / size / seconds, and send.
</Info>

<Tip>
  **Scope**: This page covers "generate video from a reference image" — upload one image as the starting frame / visual anchor to animate static visuals. If you don't need a reference image, use the [Text-to-Video endpoint](/en/api-capabilities/sora-2/text-to-video) (same path, JSON body).
</Tip>

<Warning>
  **⚠️ Reference image dimensions must exactly match `size`**

  * The uploaded image's pixel dimensions must equal the `size` field (e.g. `size=1280x720` requires a 1280×720 image)
  * Mismatch returns 400: `Inpaint image must match the requested width and height`
  * **Pre-crop with ffmpeg / Pillow before upload**

  Other notes:

  * Content-Type must be `multipart/form-data` (not JSON)
  * Only one file is supported; the field name is fixed as `input_reference`
  * Accepted formats: `image/jpeg` / `image/png` / `image/webp`
</Warning>

## Code Samples

### Python (OpenAI SDK Drop-In)

```python theme={null}
from openai import OpenAI
import time

client = OpenAI(
    api_key="sk-your-api-key",
    base_url="https://api.apiyi.com/v1"
)

# Step 1: Submit (the OpenAI SDK auto-handles multipart when input_reference is provided)
with open("./reference.png", "rb") as f:
    video = client.videos.create(
        model="sora-2",
        prompt="Animate this scene: gentle waves lapping against the shore, leaves swaying in the breeze",
        seconds="8",
        size="1280x720",
        input_reference=f
    )
print(f"Video ID: {video.id}, status: {video.status}")

# Step 2: Poll
while True:
    video = client.videos.retrieve(video.id)
    print(f"Status: {video.status}, progress: {getattr(video, 'progress', 0)}%")
    if video.status == "completed":
        break
    if video.status == "failed":
        raise RuntimeError(f"Generation failed: {video}")
    time.sleep(15)

# Step 3: Download
client.videos.download_content(video.id).write_to_file("output.mp4")
print("Saved: output.mp4")
```

### Python (Raw requests + multipart)

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

API_KEY = "sk-your-api-key"
BASE_URL = "https://api.apiyi.com/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

# Step 1: Multipart upload (image dimensions must equal size)
with open("./reference.png", "rb") as f:
    resp = requests.post(
        f"{BASE_URL}/videos",
        headers=HEADERS,  # Don't manually set Content-Type — requests handles the multipart boundary
        data={
            "model": "sora-2",
            "prompt": "Animate this scene with cinematic camera push-in, soft golden hour lighting",
            "seconds": "8",
            "size": "1280x720"
        },
        files={
            "input_reference": ("reference.png", f, "image/png")
        },
        timeout=60  # Multipart uploads of large images can be slow; use a 60-second timeout
    ).json()
video_id = resp["id"]
print(f"Video ID: {video_id}, status: {resp['status']}")

# Step 2: Poll
deadline = time.time() + 900
while time.time() < deadline:
    status_resp = requests.get(f"{BASE_URL}/videos/{video_id}", headers=HEADERS).json()
    print(f"Status: {status_resp['status']}, progress: {status_resp.get('progress', 0)}%")
    if status_resp["status"] == "completed":
        break
    if status_resp["status"] == "failed":
        raise RuntimeError(f"Generation failed: {status_resp}")
    time.sleep(15)

# Step 3: Download
with requests.get(f"{BASE_URL}/videos/{video_id}/content", headers=HEADERS, stream=True) as r:
    r.raise_for_status()
    with open("output.mp4", "wb") as f:
        for chunk in r.iter_content(chunk_size=8192):
            f.write(chunk)
print("Saved: output.mp4")
```

### cURL

```bash theme={null}
{/* Step 1: Multipart upload + submit */}
curl -X POST "https://api.apiyi.com/v1/videos" \
  -H "Authorization: Bearer sk-your-api-key" \
  -F "model=sora-2" \
  -F "prompt=Animate this scene: gentle waves lapping, leaves swaying, cinematic" \
  -F "seconds=8" \
  -F "size=1280x720" \
  -F "input_reference=@./reference.png;type=image/png"

{/* Step 2: Poll */}
curl -X GET "https://api.apiyi.com/v1/videos/video_abc123" \
  -H "Authorization: Bearer sk-your-api-key"

{/* Step 3: Download */}
curl -X GET "https://api.apiyi.com/v1/videos/video_abc123/content" \
  -H "Authorization: Bearer sk-your-api-key" \
  -o output.mp4
```

### Node.js (fetch + FormData)

```javascript theme={null}
import fs from 'node:fs';
import { fileFromPath } from 'formdata-node/file-from-path';
import { FormData } from 'formdata-node';

const API_KEY = 'sk-your-api-key';
const BASE_URL = 'https://api.apiyi.com/v1';

// Step 1: Multipart upload
const form = new FormData();
form.set('model', 'sora-2');
form.set('prompt', 'Animate this scene with cinematic camera push-in, soft lighting');
form.set('seconds', '8');
form.set('size', '1280x720');
form.set('input_reference', await fileFromPath('./reference.png'));

const submitResp = await fetch(`${BASE_URL}/videos`, {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${API_KEY}` },  // Don't manually set Content-Type
    body: form
});
const { id: videoId } = await submitResp.json();
console.log(`Video ID: ${videoId}`);

// Step 2: Poll
let status = 'queued';
while (status !== 'completed' && status !== 'failed') {
    await new Promise(r => setTimeout(r, 15000));
    const data = await (await fetch(`${BASE_URL}/videos/${videoId}`, {
        headers: { 'Authorization': `Bearer ${API_KEY}` }
    })).json();
    status = data.status;
    console.log(`Status: ${status}, progress: ${data.progress ?? 0}%`);
}

if (status === 'failed') throw new Error('Generation failed');

// Step 3: Download
const contentResp = await fetch(`${BASE_URL}/videos/${videoId}/content`, {
    headers: { 'Authorization': `Bearer ${API_KEY}` }
});
fs.writeFileSync('output.mp4', Buffer.from(await contentResp.arrayBuffer()));
console.log('Saved: output.mp4');
```

### Browser JavaScript

```javascript theme={null}
{/* Demo only; route through your backend in production to avoid leaking the API key. */}
const fileInput = document.getElementById('refImage');  // <input type="file" />
const file = fileInput.files[0];

const form = new FormData();
form.append('model', 'sora-2');
form.append('prompt', 'Animate this scene, gentle motion');
form.append('seconds', '4');
form.append('size', '1280x720');
form.append('input_reference', file);

const submitResp = await fetch('https://api.apiyi.com/v1/videos', {
    method: 'POST',
    headers: { 'Authorization': 'Bearer sk-your-api-key' },
    body: form
});
const { id } = await submitResp.json();
console.log('Video ID:', id);

{/* After polling completes, route the video URL through a backend proxy to avoid downloading large files in the browser. */}
```

## Parameters Quick Reference

| Parameter         | Type   | Required | Default    | Description                                                                                                          |
| ----------------- | ------ | -------- | ---------- | -------------------------------------------------------------------------------------------------------------------- |
| `model`           | string | Yes      | —          | `sora-2` (720p only) or `sora-2-pro` (720p / 1024p / 1080p tiers)                                                    |
| `prompt`          | string | Yes      | —          | Video description; focus on **how the static image should animate** (camera motion, object motion, lighting changes) |
| `seconds`         | string | No       | `"4"`      | Duration as **string enum**: `"4"` / `"8"` / `"12"`                                                                  |
| `size`            | string | No       | `720x1280` | Output resolution, **must equal the `input_reference` image dimensions exactly**                                     |
| `input_reference` | file   | Yes      | —          | Reference image file: `image/jpeg` / `image/png` / `image/webp`, **dimensions must equal `size`**                    |

<Tip>
  Detailed parameter constraints, allowed values, and examples are visible in the right-hand Playground. **`input_reference` must be uploaded via multipart** — URLs and base64 are not accepted.
</Tip>

## Reference Image Preparation

<Steps>
  <Step title="Pick the target resolution">
    Choose `size` first based on your use case: portrait `720x1280`, landscape `1280x720`, Pro 1080p landscape `1920x1080`, etc.
  </Step>

  <Step title="Crop locally to exact pixels">
    Use Pillow / ffmpeg to crop the image to the target dimensions:

    ```python theme={null}
    from PIL import Image
    img = Image.open("source.jpg")
    img = img.resize((1280, 720), Image.LANCZOS)  # Or crop first then resize to preserve aspect ratio
    img.save("reference.png")
    ```

    Or one-line ffmpeg:

    ```bash theme={null}
    ffmpeg -i source.jpg -vf "scale=1280:720" reference.png
    ```
  </Step>

  <Step title="Pick the right format">
    Prefer PNG (lossless, ideal for illustrations / screenshots), JPEG for photos to save bytes, WebP if you need transparency.
  </Step>

  <Step title="Focus the prompt on &#x22;motion&#x22; not &#x22;appearance&#x22;">
    The reference image already defines the visuals. The prompt should focus on **how it should animate**: camera push/pull, object motion, lighting changes, character expressions, etc. Example: `"Camera slowly pushes in, leaves gently swaying, sunlight flickering through branches"`.
  </Step>
</Steps>

## Response Format

The response shape is **identical** to [Text-to-Video](/en/api-capabilities/sora-2/text-to-video#response-format): submit returns `id` + `status: "queued"`, polling reports progress, completion downloads via `/v1/videos/{id}/content` as MP4.

```json theme={null}
{
  "id": "video_abc123def456",
  "object": "video",
  "model": "sora-2",
  "status": "queued",
  "progress": 0,
  "created_at": 1712697600,
  "size": "1280x720",
  "seconds": "8",
  "quality": "standard"
}
```

<Warning>
  **⚠️ Common 400 errors**

  * `Inpaint image must match the requested width and height` — reference image dimensions don't match `size`. **Most common.** Validate dimensions client-side before upload
  * `Invalid file format` — uploaded file is not jpeg / png / webp, or is corrupted
  * `Missing required parameter: input_reference` — multipart field name is wrong (must be `input_reference`, not `image` or `reference`)
  * `seconds must be one of "4", "8", "12"` — passed integer `4` instead of string `"4"`
</Warning>

<Info>
  Image-to-video and text-to-video have the **same per-second pricing** (billed by `seconds`); uploading a reference image does not cost extra. See the [pricing table](/en/api-capabilities/sora-2/overview#pricing).
</Info>


## OpenAPI

````yaml api-reference/sora-2-image-to-video-openapi-en.yaml POST /v1/videos
openapi: 3.1.0
info:
  title: Sora 2 Image-to-Video API
  description: >
    OpenAI Sora 2 / Sora 2 Pro image-to-video endpoint (multipart/form-data
    upload of `input_reference`).


    - **Reference image dimensions must exactly match `size`**, otherwise you
    get `Inpaint image must match the requested width and height`

    - Accepted image formats: `image/jpeg` / `image/png` / `image/webp`

    - Same per-second pricing as text-to-video — uploading a reference image
    does not cost extra

    - **Async task endpoint**: this call only submits the task; combine with
    `GET /v1/videos/{id}` polling and `GET /v1/videos/{id}/content` download


    **Authentication**: Add `Authorization: Bearer YOUR_API_KEY` to the request
    header


    **API Key configuration**: In your APIYI console, set the group to **Sora2官转
    (Sora2 Official)** and billing mode to **usage-based**


    **Get API Key**: Visit the [APIYI Console](https://api.apiyi.com/token) to
    create a token
  version: 1.0.0
servers:
  - url: https://api.apiyi.com
    description: Primary endpoint
  - url: https://vip.apiyi.com
    description: Backup endpoint
security:
  - bearerAuth: []
paths:
  /v1/videos:
    post:
      tags:
        - Video Generation
      summary: 'Image-to-video: submit a video generation task from a reference image'
      description: >
        Submits a Sora 2 image-to-video task. The client must use
        multipart/form-data to upload one reference image plus text fields.


        - Required: `model`, `prompt`, `input_reference`

        - Optional: `seconds` (default `"4"`), `size` (default `"720x1280"`)

        - **`input_reference` image dimensions must equal `size`** — pre-crop
        with ffmpeg / Pillow before upload

        - Response shape and polling/download flow are identical to
        text-to-video
      operationId: generateSora2ImageToVideoEn
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/Sora2ImageToVideoRequest'
            example:
              model: sora-2
              prompt: >-
                Animate this scene: gentle waves lapping, leaves swaying,
                cinematic camera push-in
              seconds: '8'
              size: 1280x720
      responses:
        '200':
          description: Task submitted, returns video_id with queued status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Sora2VideoTask'
        '400':
          description: >-
            Invalid parameters (most common: reference image dimensions mismatch
            with size; also unsupported file format, seconds out of range)
        '401':
          description: Unauthorized — invalid API Key
        '403':
          description: Content policy / not on usage-based billing / group is not Sora2官转
        '413':
          description: Uploaded image too large
        '429':
          description: Rate limit exceeded or insufficient balance
        '500':
          description: Upstream OpenAI gateway error — retry 1–2 times
      security:
        - bearerAuth: []
components:
  schemas:
    Sora2ImageToVideoRequest:
      type: object
      required:
        - model
        - prompt
        - input_reference
      properties:
        model:
          type: string
          description: >-
            Model ID. `sora-2` supports 720p only; `sora-2-pro` supports 720p /
            1024p / 1080p
          enum:
            - sora-2
            - sora-2-pro
          default: sora-2
        prompt:
          type: string
          description: >-
            Video generation prompt. **Focus on how the image should animate**:
            camera motion, object motion, lighting changes
          example: >-
            Animate this scene: gentle waves lapping, leaves swaying, cinematic
            camera push-in
        seconds:
          type: string
          description: 'Video duration as **string enum**: `"4"` / `"8"` / `"12"`'
          enum:
            - '4'
            - '8'
            - '12'
          default: '4'
        size:
          type: string
          description: >
            Output resolution. **Must exactly match the `input_reference` image
            dimensions**:


            - `sora-2` (720p only): `720x1280` / `1280x720`

            - `sora-2-pro` additionally: `1024x1792` / `1792x1024` / `1080x1920`
            / `1920x1080`
          enum:
            - 720x1280
            - 1280x720
            - 1024x1792
            - 1792x1024
            - 1080x1920
            - 1920x1080
          default: 720x1280
        input_reference:
          type: string
          format: binary
          description: >
            Reference image file used as the video's starting frame / visual
            anchor.


            - Accepted formats: `image/jpeg` / `image/png` / `image/webp`

            - **Dimensions must equal `size`**, otherwise you get `Inpaint image
            must match the requested width and height`

            - Only one file is supported; field name is fixed as
            `input_reference`
    Sora2VideoTask:
      type: object
      properties:
        id:
          type: string
          description: Task ID for subsequent polling and download
          example: video_abc123def456
        object:
          type: string
          description: Object type, fixed `video`
          example: video
        model:
          type: string
          description: Model ID used for this task
          example: sora-2
        status:
          type: string
          description: |
            Task status:
            - `queued` — submitted, waiting in queue
            - `in_progress` — generating
            - `completed` — done, ready to download (`/v1/videos/{id}/content`)
            - `failed` — failed (not billed), safe to retry
          enum:
            - queued
            - in_progress
            - completed
            - failed
          example: queued
        progress:
          type: integer
          description: Generation progress percentage (0–100), not strictly linear
          example: 0
        created_at:
          type: integer
          description: Task creation Unix timestamp (seconds)
          example: 1712697600
        completed_at:
          type: integer
          description: >-
            Task completion Unix timestamp (seconds), present only on completed
            status
          example: 1712697900
        size:
          type: string
          description: Actual output resolution (matches the requested `size`)
          example: 1280x720
        seconds:
          type: string
          description: Actual duration generated (matches the requested `seconds`)
          example: '8'
        quality:
          type: string
          description: Quality tier (`standard` for sora-2, `high` for sora-2-pro)
          example: standard
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        API Key from the APIYI console (must use Sora2官转 group + usage-based
        billing)

````