curl -X POST "https://api.hooked.so/v1/media/generate/video" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "prompt": "Slow push-in on the serum bottle as water droplets roll down the glass", "model": "seedance_2_0", "imageMediaId": "cm4x9r2t50007ab12gh34ij78", "durationSeconds": 5, "aspectRatio": "ratio_9_16" }'
const headers = { "x-api-key": process.env.HOOKED_API_KEY, "Content-Type": "application/json" };
const response = await fetch("https://api.hooked.so/v1/media/generate/video", {
method: "POST",
headers,
body: JSON.stringify({
prompt: "Slow push-in on the serum bottle as water droplets roll down the glass",
model: "seedance_2_0",
imageMediaId: "cm4x9r2t50007ab12gh34ij78",
durationSeconds: 5,
}),
});
const { data } = await response.json();
let media;
do {
await new Promise((resolve) => setTimeout(resolve, 10000));
media = (await (await fetch(`https://api.hooked.so/v1/media/${data.mediaId}`, { headers })).json()).data;
} while (media.status === "PROCESSING" || media.status === "PENDING");
console.log(media.status, media.url);
import os
import time
import requests
headers = {"x-api-key": os.environ["HOOKED_API_KEY"]}
data = requests.post(
"https://api.hooked.so/v1/media/generate/video",
headers=headers,
json={
"prompt": "Slow push-in on the serum bottle as water droplets roll down the glass",
"model": "seedance_2_0",
"imageMediaId": "cm4x9r2t50007ab12gh34ij78",
"durationSeconds": 5,
},
).json()["data"]
while True:
time.sleep(10)
media = requests.get(f"https://api.hooked.so/v1/media/{data['mediaId']}", headers=headers).json()["data"]
if media["status"] in ("COMPLETED", "FAILED"):
break
print(media["status"], media["url"])
{
"success": true,
"message": "Video generation started",
"data": {
"status": "PROCESSING",
"mediaId": "cm4xa1b2c0009ab12kl56mn90",
"type": "video",
"model": "seedance_2_0",
"aspectRatio": "ratio_9_16",
"durationSeconds": 5,
"resolution": "1080p",
"sourceMediaId": "cm4x9r2t50007ab12gh34ij78",
"usedCredits": 155
}
}
{
"success": false,
"message": "durationSeconds: Seedance 2.0 makes clips of 5, 10 seconds"
}
Generate Video
Generate an AI video clip from a prompt or an image into your media library
curl -X POST "https://api.hooked.so/v1/media/generate/video" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "prompt": "Slow push-in on the serum bottle as water droplets roll down the glass", "model": "seedance_2_0", "imageMediaId": "cm4x9r2t50007ab12gh34ij78", "durationSeconds": 5, "aspectRatio": "ratio_9_16" }'
const headers = { "x-api-key": process.env.HOOKED_API_KEY, "Content-Type": "application/json" };
const response = await fetch("https://api.hooked.so/v1/media/generate/video", {
method: "POST",
headers,
body: JSON.stringify({
prompt: "Slow push-in on the serum bottle as water droplets roll down the glass",
model: "seedance_2_0",
imageMediaId: "cm4x9r2t50007ab12gh34ij78",
durationSeconds: 5,
}),
});
const { data } = await response.json();
let media;
do {
await new Promise((resolve) => setTimeout(resolve, 10000));
media = (await (await fetch(`https://api.hooked.so/v1/media/${data.mediaId}`, { headers })).json()).data;
} while (media.status === "PROCESSING" || media.status === "PENDING");
console.log(media.status, media.url);
import os
import time
import requests
headers = {"x-api-key": os.environ["HOOKED_API_KEY"]}
data = requests.post(
"https://api.hooked.so/v1/media/generate/video",
headers=headers,
json={
"prompt": "Slow push-in on the serum bottle as water droplets roll down the glass",
"model": "seedance_2_0",
"imageMediaId": "cm4x9r2t50007ab12gh34ij78",
"durationSeconds": 5,
},
).json()["data"]
while True:
time.sleep(10)
media = requests.get(f"https://api.hooked.so/v1/media/{data['mediaId']}", headers=headers).json()["data"]
if media["status"] in ("COMPLETED", "FAILED"):
break
print(media["status"], media["url"])
{
"success": true,
"message": "Video generation started",
"data": {
"status": "PROCESSING",
"mediaId": "cm4xa1b2c0009ab12kl56mn90",
"type": "video",
"model": "seedance_2_0",
"aspectRatio": "ratio_9_16",
"durationSeconds": 5,
"resolution": "1080p",
"sourceMediaId": "cm4x9r2t50007ab12gh34ij78",
"usedCredits": 155
}
}
{
"success": false,
"message": "durationSeconds: Seedance 2.0 makes clips of 5, 10 seconds"
}
Overview
The dashboard’s Media Generator, from the API: one clip, text-to-video or image-to-video, made with the same models at the same price, and saved to your media library. The call answers202 right away with the new mediaId; the clip is generated after the response. Poll Get Media until status is COMPLETED (its url is the video) or FAILED.
To animate an image, send it as imageMediaId (a finished image of your library, for example one made with Generate Image) or as imageUrl (a public https JPEG, PNG or WEBP, imported into your library first). Without either, the clip is generated from the prompt alone.
The model decides what else the request can carry. GET /v1/catalog/video-models lists, per model:
| Catalog field | Request field |
|---|---|
durations, defaultDuration | durationSeconds: snapped up to the next length the model makes (at most its longest). |
resolutions, defaultResolution | resolution. Some models are priced by resolution. |
aspectRatios | aspectRatio. Defaults to the start image’s ratio when the model films it, else ratio_9_16. |
startFrame | imageMediaId / imageUrl |
endFrame | endImageMediaId: a library image to end on (needs a start image). |
maxReferenceImages | referenceMediaIds: library images of a character or style to keep. |
makesSound | audio: true asks for sound with the clip. |
400 naming it, before anything is charged. model defaults to seedance_2_0.
durationSeconds and usedCredits in the answer). Teams on their own provider keys (BYOK) are not charged and need their OpenRouter key; gemini_omni_flash also needs their Gemini key. If the generation fails the credits come back, and one still running after 60 minutes is given up on, reads as FAILED and is refunded too.webhook (with optional metadata) to be told once it is COMPLETED or FAILED: see media webhooks. A clip usually takes one to a few minutes. A FAILED media says why in its error.
curl -X POST "https://api.hooked.so/v1/media/generate/video" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "prompt": "Slow push-in on the serum bottle as water droplets roll down the glass", "model": "seedance_2_0", "imageMediaId": "cm4x9r2t50007ab12gh34ij78", "durationSeconds": 5, "aspectRatio": "ratio_9_16" }'
const headers = { "x-api-key": process.env.HOOKED_API_KEY, "Content-Type": "application/json" };
const response = await fetch("https://api.hooked.so/v1/media/generate/video", {
method: "POST",
headers,
body: JSON.stringify({
prompt: "Slow push-in on the serum bottle as water droplets roll down the glass",
model: "seedance_2_0",
imageMediaId: "cm4x9r2t50007ab12gh34ij78",
durationSeconds: 5,
}),
});
const { data } = await response.json();
let media;
do {
await new Promise((resolve) => setTimeout(resolve, 10000));
media = (await (await fetch(`https://api.hooked.so/v1/media/${data.mediaId}`, { headers })).json()).data;
} while (media.status === "PROCESSING" || media.status === "PENDING");
console.log(media.status, media.url);
import os
import time
import requests
headers = {"x-api-key": os.environ["HOOKED_API_KEY"]}
data = requests.post(
"https://api.hooked.so/v1/media/generate/video",
headers=headers,
json={
"prompt": "Slow push-in on the serum bottle as water droplets roll down the glass",
"model": "seedance_2_0",
"imageMediaId": "cm4x9r2t50007ab12gh34ij78",
"durationSeconds": 5,
},
).json()["data"]
while True:
time.sleep(10)
media = requests.get(f"https://api.hooked.so/v1/media/{data['mediaId']}", headers=headers).json()["data"]
if media["status"] in ("COMPLETED", "FAILED"):
break
print(media["status"], media["url"])
{
"success": true,
"message": "Video generation started",
"data": {
"status": "PROCESSING",
"mediaId": "cm4xa1b2c0009ab12kl56mn90",
"type": "video",
"model": "seedance_2_0",
"aspectRatio": "ratio_9_16",
"durationSeconds": 5,
"resolution": "1080p",
"sourceMediaId": "cm4x9r2t50007ab12gh34ij78",
"usedCredits": 155
}
}
{
"success": false,
"message": "durationSeconds: Seedance 2.0 makes clips of 5, 10 seconds"
}
Errors
Every refusal below happens before anything is charged.| Status | Message | Cause |
|---|---|---|
| 400 | Invalid JSON body | The body is not JSON. |
| 400 | prompt: Required | No prompt, or an empty one (at most 5000 characters). |
| 400 | model: Invalid model "…" | Not an id of GET /v1/catalog/video-models. |
| 400 | imageMediaId: … does not animate an image | The model has no startFrame. |
| 400 | imageUrl: Send imageMediaId or imageUrl, not both | Both start images sent. |
| 400 | imageUrl: … | Not a public https URL, or the image could not be imported (unreachable, not JPEG/PNG/WEBP, over 25 MB). |
| 400 | endImageMediaId: … | The model takes no end frame, or there is no start image. |
| 400 | durationSeconds / aspectRatio / resolution / audio: … | A value the model does not make. |
| 400 | imageMediaId / endImageMediaId / referenceMediaIds: … | An id that is not a finished image of your library, or more references than the model takes. |
| 401 | code: "not_authenticated" | Missing or invalid x-api-key. |
| 402 | INSUFFICIENT_CREDITS / subscription_required / missing_credentials | Not enough credits, no live plan, or a BYOK team without its keys. |
| 403 | code: "entitlement_required" | Your team does not have the Hooked app product. |
Authorizations
Headers
Makes the request safe to retry. 1-255 printable ASCII characters, one per operation (your job id, or a UUID you store), reused on every retry. Within 24 hours the same key with the same body answers with the first response and the header Idempotent-Replayed: true, without running or charging again. Keys are scoped to your team and the endpoint. 5xx answers and refusals before anything ran (401, 402, 403, 409, 429) are not kept. See Idempotency.
1 - 255^[\x20-\x7E]+$Body
What to generate.
5000An id of GET /v1/catalog/video-models.
A finished image of your library to animate from (models with startFrame).
Or a public https URL of the start image (JPEG, PNG or WEBP, up to 25 MB): it is imported into your library first. Not with imageMediaId.
A library image for the clip to end on (models with endFrame). Needs a start image.
Library images of a character or style to keep, at most the model's maxReferenceImages.
Clip length. Snapped up to the next of the model's durations (at most its longest); defaults to the model's defaultDuration.
One of the model's aspectRatios. Defaults to the start image's ratio when the model films it, else ratio_9_16.
ratio_9_16, ratio_1_1, ratio_16_9 One of the model's resolutions; defaults to its defaultResolution. Some models are priced by resolution.
Ask for sound with the clip (models with makesSound). Left out, the model's own default.
Name in your library. Defaults to Generated Image - <date> / Generated Video - <date>.
255Any JSON object of your own (max 5 KB). Sent back in the webhook payload.