{
  "openapi": "3.1.0",
  "info": {
    "title": "Eternal AI Image-to-Video API",
    "version": "1.0.0",
    "summary": "Production REST API for animating a still image into a short video.",
    "description": "Submit an image-to-video generation job and poll for the result. Billing is pay-as-you-go in credits.\n\n**Authentication.** Every request requires an API key prefixed with `sk_`. Send it as `Authorization: Bearer sk_<your-key>` (recommended) or as the `api-key` header. The server's exact rejection on a missing/invalid key is: `missing or invalid API key (expected Authorization: Bearer sk_... or api-key header)`. Get a key at https://eternalai.org/api/keys.\n\n**API host.** All requests must go to `https://open.eternalai.org`. This is NOT the same host that serves the docs (eternalai.org / staging.eternalai.org / preview hosts). Do not concatenate endpoint paths to the docs host — the resulting URLs do not exist. Use the `servers` array below as the source of truth.\n\n**Required fields when submitting**: `prompt`, `image_url`, and `model_id`. There is currently exactly one supported model — always set `model_id` to `\"wan-ai/wan2.2-i2v-a14b-lightning\"`. Every other field has a sensible default.\n\n**Type gotcha.** `duration` is a **string**, not a number. Send `\"3\"`, not `3`. JSON-encoding a number here produces `{\"status\": false, \"error\": \"invalid JSON body\"}` even though the JSON itself parses fine — the failure is schema-type validation, not a parse error.\n\nFor end-to-end examples and a live playground, see https://eternalai.org/api.",
    "termsOfService": "https://eternalai.org/api/legal/api-services",
    "contact": {
      "name": "Eternal AI",
      "url": "https://eternalai.org/api"
    }
  },
  "servers": [
    {
      "url": "https://open.eternalai.org",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Full developer docs",
    "url": "https://eternalai.org/api/docs"
  },
  "security": [
    {
      "BearerAuth": []
    },
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "Image to Video",
      "description": "Generate a video from a starting image."
    }
  ],
  "paths": {
    "/api/image-to-video": {
      "post": {
        "tags": [
          "Image to Video"
        ],
        "summary": "Submit an image-to-video generation job",
        "description": "Submit a generation job. Returns a `request_id` which you poll with `GET /api/image-to-video/{request_id}/status` until the job is `completed` or `failed`.",
        "operationId": "submitImageToVideo",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmitRequest"
              },
              "examples": {
                "minimal": {
                  "summary": "Minimal request",
                  "value": {
                    "prompt": "A cat slowly turning its head toward the camera",
                    "image_url": "https://example.com/cat-start.jpg",
                    "model_id": "wan-ai/wan2.2-i2v-a14b-lightning"
                  }
                },
                "full": {
                  "summary": "All optional fields",
                  "value": {
                    "prompt": "A cat slowly turning its head toward the camera, soft natural light",
                    "image_url": "https://example.com/cat-start.jpg",
                    "model_id": "wan-ai/wan2.2-i2v-a14b-lightning",
                    "end_image_url": "https://example.com/cat-end.jpg",
                    "duration": "5",
                    "aspect_ratio": "16:9",
                    "resolution": "720p",
                    "negative_prompt": "blur, low quality, watermark",
                    "cfg_scale": 0.5,
                    "seed": 12345
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Job accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubmitResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body, unsupported model_id, or bad parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or inactive API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "Insufficient credit balance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Failed to persist the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "502": {
            "description": "Generation backend rejected the submission. Credit is refunded automatically.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/image-to-video/{request_id}/status": {
      "get": {
        "tags": [
          "Image to Video"
        ],
        "summary": "Poll an image-to-video job",
        "description": "Returns the current job status and, when `status` is `completed`, a `video_url`.\n\n**Cadence:** poll every 2–5 seconds. Status calls are not billed and not rate-limited at typical polling rates.\n\n**Typical completion time:** 30–90 seconds for a 720p / 5s clip. Set client timeouts to ~5 minutes (90 polls at 3s) to absorb queue spikes.\n\n**Terminal states:** `completed` (with `result.video_url`) and `failed` (with `result.error` populated). After a terminal state, the `request_id` remains addressable for roughly 24 hours. The `video_url` is served from a CDN and expires about 24 hours after generation — download and re-host it if you need long-term storage.\n\n**Error location on failure:** the failure reason lives at `result.error` (string), with the envelope `error` field set to `null`. The top-level `status` boolean is `true` for any successful status fetch — the job-level outcome is in `result.status`.",
        "operationId": "getImageToVideoStatus",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "request_id",
            "in": "path",
            "required": true,
            "description": "The `request_id` returned by the submit endpoint.",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "9a4f1c84-2bb3-4d29-a4e1-7c0d3b8f5a91"
          }
        ],
        "responses": {
          "200": {
            "description": "Current job state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PollResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or inactive API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Request belongs to a different API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such request_id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "sk_<your-key>",
        "description": "Recommended. Send `Authorization: Bearer sk_<your-key>`. Keys are prefixed with `sk_`. Get a key at https://eternalai.org/api/keys. Server rejection text on missing/invalid key: `missing or invalid API key (expected Authorization: Bearer sk_... or api-key header)`."
      },
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "api-key",
        "description": "Alternative for clients that cannot set the `Authorization` header. Equivalent to `Authorization: Bearer`. Header name is `api-key` (no `x-` prefix —  keys reject `x-api-key`)."
      }
    },
    "schemas": {
      "SubmitRequest": {
        "type": "object",
        "required": [
          "prompt",
          "image_url",
          "model_id"
        ],
        "properties": {
          "prompt": {
            "type": "string",
            "description": "Text description of the desired video. Keep concise for best results."
          },
          "image_url": {
            "type": "string",
            "description": "Starting still (jpg/png/webp). Accepts a publicly reachable URL or a base64-encoded data URI (e.g. `data:image/jpeg;base64,...`). Maximum image file size is 15 MB."
          },
          "model_id": {
            "type": "string",
            "description": "Generation model. **Use the literal string `wan-ai/wan2.2-i2v-a14b-lightning`** — this is currently the only supported model. Any other value returns 400.",
            "enum": [
              "wan-ai/wan2.2-i2v-a14b-lightning"
            ],
            "default": "wan-ai/wan2.2-i2v-a14b-lightning",
            "example": "wan-ai/wan2.2-i2v-a14b-lightning",
            "x-recommended-value": "wan-ai/wan2.2-i2v-a14b-lightning"
          },
          "end_image_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional end frame for two-image conditioning. URL or base64 data URI.",
            "default": null
          },
          "duration": {
            "type": "string",
            "description": "Output length in seconds. **Send as a string, not a number.** `\"3\"` works; `3` returns `{\"error\": \"invalid JSON body\"}` even though the JSON parses fine — the failure is schema-type validation. UIs that bind to `<input type=\"number\">` must `String(value)` before sending.",
            "enum": [
              "1",
              "2",
              "3",
              "4",
              "5"
            ],
            "default": "5",
            "example": "5"
          },
          "aspect_ratio": {
            "type": "string",
            "description": "Output aspect ratio.",
            "enum": [
              "auto",
              "16:9",
              "9:16",
              "1:1",
              "4:3",
              "3:4"
            ],
            "default": "16:9"
          },
          "resolution": {
            "type": "string",
            "description": "Output resolution.",
            "enum": [
              "480p",
              "580p",
              "720p"
            ],
            "default": "720p"
          },
          "negative_prompt": {
            "type": "string",
            "description": "Things to avoid.",
            "default": "blur, distort, and low quality"
          },
          "cfg_scale": {
            "type": "number",
            "description": "CFG scale. Higher values follow the prompt more strictly.",
            "minimum": 0,
            "maximum": 1,
            "default": 0.5
          },
          "seed": {
            "type": "integer",
            "description": "Deterministic seed. Omit for a random seed each call."
          }
        }
      },
      "SubmitResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "boolean",
            "example": true
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "result": {
            "type": "object",
            "properties": {
              "request_id": {
                "type": "string",
                "format": "uuid",
                "example": "9a4f1c84-2bb3-4d29-a4e1-7c0d3b8f5a91"
              }
            }
          }
        }
      },
      "PollResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "boolean",
            "example": true
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "result": {
            "type": "object",
            "properties": {
              "request_id": {
                "type": "string",
                "format": "uuid",
                "example": "9a4f1c84-2bb3-4d29-a4e1-7c0d3b8f5a91"
              },
              "status": {
                "type": "string",
                "description": "Lifecycle state.",
                "enum": [
                  "pending",
                  "processing",
                  "completed",
                  "failed"
                ]
              },
              "progress": {
                "type": "integer",
                "description": "Percent complete (0–100).",
                "minimum": 0,
                "maximum": 100
              },
              "video_url": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Generated MP4 URL. Present only when `status` is `completed`. Served from a CDN and expires about 24 hours after generation; download and re-host if you need long-term storage."
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "boolean",
            "example": false
          },
          "error": {
            "type": "string"
          },
          "result": {
            "type": [
              "object",
              "null"
            ],
            "example": null
          }
        }
      }
    }
  }
}