curl -X POST "https://api.hooked.so/v1/project/create/scrolling-video" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"scrollingVideoSettings": {
"websiteUrl": "https://example.com",
"targetDuration": 20,
"scrollMode": "once"
},
"videoSettings": { "aspectRatio": "ratio_16_9" },
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET"
}'
const response = await fetch("https://api.hooked.so/v1/project/create/scrolling-video", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
scrollingVideoSettings: {
websiteUrl: "https://example.com",
targetDuration: 20,
scrollMode: "once",
},
videoSettings: { aspectRatio: "ratio_16_9" },
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/scrolling-video",
headers={"x-api-key": "your_api_key_here"},
json={
"scrollingVideoSettings": {
"websiteUrl": "https://example.com",
"targetDuration": 20,
"scrollMode": "once",
},
"videoSettings": {"aspectRatio": "ratio_16_9"},
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
},
)
print(response.json()["data"]["projectId"])
{
"success": true,
"data": {
"projectId": "cm4x9k2p10001ab12cd34ef56",
"status": "processing"
},
"message": "Scrolling Video successfully created"
}
{
"success": false,
"message": "scrollingVideoSettings.websiteUrl: Invalid website URL"
}
{
"code": "not_authenticated",
"message": "Not authenticated",
"errorCode": "NOT_AUTHENTICATED",
"details": { "x-api-key": "Header not provided or API Key invalid" }
}
{
"success": false,
"code": "missing_credentials",
"errorCode": "MISSING_CREDENTIALS",
"missingProviders": ["openrouter"],
"message": "Your team uses its own provider keys (BYOK) and these are missing or invalid: OpenRouter. Add them in Settings → AI keys."
}
{
"success": false,
"code": "subscription_required",
"errorCode": "SUBSCRIPTION_REQUIRED",
"message": "Your team generates with Hooked's keys, which are paid by your plan. Subscribe to keep generating."
}
{
"success": false,
"errorCode": "INSUFFICIENT_CREDITS",
"message": "Not enough credits for Scrolling Video creation. Estimated: 17, Available: 5",
"creditsNeeded": 17,
"creditsAvailable": 5
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
Scrolling Video
Record a website scrolling as a video
curl -X POST "https://api.hooked.so/v1/project/create/scrolling-video" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"scrollingVideoSettings": {
"websiteUrl": "https://example.com",
"targetDuration": 20,
"scrollMode": "once"
},
"videoSettings": { "aspectRatio": "ratio_16_9" },
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET"
}'
const response = await fetch("https://api.hooked.so/v1/project/create/scrolling-video", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
scrollingVideoSettings: {
websiteUrl: "https://example.com",
targetDuration: 20,
scrollMode: "once",
},
videoSettings: { aspectRatio: "ratio_16_9" },
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/scrolling-video",
headers={"x-api-key": "your_api_key_here"},
json={
"scrollingVideoSettings": {
"websiteUrl": "https://example.com",
"targetDuration": 20,
"scrollMode": "once",
},
"videoSettings": {"aspectRatio": "ratio_16_9"},
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
},
)
print(response.json()["data"]["projectId"])
{
"success": true,
"data": {
"projectId": "cm4x9k2p10001ab12cd34ef56",
"status": "processing"
},
"message": "Scrolling Video successfully created"
}
{
"success": false,
"message": "scrollingVideoSettings.websiteUrl: Invalid website URL"
}
{
"code": "not_authenticated",
"message": "Not authenticated",
"errorCode": "NOT_AUTHENTICATED",
"details": { "x-api-key": "Header not provided or API Key invalid" }
}
{
"success": false,
"code": "missing_credentials",
"errorCode": "MISSING_CREDENTIALS",
"missingProviders": ["openrouter"],
"message": "Your team uses its own provider keys (BYOK) and these are missing or invalid: OpenRouter. Add them in Settings → AI keys."
}
{
"success": false,
"code": "subscription_required",
"errorCode": "SUBSCRIPTION_REQUIRED",
"message": "Your team generates with Hooked's keys, which are paid by your plan. Subscribe to keep generating."
}
{
"success": false,
"errorCode": "INSUFFICIENT_CREDITS",
"message": "Not enough credits for Scrolling Video creation. Estimated: 17, Available: 5",
"creditsNeeded": 17,
"creditsAvailable": 5
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
Overview
Scrolling Video opens a web page, records it while it scrolls and returns that recording as a video. There is no avatar, voiceover or captions: only the page, in the size you choose, with your logo and a background track (musicId, from List Music) if you want them. Use it as a screen capture of a landing page, a store or an article.
The request returns a projectId right away. The recording and the render run in the background.
POST https://api.hooked.so/v1/project/create/scrolling-video
Only
scrollingVideoSettings.websiteUrl is required. The size and the logo go inside videoSettings; the background music is the top-level musicId.
curl -X POST "https://api.hooked.so/v1/project/create/scrolling-video" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"scrollingVideoSettings": {
"websiteUrl": "https://example.com",
"targetDuration": 20,
"scrollMode": "once"
},
"videoSettings": { "aspectRatio": "ratio_16_9" },
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET"
}'
const response = await fetch("https://api.hooked.so/v1/project/create/scrolling-video", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
scrollingVideoSettings: {
websiteUrl: "https://example.com",
targetDuration: 20,
scrollMode: "once",
},
videoSettings: { aspectRatio: "ratio_16_9" },
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/scrolling-video",
headers={"x-api-key": "your_api_key_here"},
json={
"scrollingVideoSettings": {
"websiteUrl": "https://example.com",
"targetDuration": 20,
"scrollMode": "once",
},
"videoSettings": {"aspectRatio": "ratio_16_9"},
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
},
)
print(response.json()["data"]["projectId"])
{
"success": true,
"data": {
"projectId": "cm4x9k2p10001ab12cd34ef56",
"status": "processing"
},
"message": "Scrolling Video successfully created"
}
{
"success": false,
"message": "scrollingVideoSettings.websiteUrl: Invalid website URL"
}
{
"code": "not_authenticated",
"message": "Not authenticated",
"errorCode": "NOT_AUTHENTICATED",
"details": { "x-api-key": "Header not provided or API Key invalid" }
}
{
"success": false,
"code": "missing_credentials",
"errorCode": "MISSING_CREDENTIALS",
"missingProviders": ["openrouter"],
"message": "Your team uses its own provider keys (BYOK) and these are missing or invalid: OpenRouter. Add them in Settings → AI keys."
}
{
"success": false,
"code": "subscription_required",
"errorCode": "SUBSCRIPTION_REQUIRED",
"message": "Your team generates with Hooked's keys, which are paid by your plan. Subscribe to keep generating."
}
{
"success": false,
"errorCode": "INSUFFICIENT_CREDITS",
"message": "Not enough credits for Scrolling Video creation. Estimated: 17, Available: 5",
"creditsNeeded": 17,
"creditsAvailable": 5
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
What happens next
- Keep the
projectId. - Either poll
GET /v1/project/{projectId}untilvideo.statusisCOMPLETED(or the projectstatusisfailed), or pass awebhookand wait for the call. - Download the file from
video.url(ordata.urlin the webhook).
{ "status": "COMPLETED", "message": "Video completed", "data": { "videoId", "projectId", "status": "COMPLETED", "url", "metadata" } }. A project that fails before it has a video sends "status": "FAILED" with videoId: null. See Webhooks.
Credits and your own keys
Teams on Hooked’s keys (managed): 13 credits plus 4 credits per started 30 seconds oftargetDuration (a 30-second video is 17 credits, a 60-second one 21). The balance is checked when you create the project (402 INSUFFICIENT_CREDITS if it is short) and the credits are taken when the recording starts. If the project fails, they are refunded automatically.
Teams on their own keys (BYOK): no credits are charged. The team needs an OpenRouter key in Settings → AI keys (the project name is generated with it). Without it the request answers 402 missing_credentials before anything is created.
Errors
| Status | Body | When |
|---|---|---|
400 | { "success": false, "message": "<field>: <reason>" } | Validation failed. Examples: Invalid JSON body, scrollingVideoSettings.websiteUrl: Required, scrollingVideoSettings.websiteUrl: Invalid website URL, scrollingVideoSettings.targetDuration: Number must be less than or equal to 120, videoSettings.caption.preset: Invalid caption preset "…". Allowed values are: … (caption.preset: … when sent as the flat caption), webhook: Must be a valid HTTPS URL. |
401 | not_authenticated | Missing or invalid x-api-key. |
402 | missing_credentials | BYOK team without an OpenRouter key. |
402 | subscription_required | Managed team without an active plan. |
402 | INSUFFICIENT_CREDITS | Managed team without enough credits. |
403 | entitlement_required | The team does not have the product this endpoint needs. |
500 | { "success": false, "message": "..." } | Unexpected error. |
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.Next Steps
Get Project
Webhooks
Hook + Demo
UGC Studio: App Demo
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 record and how it scrolls.
Show child attributes
Show child attributes
Project name (max 100 characters). Generated from the URL when omitted.
100Output settings. A flat top-level aspectRatio is still accepted; this one wins. No voice or captions are added; background music comes from musicId.
Show child attributes
Show child attributes
Background music ID from /v1/music/list, played under the whole recording. An unknown ID answers 400. Omit it for a silent video.
30HTTPS 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). Sent back in the webhook payload. The key automationId is reserved for dashboard automations and is removed.