Permalink to VideoVideo
POST /v1/videos submits a clip and answers 202 Accepted with a
job envelope. Video is the longest-running thing on this API — minutes, not
seconds — so the asynchronous ladder is not an inconvenience here, it is the only workable shape.
Permalink to submitSubmit
curl https://hypit.ai/v1/videos \
-H "Authorization: Bearer $HYPIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "bytedance/seedance-2",
"prompt": "a paper boat drifting down a rain gutter, camera tracking alongside",
"seconds": 5,
"resolution": "720p",
"aspect_ratio": "16:9"
}'{
"id": "job_2f81c0a4d3b57e9016ab24cf",
"status": "queued",
"kind": "video",
"model": "bytedance/seedance-2",
"progress": 0,
"created_at": 1787824589,
"dispatch_deadline_at": 1787825189
}Permalink to request-bodyRequest body
| Field | Type | Notes |
|---|---|---|
model | string | required |
prompt | string | required unless you supply a reference image, a first frame, reference media, or ref_video_url |
seconds | number | clip length. duration is accepted as a synonym |
resolution | string | 480p, 720p, 1080p, 4k — whichever the model publishes |
size | string | e.g. "1920x1080", an alternative to resolution |
aspect_ratio | string | e.g. "16:9". ratio is accepted as a synonym |
fps | integer | must not be negative |
seed | integer | |
negative_prompt | string | |
camera_fixed | boolean | |
watermark | boolean | |
input_reference | reference | one reference image, distinct from a first frame |
reference_image_urls | reference[] | reference-image list, on models that support it; distinct from frame slots |
image_url | reference | compatibility alias for one first-frame image; use reference_image_urls for a list |
first_frame | reference | first frame for image-to-video. image is an alias |
last_frame | reference | final frame, on models that support one |
reference_videos | string[] | reference-video URLs, on multimodal models that support them |
reference_audios | string[] | reference-audio URLs, on multimodal models that support them |
generate_audio | boolean | whether to generate native audio, on models that support it |
ref_video_url | string | a source clip, on models that support video-to-video; video_url is an alias |
extra | object | vendor passthrough |
input_reference / reference_image_urls always mean reference images. image_url, first_frame /
image and last_frame always mean frame slots. The gateway
never converts one semantic slot into another; a model that does not support the requested slot returns
400. reference_videos and reference_audios accept URL arrays only. For larger media, upload first
with POST /v1/files and pass the returned URL.
The body is capped at 24 MiB, because an image may arrive as a base64 data: URL inside it.
Permalink to referencesReferences
input_reference, reference_image_urls, image_url, first_frame, image and last_frame each
accept three spellings:
{ "input_reference": "https://example.com/reference.png" }{ "first_frame": "data:image/png;base64,iVBORw0KGgo…" }{ "input_reference": { "url": "https://example.com/frame.png" } }{ "input_reference": { "b64": "iVBORw0KGgo…", "mime_type": "image/png" } }A bare base64 string is also accepted when the decoded bytes are recognisably an image — several
SDKs send raw bytes that way. Anything else is 400 invalid_input_reference.
Every http(s) URL is fetched through a guarded fetcher that refuses private and link-local
addresses and re-checks each redirect.
Model-specific fields not listed in the table are retained and passed through (either at the top level
or inside extra), for example cfg_scale, camera_control and output_format. Media URLs still
go through the same safety validation; an adaptor returns an explicit 400 when its model does not
support a requested field.
Permalink to pricing-shapePricing shape
Video is priced per second of output, multiplied by a resolution ratio. Read
pricing.credits.per_second and pricing.resolution_ratio from the
model card; a 1080p second commonly costs several times a 480p one on the
same model. Some models add a flag_ratio for axes the request fields cannot express — a generated
audio track, a reference video — which is why a submitted clip's estimate can exceed the naive
per_second × seconds figure.
Permalink to collecting-the-clipCollecting the clip
GET /v1/jobs/{job_id} for status, then GET /v1/jobs/{job_id}/assets for signed MP4 links. One
status alias exists for readability:
# same handler as GET /v1/jobs/{job_id}
curl https://hypit.ai/v1/videos/job_2f81c0a4d3b57e9016ab24cf \
-H "Authorization: Bearer $HYPIT_API_KEY"
# list assets and download the `items[0].url` returned above without the API key
curl https://hypit.ai/v1/jobs/job_2f81c0a4d3b57e9016ab24cf/assets \
-H "Authorization: Bearer $HYPIT_API_KEY"Signed URLs live for 15 minutes and support HTTP range requests, so they can be handed straight to a
<video> element. Re-call the assets route for a fresh one; artifacts are retained for 30 days.
Permalink to end-to-endEnd to end
import os, time, httpx
BASE = os.environ.get("HYPIT_BASE_URL", "https://hypit.ai/v1")
AUTH = {"Authorization": f"Bearer {os.environ['HYPIT_API_KEY']}"}
created = httpx.post(
f"{BASE}/videos",
headers={**AUTH, "Idempotency-Key": "demo-clip-0001"},
json={
"model": "bytedance/seedance-2",
"prompt": "a paper boat drifting down a rain gutter",
"seconds": 5,
"resolution": "720p",
},
timeout=60,
)
created.raise_for_status()
job_id = created.json()["id"]
print("job", job_id)
delay = 5.0
while True:
job = httpx.get(f"{BASE}/jobs/{job_id}", headers=AUTH, timeout=30).json()
print(job["status"], job["progress"])
if job["status"] in {"succeeded", "failed", "queue_expired"}:
break
time.sleep(delay)
delay = min(delay * 1.3, 15.0)
if job["status"] != "succeeded":
raise RuntimeError(f"{job['status']}: {job.get('error_code')} {job.get('error')}")
asset = httpx.get(f"{BASE}/jobs/{job_id}/assets", headers=AUTH, timeout=30).json()["items"][0]
with httpx.stream("GET", asset["url"], follow_redirects=True, timeout=None) as src:
src.raise_for_status()
with open("clip.mp4", "wb") as f:
for chunk in src.iter_bytes():
f.write(chunk)import { writeFile } from "node:fs/promises";
const BASE = process.env.HYPIT_BASE_URL ?? "https://hypit.ai/v1";
const AUTH = { Authorization: `Bearer ${process.env.HYPIT_API_KEY}` };
const TERMINAL = new Set(["succeeded", "failed", "queue_expired"]);
const created = await fetch(`${BASE}/videos`, {
method: "POST",
headers: { ...AUTH, "Content-Type": "application/json", "Idempotency-Key": "demo-clip-0001" },
body: JSON.stringify({
model: "bytedance/seedance-2",
prompt: "a paper boat drifting down a rain gutter",
seconds: 5,
resolution: "720p",
}),
});
if (!created.ok) throw new Error(`${created.status} ${await created.text()}`);
const { id } = await created.json();
let job, delay = 5_000;
for (;;) {
job = await (await fetch(`${BASE}/jobs/${id}`, { headers: AUTH })).json();
console.log(job.status, job.progress);
if (TERMINAL.has(job.status)) break;
await new Promise((r) => setTimeout(r, delay));
delay = Math.min(delay * 1.3, 15_000);
}
if (job.status !== "succeeded") throw new Error(`${job.status}: ${job.error_code} ${job.error}`);
const { items } = await (await fetch(`${BASE}/jobs/${id}/assets`, { headers: AUTH })).json();
await writeFile("clip.mp4", Buffer.from(await (await fetch(items[0].url)).arrayBuffer()));Permalink to errors-specific-to-this-routeErrors specific to this route
| Status | code | Meaning |
|---|---|---|
400 | missing_model | model is required |
400 | missing_prompt | none of prompt, a reference image, a first frame, or ref_video_url was supplied |
400 | invalid_seconds | seconds must not be negative |
400 | invalid_fps | fps must not be negative |
400 | invalid_input_reference | the reference is not a URL, data: URL or base64 image |
400 | invalid_json | the body is not valid JSON |
400 | invalid_media_url / unsupported_media_url_scheme | a URL failed the guarded-fetch policy |
413 | body_too_large | the body exceeds 24 MiB |
404 | model_not_found | no enabled provider serves this model for video |