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

bash
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"
  }'
json
{
  "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

FieldTypeNotes
modelstringrequired
promptstringrequired unless you supply a reference image, a first frame, reference media, or ref_video_url
secondsnumberclip length. duration is accepted as a synonym
resolutionstring480p, 720p, 1080p, 4k — whichever the model publishes
sizestringe.g. "1920x1080", an alternative to resolution
aspect_ratiostringe.g. "16:9". ratio is accepted as a synonym
fpsintegermust not be negative
seedinteger
negative_promptstring
camera_fixedboolean
watermarkboolean
input_referencereferenceone reference image, distinct from a first frame
reference_image_urlsreference[]reference-image list, on models that support it; distinct from frame slots
image_urlreferencecompatibility alias for one first-frame image; use reference_image_urls for a list
first_framereferencefirst frame for image-to-video. image is an alias
last_framereferencefinal frame, on models that support one
reference_videosstring[]reference-video URLs, on multimodal models that support them
reference_audiosstring[]reference-audio URLs, on multimodal models that support them
generate_audiobooleanwhether to generate native audio, on models that support it
ref_video_urlstringa source clip, on models that support video-to-video; video_url is an alias
extraobjectvendor 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:

json
{ "input_reference": "https://example.com/reference.png" }
json
{ "first_frame": "data:image/png;base64,iVBORw0KGgo…" }
json
{ "input_reference": { "url": "https://example.com/frame.png" } }
json
{ "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:

bash
# 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

python
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)
js
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

StatuscodeMeaning
400missing_modelmodel is required
400missing_promptnone of prompt, a reference image, a first frame, or ref_video_url was supplied
400invalid_secondsseconds must not be negative
400invalid_fpsfps must not be negative
400invalid_input_referencethe reference is not a URL, data: URL or base64 image
400invalid_jsonthe body is not valid JSON
400invalid_media_url / unsupported_media_url_schemea URL failed the guarded-fetch policy
413body_too_largethe body exceeds 24 MiB
404model_not_foundno enabled provider serves this model for video