Video
Video takes minutes to render, so it is a job: you create it, poll it until it is done, then download the file. Every video model works this way, through the same three calls:
| Step | Call | Returns |
|---|---|---|
| Create | POST /v1/videos | The job, with its id and status queued |
| Poll | GET /v1/videos/{video_id} | The job's current status; once completed, a content_url |
| Download | GET /v1/videos/{video_id}/content | The MP4 file |
This sample is a file we run against the service before publishing. It makes a 4-second clip from a prompt:
# Video generation is a job: submit it, poll until it is done, then download the file.
# Videos take minutes to render, so never wait on the first request.
import os
import time
import requests
BASE = os.getenv("OCEANTOKEN_BASE_URL", "https://api.oceantoken.ai/v1")
HEADERS = {"Authorization": f"Bearer {os.environ['OCEANTOKEN_API_KEY']}"}
job = requests.post(
f"{BASE}/videos",
headers=HEADERS,
json={
"model": "bytedance/seedance-1-5-pro",
"prompt": "A paper boat drifting down a rain gutter, close up, soft light",
"seconds": "4",
},
timeout=60,
)
if not job.ok:
# The error body says why (see Errors in the docs), e.g. insufficient_credits.
raise SystemExit(f"{job.status_code}: {job.text}")
video_id = job.json()["id"]
print("submitted", video_id)
while True:
status = requests.get(f"{BASE}/videos/{video_id}", headers=HEADERS, timeout=60).json()
if status["status"] not in ("queued", "in_progress"):
break # completed, or failed with an error code and message
time.sleep(10)
if status["status"] != "completed":
raise SystemExit(f"video failed: {status.get('error')}")
content = requests.get(f"{BASE}/videos/{video_id}/content", headers=HEADERS, timeout=120)
content.raise_for_status()
with open(f"{video_id}.mp4", "wb") as f:
f.write(content.content)
print("saved", f"{video_id}.mp4", len(content.content), "bytes")
The request
| Field | What it does |
|---|---|
model | A video model's ID, for example bytedance/seedance-2.0. |
prompt | What happens in the clip. |
duration | Length in whole seconds, such as 5. seconds is accepted too, as a number or a string. Without it, most models make 5 seconds. |
resolution | 480p, 720p or 1080p, as the model offers. Lower is faster and cheaper. Some models are made at one resolution only, and any other is refused: kwaivgi/kling-v3.0-pro at 1080p, kwaivgi/kling-v3.0-std at 720p. |
aspect_ratio | 16:9, 9:16, 1:1, 4:3, 3:4 or 21:9, as the model offers. Models that list adaptive (Seedance 2.0, 2.0 Fast, 2.0 Mini and 2.5, among others) take it too: the clip takes the shape of the first frame. See The clip's shape. |
size | Instead of resolution and aspect_ratio: width x height, such as 1280x720. The nearest size the model offers is used. |
generate_audio | true for a clip with sound, on models that make it; false for a silent clip. |
frame_images | The first and last frame, for image to video. |
input_references | Reference images, videos or audio, for reference to video. |
omni_reference_task_type | On bytedance/seedance-2.0, -2.0-fast, -2.0-mini and bytedance/seedance-2.5, with reference media: reference, edit, extend or auto (reference is the default on 2.5). See Continue or edit a video. |
Each model takes its own durations, resolutions and inputs. A value it does not take is refused with 400 invalid_request before anything runs or is charged, and the message says what it takes, for example duration must be 4 or 8 seconds for this model. To read them first, see A model's options.
Some optional fields, such as adaptive and omni_reference_task_type, cannot be applied to every request. When one cannot, the request is not refused: the clip is made without it, and the create response's x-oceantoken-dropped-params header lists it, for example aspect_ratio.
Image to video
Give the first frame, and on models that take one, the last frame. Each image is a public https:// URL or a data URL:
{
"model": "bytedance/seedance-2.0",
"prompt": "The paper boat drifts out of frame, the camera stays still",
"duration": 5,
"resolution": "720p",
"frame_images": [
{"type": "image_url", "frame_type": "first_frame", "image_url": {"url": "https://example.com/start.png"}},
{"type": "image_url", "frame_type": "last_frame", "image_url": {"url": "https://example.com/end.png"}}
]
}
Shorter forms work too: "first_frame": "https://..." and "last_frame": "https://...", or OpenAI's input_reference for the first frame, either a URL or an uploaded file in a multipart request.
The clip's shape
On bytedance/seedance-2.0, bytedance/seedance-2.0-fast, bytedance/seedance-2.0-mini and bytedance/seedance-2.5, a request with a first frame and no aspect_ratio follows the first frame: a portrait frame gives a portrait clip. "aspect_ratio": "adaptive" asks for the same, on any model that lists it.
For a set shape instead, send it, such as "aspect_ratio": "9:16". The exception is bytedance/seedance-2.5: with a frame, its clip always keeps the frame's shape, and a different aspect_ratio is left out (listed in x-oceantoken-dropped-params).
Reference to video
Models that take references (Seedance 2.0 and newer, among others) build the clip from images, and on some models video and audio, that you supply: a character, a product, a camera move, a soundtrack.
{
"model": "bytedance/seedance-2.0",
"prompt": "The woman from the first image walks through the market from the video, to the music",
"duration": 8,
"input_references": [
{"type": "image_url", "image_url": {"url": "https://example.com/character.png"}},
{"type": "video_url", "video_url": {"url": "https://example.com/market.mp4"}},
{"type": "audio_url", "audio_url": {"url": "https://example.com/theme.mp3"}}
]
}
How many of each a model takes is in its options (referenceLimits). More than that is refused with 400, for example kwaivgi/kling-v3.0-std takes at most 6 reference images; 7 were sent. A reference the model cannot use is refused with 400 too, never dropped without telling you. The -upscaled Seedance models take reference images only, from 1 to 9, and no frames.
Media you send: public URLs or data URLs, each up to 25 MB; the whole request is at most 10 MB, so send video and large images as URLs. A URL the model cannot fetch fails the job with invalid_request.
Continue or edit a video
Give a reference video and say in the prompt what to do with it.
bytedance/seedance-2.0, bytedance/seedance-2.0-fast, bytedance/seedance-2.0-mini and bytedance/seedance-2.5 take omni_reference_task_type, which says what a request with reference media is: reference makes a new clip from the references, edit changes the reference video, extend continues it, and auto lets the model decide from the prompt.
- Seedance 2.0, 2.0 Fast and 2.0 Mini also go by the prompt, so say the same thing there: for example
editwith Edit the video: make it a snowy winter scene, orextendwith Continue from the end of video 1. Left out, the model decides from the prompt. The new clip is the length you ask for; the original is not joined onto it. - Seedance 2.5 reads a request with reference media as
referenceunless told otherwise. To continue a video exactly where it ends, use"omni_reference_task_type": "extend".
Without reference media, or when the request is served where the field cannot be applied, omni_reference_task_type is left out and listed in x-oceantoken-dropped-params.
The job
{
"id": "video_9Ck2...",
"object": "video",
"status": "completed",
"model": "bytedance/seedance-2.0",
"seconds": "5",
"size": "1280x720",
"created_at": 1792000000,
"completed_at": 1792000095,
"content_url": "https://...",
"error": null
}
statusgoesqueued→in_progress→completedorfailed. Poll every 10 to 20 seconds; a clip usually takes a few minutes.content_urlappears once the job iscompleted. It is valid for 24 hours; download the file within that time, from it or fromGET /v1/videos/{video_id}/content.error, on afailedjob, has acodeand amessage:
code | What happened | What to do |
|---|---|---|
content_filtered | The prompt, a reference or the result was refused by content moderation. | Change the prompt or the media. |
invalid_request | The model refused the parameters, or could not read a reference. | Check every URL is public and the values are in the model's options. |
rate_limited | The model is busy. | Submit again in a minute. |
timeout | The job did not finish in time. | Submit again. |
temporarily_unavailable | The model cannot take jobs right now. | Submit again later, or use another model. |
generation_failed | Anything else. | Submit again; if it keeps failing, write to us with the job id. |
GET /v1/videos lists your jobs, newest first (limit up to 100, after an id from the previous page), if you lose an id. The console lists them too, under Media.
A model's options
The durations, resolutions, aspect ratios, sound and inputs a model takes:
curl "https://api.oceantoken.ai/oceantoken/video/model-spec?model=bytedance/seedance-2.0" \
-H "Authorization: Bearer $OCEANTOKEN_API_KEY"
The answer's spec has duration (min, max, default), resolutions, aspectRatios (with adaptive on models that can follow the first frame), generateAudio, modes (t2v text to video, i2v first frame, flf2v first and last frame, r2v references), referenceLimits and, on bytedance/seedance-2.5, omniReferenceTaskTypes.
Cost and retries
- A job holds its estimated cost against your balance when it is created and is charged the actual cost when it finishes. A
failedjob is not charged and its hold is released. If your available balance cannot cover the estimate, the create call is refused with a400and no job starts. - What a clip costs depends on its length, resolution and sound; see How you are charged and each model's page on oceantoken.ai. The console's Logs shows every job's charge.
- Send an
Idempotency-Keywith the create call. A retry after a network timeout then returns the job already created instead of starting, and charging, a second one (Errors). - A job is accepted by one channel and runs there to the end; see Retries and failover → Video jobs.