# OpenAI Video API (`/v1/videos`, Sora 2) **Status:** DOCUMENTED + **DEPRECATED** — the deprecations page (notice 2026-03-24) schedules removal of the **Videos API and every Sora 2 model on 2026-09-24**. On 2026-09-18 the endpoints still answer: `GET /v1/videos`, `GET /v1/videos/{id}`, `GET /v1/videos/{id}/content` LIVE_VERIFIED; `POST /v1/videos`, remix, edits, extensions, characters, DELETE DOCUMENTED/UNVERIFIED (not called: cost / destructive). **Sources:** [Videos reference](https://developers.openai.com/api/reference/resources/videos) · [Video generation guide](https://developers.openai.com/api/docs/guides/video-generation) · [Pricing](https://developers.openai.com/api/docs/pricing) · [Deprecations](https://developers.openai.com/api/docs/deprecations) · [Changelog](https://developers.openai.com/api/docs/changelog) (2026-03-12 entries) · model pages `sora-2`, `sora-2-pro` · OpenAPI `CreateVideo*Body`, `VideoResource`, `VideoCharacterResource`. **Last verified:** 2026-09-18. **Machine-readable:** endpoints fragment `openai-images-video-embeddings-moderation.json`, `parameters/openai-videos.json`, `objects/openai-media-objects.json`, `prices/openai-media.json`. ## Models | Model | Alias → snapshot | Output | Sizes | Price / second (standard · batch) | Rate limit (RPM T1…T5) | Status | |---|---|---|---|---|---|---| | `sora-2` (speed, iteration) | `sora-2` → `sora-2-2025-12-08` (older `sora-2-2025-10-06`) | video + synced audio | 720×1280, 1280×720 | $0.10 · $0.05 | 25 / 50 / 125 / 200 / 375 | DEPRECATED → 2026-09-24 | | `sora-2-pro` (production) | `sora-2-pro` → `sora-2-pro-2025-10-06` | video + audio | 720p $0.30 · $0.15; 1024×1792 / 1792×1024 $0.50 · $0.25; 1080×1920 / 1920×1080 $0.70 · $0.35 | | 10 / 25 / 50 / 75 / 150 | DEPRECATED → 2026-09-24 | Both models accept 4, 8, 12 s (spec enum) and, per guide/changelog 2026-03-12, 16 and 20 s. Cost = seconds × per-second rate (an 8 s 720p sora-2 clip ≈ $0.80). ## Operations | Method / path | Body | Returns | Status | |---|---|---|---| | `POST /v1/videos` | JSON or multipart: `prompt`*, `model`, `seconds`, `size`, `input_reference`, `characters[]` (guide only) | `Video` job (`status: queued`) | DOCUMENTED, UNVERIFIED | | `GET /v1/videos?limit&after&order` | — | `{object: list, data: Video[], first_id, last_id, has_more}` | **LIVE_VERIFIED** (200) | | `GET /v1/videos/{video_id}` | — | `Video` | **LIVE_VERIFIED** (200; unknown id → 404 `Video with id '…' not found.`) | | `DELETE /v1/videos/{video_id}` | — | `{id, deleted: true, object: "video.deleted"}` | DOCUMENTED (destructive, not run) | | `GET /v1/videos/{video_id}/content?variant=video|thumbnail|spritesheet` | — | `video/mp4` · `image/webp` · jpg | **LIVE_VERIFIED** endpoint; asset expired → 404 `The video is no longer available. Downloads expire after 48 hours.` | | `POST /v1/videos/{video_id}/remix` | `{prompt}` | `Video` (`remixed_from_video_id`) | DOCUMENTED; being replaced by edits (changelog 2026-03-12: deprecated in 6 months) | | `POST /v1/videos/edits` | JSON `{video: {id}, prompt}` or multipart `video=@file, model, prompt` | `Video` | DOCUMENTED, UNVERIFIED (upload path = eligible customers only) | | `POST /v1/videos/extensions` | `{video: {id}, prompt, seconds}` (JSON or multipart) | `Video` (`seconds` = stitched total) | DOCUMENTED, UNVERIFIED | | `POST /v1/videos/characters` | multipart `video=@clip.mp4;type=video/mp4`, `name` (1–80) | `{id, name, created_at}` | DOCUMENTED, UNVERIFIED | | `GET /v1/videos/characters/{character_id}` | — | `{id, name, created_at}` | DOCUMENTED, UNVERIFIED | There is **no list-characters operation**: `GET /v1/videos/characters?limit=1` → `400 invalid_value "Invalid 'video_id': 'characters'. Expected an ID that begins with 'video'."` (routed as `/videos/{video_id}`). SDKs: Python `client.videos.create | retrieve | list | delete | download_content | remix | edit | extend | create_character | get_character | create_and_poll | poll`; Node `client.videos.create | retrieve | list | delete | downloadContent | remix | edit | extend | createCharacter | getCharacter`. ## Parameters of `POST /v1/videos` | Param | Type | Default | Notes | |---|---|---|---| | `prompt` * | string 1–32 000 | — | describe shot type, subject, action, setting, lighting | | `model` | `sora-2` \| `sora-2-pro` \| snapshots | `sora-2` | | | `seconds` | string `"4"|"8"|"12"` (+ `"16"|"20"` per guide) | `"4"` | strings, not ints | | `size` | `720x1280` \| `1280x720` \| `1024x1792` \| `1792x1024` (+ `1080x1920` \| `1920x1080` sora-2-pro, guide/pricing) | `720x1280` | spec enum lacks 1080p | | `input_reference` | multipart file **or** JSON `{file_id}` / `{image_url}` (URL or data URL ≤ 20 MiB) | — | becomes the **first frame**; human faces rejected; Batch: JSON only | | `characters` | `[{id}]`, ≤ 2 | — | guide only, **absent from OpenAPI**; name must appear verbatim in prompt; not with extensions | ## Job lifecycle ``` POST /v1/videos ──► {status:"queued", progress:0} │ poll GET /v1/videos/{id} every 10–20 s (exp. backoff) or webhook video.completed / video.failed ▼ in_progress (progress %) ──► completed (completed_at, expires_at) ──► GET …/content (mp4 | thumbnail | spritesheet) └──► failed (error{code,message,misalignment?}) ``` - A render "may take several minutes"; longer/1080p jobs take materially longer. - `expires_at` marks asset expiry: live job had `expires_at − completed_at = 172 800 s = 48 h`; batch-generated videos downloadable ≤ 24 h after the batch completes. Metadata stays listable after expiry (our 2-month-old job still lists as `completed`). - Webhook payload: `{"id":"evt_…","object":"event","created_at":…,"type":"video.completed"|"video.failed","data":{"id":"video_…"}}` — id only, fetch details afterwards. - Batch API: `POST /v1/videos` only, JSON bodies, terminal states `completed|failed|expired`. ### `Video` object (live sample) ```json {"id":"video_6a5e…a57a","object":"video","created_at":1784590069,"status":"completed","completed_at":1784590117, "expires_at":1784762917,"model":"sora-2","progress":100,"prompt":"A simple blue geometric cube rotating on a white background.", "remixed_from_video_id":null,"edited_from_video_id":null,"extended_from_video_id":null,"error":null,"seconds":"4","size":"720x1280"} ``` `edited_from_video_id` and `extended_from_video_id` are **LIVE_DISCOVERED** (absent from reference and spec). `error.misalignment{error_type, detailed_explanation, steer.message}` reuses the safety-alert error types (see `docs/openai/safety.md`). ## Remix, edit, extend, characters - **Remix** (`/{id}/remix`): re-render a completed video with a new prompt; superseded by **edits**. - **Edits** (`/videos/edits`): targeted change keeping structure/continuity; one clear adjustment per edit; source = generated video id (model inferred) or uploaded MP4 (+ explicit `model`, eligible customers). - **Extensions** (`/videos/extensions`): continue a completed clip using the whole source as context; each extension adds ≤ 20 s; ≤ 6 extensions, ≤ 120 s total; no characters / image references. - **Characters**: reusable non-human subject from a 2–4 s MP4 (16:9 or 9:16, 720p–1080p, match output aspect ratio); ≤ 2 per video; combinable with `input_reference`; human likeness blocked by default (sales/eligibility). ## Content policy and restrictions (guide) Under-18-suitable content only (bypass "available in the future"); copyrighted characters/music rejected; real people incl. public figures cannot be generated; input images with human faces rejected; human-likeness character uploads blocked by default. ## Examples and tests `examples/openai/video/` — `list.sh|py|ts` (LIVE_VERIFIED), `create_poll_download.py|ts|sh` (UNVERIFIED create; poll + download logic follows the guide). `tests/openai/test_video.py`: list/retrieve/404 shape unmarked (free); creation behind `RUN_VIDEO_TESTS`.