# Video (https://hypit.ai/api-reference/video/)

> Text to video and image to video. One asynchronous endpoint, priced per second.

`POST /v1/videos` submits a clip and answers `202 Accepted` with a
[job envelope](/api-reference/jobs). 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.

## Submit [#submit]

```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
}
```

### Request body [#request-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.

### References [#references]

`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.

## Pricing shape [#pricing-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](/api-reference/models); 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.

## Collecting the clip [#collecting-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.

## End to end [#end-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()));
```

## Errors specific to this route [#errors-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                                     |