curl -X POST "https://api.hooked.so/v1/project/create/cinematic" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"storyBrief": "A lighthouse keeper finds a message in a bottle during a storm and decides to answer it.",
"durationSeconds": 30,
"model": "grok_imagine_video",
"storyTone": "drama",
"presetId": "cinematic",
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET"
}'
const response = await fetch("https://api.hooked.so/v1/project/create/cinematic", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
storyBrief: "A lighthouse keeper finds a message in a bottle during a storm and decides to answer it.",
durationSeconds: 30,
model: "grok_imagine_video",
storyTone: "drama",
presetId: "cinematic",
webhook: "https://example.com/hooked/webhook?token=YOUR_SECRET",
}),
});
const { data } = await response.json();
console.log(data.projectId);
import requests
response = requests.post(
"https://api.hooked.so/v1/project/create/cinematic",
headers={"x-api-key": "your_api_key_here"},
json={
"storyBrief": "A lighthouse keeper finds a message in a bottle during a storm and decides to answer it.",
"durationSeconds": 30,
"model": "grok_imagine_video",
"storyTone": "drama",
"presetId": "cinematic",
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
},
)
print(response.json()["data"]["projectId"])
{
"success": true,
"data": {
"projectId": "cm4x9k2p10001ab12cd34ef56",
"status": "processing"
},
"message": "Cinematic successfully created"
}
{
"success": false,
"message": "model: Invalid model \"sora\". Allowed values are: veo_3_fast, veo_3, gemini_omni_flash, grok_imagine_video, kling_3_0, seedance_2_0, seedance_2_0_fast, veo_3_1_lite, seedance_2_0_mini"
}
{
"code": "not_authenticated",
"message": "Not authenticated",
"errorCode": "NOT_AUTHENTICATED",
"details": { "x-api-key": "Header not provided or API Key invalid" }
}
{
"success": false,
"errorCode": "INSUFFICIENT_CREDITS",
"message": "Not enough credits. Needed: 260, Available: 100",
"creditsNeeded": 260,
"creditsAvailable": 100
}
{
"code": "missing_credentials",
"message": "Your team uses its own provider keys (BYOK) and these are missing or invalid: OpenRouter. Add them in Settings → AI keys.",
"errorCode": "MISSING_CREDENTIALS",
"missingProviders": ["openrouter"],
"details": {}
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
{
"success": false,
"code": "feature_not_available",
"error": "feature_not_available",
"message": "Cinematic videos are not available on this platform."
}
Cinematic Studio
Turn a story brief into a short film with characters who speak
curl -X POST "https://api.hooked.so/v1/project/create/cinematic" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"storyBrief": "A lighthouse keeper finds a message in a bottle during a storm and decides to answer it.",
"durationSeconds": 30,
"model": "grok_imagine_video",
"storyTone": "drama",
"presetId": "cinematic",
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET"
}'
const response = await fetch("https://api.hooked.so/v1/project/create/cinematic", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
storyBrief: "A lighthouse keeper finds a message in a bottle during a storm and decides to answer it.",
durationSeconds: 30,
model: "grok_imagine_video",
storyTone: "drama",
presetId: "cinematic",
webhook: "https://example.com/hooked/webhook?token=YOUR_SECRET",
}),
});
const { data } = await response.json();
console.log(data.projectId);
import requests
response = requests.post(
"https://api.hooked.so/v1/project/create/cinematic",
headers={"x-api-key": "your_api_key_here"},
json={
"storyBrief": "A lighthouse keeper finds a message in a bottle during a storm and decides to answer it.",
"durationSeconds": 30,
"model": "grok_imagine_video",
"storyTone": "drama",
"presetId": "cinematic",
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
},
)
print(response.json()["data"]["projectId"])
{
"success": true,
"data": {
"projectId": "cm4x9k2p10001ab12cd34ef56",
"status": "processing"
},
"message": "Cinematic successfully created"
}
{
"success": false,
"message": "model: Invalid model \"sora\". Allowed values are: veo_3_fast, veo_3, gemini_omni_flash, grok_imagine_video, kling_3_0, seedance_2_0, seedance_2_0_fast, veo_3_1_lite, seedance_2_0_mini"
}
{
"code": "not_authenticated",
"message": "Not authenticated",
"errorCode": "NOT_AUTHENTICATED",
"details": { "x-api-key": "Header not provided or API Key invalid" }
}
{
"success": false,
"errorCode": "INSUFFICIENT_CREDITS",
"message": "Not enough credits. Needed: 260, Available: 100",
"creditsNeeded": 260,
"creditsAvailable": 100
}
{
"code": "missing_credentials",
"message": "Your team uses its own provider keys (BYOK) and these are missing or invalid: OpenRouter. Add them in Settings → AI keys.",
"errorCode": "MISSING_CREDENTIALS",
"missingProviders": ["openrouter"],
"details": {}
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
{
"success": false,
"code": "feature_not_available",
"error": "feature_not_available",
"message": "Cinematic videos are not available on this platform."
}
Overview
Cinematic Studio writes a short film from a brief. Hooked turns the brief into a storyboard (scenes, cast and dialogue), draws a portrait of each character, draws a start frame for each scene, films every scene with the video model you pick (the characters speak with the model’s own audio), then adds subtitles and music and renders the video. It is the same pipeline as Automatic mode in the dashboard’s Cinematic Studio. SendstoryBrief, durationSeconds (10 to 120) and model. Everything else is optional: storyTone, presetId (the visual style), imageModel, characterIds (your characters, from List Characters), preserveExactStoryText, aspectRatio, language (en or es), musicId, name, webhook and metadata.
projectId and status processing. The storyboard, the portraits, the scenes, the final cut and the render all happen after that. What can be refused up front (an invalid body, a brief about a real person, missing keys, too few credits for the whole run) is still answered with a 400, 402 or 404 and nothing is created. If a later step fails, the project becomes failed with the reason in message and your webhook gets the failure.POST https://api.hooked.so/v1/project/create/cinematic
curl -X POST "https://api.hooked.so/v1/project/create/cinematic" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"storyBrief": "A lighthouse keeper finds a message in a bottle during a storm and decides to answer it.",
"durationSeconds": 30,
"model": "grok_imagine_video",
"storyTone": "drama",
"presetId": "cinematic",
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET"
}'
const response = await fetch("https://api.hooked.so/v1/project/create/cinematic", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
storyBrief: "A lighthouse keeper finds a message in a bottle during a storm and decides to answer it.",
durationSeconds: 30,
model: "grok_imagine_video",
storyTone: "drama",
presetId: "cinematic",
webhook: "https://example.com/hooked/webhook?token=YOUR_SECRET",
}),
});
const { data } = await response.json();
console.log(data.projectId);
import requests
response = requests.post(
"https://api.hooked.so/v1/project/create/cinematic",
headers={"x-api-key": "your_api_key_here"},
json={
"storyBrief": "A lighthouse keeper finds a message in a bottle during a storm and decides to answer it.",
"durationSeconds": 30,
"model": "grok_imagine_video",
"storyTone": "drama",
"presetId": "cinematic",
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
},
)
print(response.json()["data"]["projectId"])
More request bodies
{
"storyBrief": "Two rival street racers have to team up when their cars break down in the desert.",
"durationSeconds": 45,
"model": "kling_3_0",
"storyTone": "action",
"presetId": "anime",
"aspectRatio": "ratio_9_16",
"musicId": "1"
}
{
"storyBrief": "Lucía mira por la ventana y dice: \"Mañana me voy\". Su abuelo sonríe y responde: \"Ya lo sabía\".",
"durationSeconds": 15,
"model": "veo_3_fast",
"language": "es",
"preserveExactStoryText": true
}
GET /v1/character/list), add "characterIds": ["<characterId>"] (up to 6) and mention them in the brief as @Name. They keep their look, voice and photos; a character without a photo gets a portrait drawn like any other.
{
"success": true,
"data": {
"projectId": "cm4x9k2p10001ab12cd34ef56",
"status": "processing"
},
"message": "Cinematic successfully created"
}
{
"success": false,
"message": "model: Invalid model \"sora\". Allowed values are: veo_3_fast, veo_3, gemini_omni_flash, grok_imagine_video, kling_3_0, seedance_2_0, seedance_2_0_fast, veo_3_1_lite, seedance_2_0_mini"
}
{
"code": "not_authenticated",
"message": "Not authenticated",
"errorCode": "NOT_AUTHENTICATED",
"details": { "x-api-key": "Header not provided or API Key invalid" }
}
{
"success": false,
"errorCode": "INSUFFICIENT_CREDITS",
"message": "Not enough credits. Needed: 260, Available: 100",
"creditsNeeded": 260,
"creditsAvailable": 100
}
{
"code": "missing_credentials",
"message": "Your team uses its own provider keys (BYOK) and these are missing or invalid: OpenRouter. Add them in Settings → AI keys.",
"errorCode": "MISSING_CREDENTIALS",
"missingProviders": ["openrouter"],
"details": {}
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
{
"success": false,
"code": "feature_not_available",
"error": "feature_not_available",
"message": "Cinematic videos are not available on this platform."
}
Options
Every value these fields accept is listed by Get Catalog withname=cinematic (one item per field, with each model’s durations, resolutions and aspect ratios), read from the same lists this endpoint validates against.
| Field | Values | Default |
|---|---|---|
model | A video model with native audio, e.g. veo_3_fast, kling_3_0, seedance_2_0 | required |
storyTone | e.g. drama, comedy, thriller | drama |
presetId | A visual style of GET /v1/catalog/visual-styles (e.g. realistic, anime, pixar), or custom-<id> for one of your custom styles | realistic |
imageModel | An image model, e.g. gpt_image_2, nano_banana_pro | gpt_image_2 |
aspectRatio | ratio_16_9, ratio_9_16; ratio_1_1 only with the models whose aspectRatios include it | ratio_16_9 |
language | en, es | en |
realistic, cinematic): their face filter refuses realistic people, so that pair answers 400. Pick another model or style. In GET /v1/catalog/cinematic, the presetId item lists those styles with the models each one refuses (excludedModels).
What happens next
- During the request: the body is checked, the whole run is priced and the brief is checked (no real, identifiable or famous people, no minors). The project is created with status
processingand the request answers. - In the background: the storyboard is written and a portrait is drawn for each character; then a start frame is drawn for each scene, one after the other; then every scene is filmed, the subtitles and the music are added and the video is rendered. While the storyboard is being written, the project is
processingwith no scenes and no video yet. - Follow the project with
GET /v1/project/{projectId}untilvideo.statusisCOMPLETED(or the projectstatusisfailed), or wait for yourwebhook.
"status": "COMPLETED" with data.url), or once if the project fails ("status": "FAILED", videoId: null). See Webhooks for every payload.
Credits and your own keys
Teams on Hooked’s keys (managed):- Checked up front. Before anything is paid, the whole run is priced: a portrait for each library character without a photo, plus one start frame and one clip for each 6 seconds of
durationSeconds(filmed at 1080p, the same estimate the dashboard shows). If the balance does not cover it, the request answers402 INSUFFICIENT_CREDITSwithcreditsNeededandcreditsAvailable, and nothing is created or charged. Once the storyboard is written (in the background), the run is priced again with its real scenes and characters; if the balance no longer covers it, the project becomesfailed(“Not enough credits…” inmessage) before anything is charged, and your webhook gets the failure. - Charged in stages, as each one starts: the portraits once the storyboard is written, the scene start frames right after them, the clips once every frame is ready, and the subtitles once every clip is done (if the balance can’t cover the subtitles then, the video completes without them).
- Refunds on failure. A portrait, start frame or clip that fails is refunded, and so is every frame or clip that was paid for but never started when the run stops. A portrait that was drawn is a delivered asset: it stays in your media library and is not refunded, even if the video fails later (for example, when the scene frames can’t be paid for or queued). Once the frames have started, what was already made (frames, finished clips) stays in your media library and is not refunded.
- On the project.
usedCreditsin Get Project is what the run has cost so far: every stage charged, minus what was refunded. It grows as the stages start. On a failed project it is what you kept paying for (the portraits that were drawn).
model is gemini_omni_flash. A missing key answers 402 missing_credentials with the list in missingProviders, before anything is created.
Errors
| Status | Body | When |
|---|---|---|
400 | { "success": false, "message": "<field>: <reason>" } | Validation failed. Examples: storyBrief: Required, durationSeconds: Must be at most 120, model: Invalid model "…". Allowed values are: …, storyTone: Invalid tone "…". Allowed values are: …, imageModel: Invalid image model "…". Allowed values are: …, presetId: Invalid preset "…". Allowed values are: … (or Custom style "custom-…" not found), aspectRatio: "…" does not film ratio_1_1. Allowed values are: …, characterIds: Character "…" not found., musicId: Music "…" not found., webhook: Must be a valid HTTPS URL, Invalid JSON body, or a brief blocked by the content policy (storyBrief: Blocked by content policy: …). |
401 | not_authenticated | Missing or invalid x-api-key. |
402 | missing_credentials | BYOK team without the keys listed above. |
402 | subscription_required | Managed team without an active plan. |
402 | INSUFFICIENT_CREDITS | Managed team without enough credits for the whole run. |
403 | entitlement_required | The team does not have the product this endpoint needs. |
404 | feature_not_available | Cinematic Studio is not offered on this platform. |
500 | { "success": false, "message": "..." } | Unexpected error. A run that fails after the answer (the storyboard, a portrait, the start of the scenes) is not an error response: the project becomes failed with the reason in message, and your webhook is called once. |
Idempotency-Key header to retry safely: if a request times out or answers 5xx, sending it again with the same key and body returns the first answer instead of creating (and charging) the project twice.Writing a brief
Say what happens, who is in it and the mood: “A lighthouse keeper finds a message in a bottle during a storm and decides to answer it” gives the storyboard a character, a place and a turn. Write the dialogue in quotes and setpreserveExactStoryText: true to keep it word for word. Briefs about real, identifiable or famous people are refused; use original characters.
Next Steps
Get Project
Webhooks
Cinematic example
List Music
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 the film is about (1–4,000 characters): the story, who is in it, the mood. Mention a library character with @Name. A brief that recreates a real, identifiable or famous person (or a minor) answers 400.
1 - 4000Target length in seconds (10–120). The storyboard picks the real scene count; credits are checked up front for one 6-second scene per 6 seconds.
10 <= x <= 120Video model that films every scene, with its own audio (the cast speaks). Seedance models can't film the photorealistic styles (realistic, cinematic): that pair answers 400. gemini_omni_flash runs on Gemini.
veo_3_fast, veo_3, gemini_omni_flash, grok_imagine_video, kling_3_0, seedance_2_0, seedance_2_0_fast, veo_3_1_lite, seedance_2_0_mini Genre and pacing of the story. Default: drama.
drama, comedy, action, romance, thriller, documentary, music_video, fantasy, anime Visual style of every scene and of the cast portraits: one of the style ids (realistic, cinematic, anime, pixar, ghibli-studio…, the same list as presetSettings.preset in Script to Video), or custom-<id> for one of your team's custom styles. Default: realistic. An unknown id answers 400 (presetId: Invalid preset "…". Allowed values are: …).
realistic, cinematic, anime, real-anime, retro-anime, cyberpunk-anime, ghibli-studio, pixar, cartoon, comic-book, claymation, fantasy, 80s-fantasy-movie, creative, art-style, sketch-black-and-white, sketch-color, japanese-ink, ink-style, haunted-linework, neon-futuristic, pixel-art, collage, lego, technical-blueprints, stickman, nursery-rhyme, south-park, skeleton-3d, fruit-people, talking-fruit, talking-objects, talking-organs, minecraft, gta-v, free-fire, fortnite, roblox, pubg, bitlife, space-marines-40k, bombardiro-crocodilo, tralalero-tralala, demon-slayer, dragon-ball, one-piece, pokemon, naruto, attack-on-titan, final-fantasy, mecha-break, alien-stage, marvel, dc, star-wars, star-trek, harry-potter, lord-of-the-rings, game-of-thrones, doctor-who, sherlock-holmes, stranger-things, squid-game, wednesday, zelda, genshin-impact, dungeons-and-dragons, rick-and-morty, amazing-digital-circus Image model that draws each scene's start frame. Default: gpt_image_2.
gpt_image_2, nano_banana_pro, nano_banana_2, nano_banana, grok_imagine_image, seedream_5_0, seedream_4_5 Up to 6 characters saved in your team's dashboard (Studio → Characters) to cast: their look, voice and photos are kept; mention them in the brief as @Name. Without it the storyboard invents the cast. An id that is not your team's answers 400 (characterIds: Character "…" not found.).
6Keep the brief's own dialogue and narration word for word instead of rewriting it.
Default: horizontal. ratio_1_1 only with Kling and Seedance; a ratio the model does not film answers 400.
ratio_16_9, ratio_9_16, ratio_1_1 Language the story and its dialogue are written in.
en, es Background music under the whole film: a track from /v1/music/list or your team's own uploaded music. No music when omitted. An ID that is neither answers 400 (musicId: Music "<id>" not found.).
30Project name (max 100 characters). The storyboard's title when omitted.
100HTTPS URL on a public host, called when the video is ready or the project fails (max 500 characters). Requests are signed: verify the Hooked-Signature header (HMAC-SHA256 of ".") with the signing secret from Settings → Webhooks; Hooked-Event and Hooked-Delivery headers name the event and the delivery. No redirects are followed; 10 s timeout; 3 immediate attempts. See the Webhooks guide for the payload.
500Any JSON object of your own (max 5 KB). Stored on the project and sent back in the webhook payload. The key automationId is reserved for dashboard automations and is removed.