curl -X POST "https://api.hooked.so/v1/project/create/article-to-video" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"sourceUrl": "https://example.com/blog/remote-work-tips",
"voiceId": "1004",
"summaryType": "summarize",
"targetDuration": 45,
"customPrompt": "For first-time managers, upbeat tone",
"mediaType": "ai-images",
"presetSettings": { "preset": "cinematic" },
"aspectRatio": "ratio_9_16",
"caption": { "preset": "tiktok" },
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
"metadata": { "postId": "1234" }
}'
const response = await fetch("https://api.hooked.so/v1/project/create/article-to-video", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
sourceUrl: "https://example.com/blog/remote-work-tips",
voiceId: "1004",
summaryType: "summarize",
targetDuration: 45,
customPrompt: "For first-time managers, upbeat tone",
mediaType: "ai-images",
presetSettings: { preset: "cinematic" },
aspectRatio: "ratio_9_16",
caption: { preset: "tiktok" },
webhook: "https://example.com/hooked/webhook?token=YOUR_SECRET",
metadata: { postId: "1234" },
}),
});
const { data } = await response.json();
console.log(data.projectId, data.status); // "processing"
import requests
response = requests.post(
"https://api.hooked.so/v1/project/create/article-to-video",
headers={"x-api-key": "your_api_key_here"},
json={
"sourceUrl": "https://example.com/blog/remote-work-tips",
"voiceId": "1004",
"summaryType": "summarize",
"targetDuration": 45,
"customPrompt": "For first-time managers, upbeat tone",
"mediaType": "ai-images",
"presetSettings": {"preset": "cinematic"},
"aspectRatio": "ratio_9_16",
"caption": {"preset": "tiktok"},
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
"metadata": {"postId": "1234"},
},
)
print(response.json()["data"]["projectId"])
{
"success": true,
"data": {
"projectId": "cm4x9k2p10001ab12cd34ef56",
"status": "processing"
},
"message": "Article to Video successfully created"
}
{
"success": false,
"message": "sourceUrl: Must be a public https URL"
}
{
"code": "not_authenticated",
"message": "Not authenticated",
"errorCode": "NOT_AUTHENTICATED",
"details": { "x-api-key": "Header not provided or API Key invalid" }
}
{
"code": "missing_credentials",
"message": "Your team uses its own provider keys (BYOK) and these are missing or invalid: Firecrawl. Add them in Settings → AI keys.",
"errorCode": "MISSING_CREDENTIALS",
"missingProviders": ["firecrawl"],
"details": {}
}
{
"success": false,
"errorCode": "INSUFFICIENT_CREDITS",
"message": "Not enough credits. Needed: 60, Available: 45",
"creditsNeeded": 60,
"creditsAvailable": 45
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
Article to Video
Turn a blog post or news article into a narrated video
curl -X POST "https://api.hooked.so/v1/project/create/article-to-video" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"sourceUrl": "https://example.com/blog/remote-work-tips",
"voiceId": "1004",
"summaryType": "summarize",
"targetDuration": 45,
"customPrompt": "For first-time managers, upbeat tone",
"mediaType": "ai-images",
"presetSettings": { "preset": "cinematic" },
"aspectRatio": "ratio_9_16",
"caption": { "preset": "tiktok" },
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
"metadata": { "postId": "1234" }
}'
const response = await fetch("https://api.hooked.so/v1/project/create/article-to-video", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
sourceUrl: "https://example.com/blog/remote-work-tips",
voiceId: "1004",
summaryType: "summarize",
targetDuration: 45,
customPrompt: "For first-time managers, upbeat tone",
mediaType: "ai-images",
presetSettings: { preset: "cinematic" },
aspectRatio: "ratio_9_16",
caption: { preset: "tiktok" },
webhook: "https://example.com/hooked/webhook?token=YOUR_SECRET",
metadata: { postId: "1234" },
}),
});
const { data } = await response.json();
console.log(data.projectId, data.status); // "processing"
import requests
response = requests.post(
"https://api.hooked.so/v1/project/create/article-to-video",
headers={"x-api-key": "your_api_key_here"},
json={
"sourceUrl": "https://example.com/blog/remote-work-tips",
"voiceId": "1004",
"summaryType": "summarize",
"targetDuration": 45,
"customPrompt": "For first-time managers, upbeat tone",
"mediaType": "ai-images",
"presetSettings": {"preset": "cinematic"},
"aspectRatio": "ratio_9_16",
"caption": {"preset": "tiktok"},
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
"metadata": {"postId": "1234"},
},
)
print(response.json()["data"]["projectId"])
{
"success": true,
"data": {
"projectId": "cm4x9k2p10001ab12cd34ef56",
"status": "processing"
},
"message": "Article to Video successfully created"
}
{
"success": false,
"message": "sourceUrl: Must be a public https URL"
}
{
"code": "not_authenticated",
"message": "Not authenticated",
"errorCode": "NOT_AUTHENTICATED",
"details": { "x-api-key": "Header not provided or API Key invalid" }
}
{
"code": "missing_credentials",
"message": "Your team uses its own provider keys (BYOK) and these are missing or invalid: Firecrawl. Add them in Settings → AI keys.",
"errorCode": "MISSING_CREDENTIALS",
"missingProviders": ["firecrawl"],
"details": {}
}
{
"success": false,
"errorCode": "INSUFFICIENT_CREDITS",
"message": "Not enough credits. Needed: 60, Available: 45",
"creditsNeeded": 60,
"creditsAvailable": 45
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
Overview
Article to Video reads a web page, turns its text into a narration split into scenes, reads it with a voice and puts a visual under each scene. You choose how the text becomes the narration withsummaryType:
summaryType | Narration | Length |
|---|---|---|
summarize (default) | A short summary of the key points. customPrompt can steer it (the angle, the audience, the tone). | About targetDuration seconds (default 30) |
summarize_long | A detailed narration with the supporting details. | About targetDuration seconds |
key_as_is | The article as close to the original as possible, adapted for speech. | As long as the article needs (targetDuration is ignored) |
mediaType, as in Script to Video: ai-images (default), ai-videos, media (your own library files) or gameplay. Motion graphics are not available for this format.
sourceUrl must be a public https:// page; it is read with Firecrawl. The narration is written in the article’s own language.
projectId and status processing. Reading the article, splitting it into scenes and making the video all happen after that. What can be refused up front (an invalid body or URL, missing keys, too few credits) is still answered with a 400 or 402 and nothing is created. If the article cannot be read or summarised later (the page blocks reading, has no text…), the project becomes failed with the reason in message, the credits come back and your webhook gets the failure.POST https://api.hooked.so/v1/project/create/article-to-video
curl -X POST "https://api.hooked.so/v1/project/create/article-to-video" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"sourceUrl": "https://example.com/blog/remote-work-tips",
"voiceId": "1004",
"summaryType": "summarize",
"targetDuration": 45,
"customPrompt": "For first-time managers, upbeat tone",
"mediaType": "ai-images",
"presetSettings": { "preset": "cinematic" },
"aspectRatio": "ratio_9_16",
"caption": { "preset": "tiktok" },
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
"metadata": { "postId": "1234" }
}'
const response = await fetch("https://api.hooked.so/v1/project/create/article-to-video", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
sourceUrl: "https://example.com/blog/remote-work-tips",
voiceId: "1004",
summaryType: "summarize",
targetDuration: 45,
customPrompt: "For first-time managers, upbeat tone",
mediaType: "ai-images",
presetSettings: { preset: "cinematic" },
aspectRatio: "ratio_9_16",
caption: { preset: "tiktok" },
webhook: "https://example.com/hooked/webhook?token=YOUR_SECRET",
metadata: { postId: "1234" },
}),
});
const { data } = await response.json();
console.log(data.projectId, data.status); // "processing"
import requests
response = requests.post(
"https://api.hooked.so/v1/project/create/article-to-video",
headers={"x-api-key": "your_api_key_here"},
json={
"sourceUrl": "https://example.com/blog/remote-work-tips",
"voiceId": "1004",
"summaryType": "summarize",
"targetDuration": 45,
"customPrompt": "For first-time managers, upbeat tone",
"mediaType": "ai-images",
"presetSettings": {"preset": "cinematic"},
"aspectRatio": "ratio_9_16",
"caption": {"preset": "tiktok"},
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
"metadata": {"postId": "1234"},
},
)
print(response.json()["data"]["projectId"])
More request bodies
{
"sourceUrl": "https://example.com/news/space-mission-launch",
"voiceId": "1004",
"summaryType": "key_as_is",
"mediaType": "ai-videos",
"presetSettings": { "preset": "realistic", "quality": "pro" }
}
{
"sourceUrl": "https://example.com/blog/our-new-collection",
"voiceId": "1004",
"summaryType": "summarize_long",
"targetDuration": 90,
"mediaType": "media",
"media": ["cm4x9k2p10001ab12cd34ef56", "cm4x9k2p10002ab12cd34ef56"]
}
{
"sourceUrl": "https://example.com/blog/history-of-coffee",
"voiceId": "1004",
"mediaType": "gameplay",
"gameplaySettings": { "selectedGame": "subway-s", "selectedVideo": "subway-s-3" }
}
{
"success": true,
"data": {
"projectId": "cm4x9k2p10001ab12cd34ef56",
"status": "processing"
},
"message": "Article to Video successfully created"
}
{
"success": false,
"message": "sourceUrl: Must be a public https URL"
}
{
"code": "not_authenticated",
"message": "Not authenticated",
"errorCode": "NOT_AUTHENTICATED",
"details": { "x-api-key": "Header not provided or API Key invalid" }
}
{
"code": "missing_credentials",
"message": "Your team uses its own provider keys (BYOK) and these are missing or invalid: Firecrawl. Add them in Settings → AI keys.",
"errorCode": "MISSING_CREDENTIALS",
"missingProviders": ["firecrawl"],
"details": {}
}
{
"success": false,
"errorCode": "INSUFFICIENT_CREDITS",
"message": "Not enough credits. Needed: 60, Available: 45",
"creditsNeeded": 60,
"creditsAvailable": 45
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
What happens next
- Keep the
projectId. The project starts asprocessingwhile the article is read and split into scenes; if that fails it becomesfailed, with the reason inmessageand the credits refunded. - 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).
Credits and your own keys
Teams on Hooked’s keys (managed): the project is priced fromtargetDuration, the media type and the quality or model, and the credits are taken before the article is read. If the balance is short, the request answers 402 INSUFFICIENT_CREDITS with creditsNeeded and creditsAvailable and nothing is created. If the article cannot be read or the project fails later, the credits are refunded automatically.
Teams on their own keys (BYOK): no credits are charged. The team needs these keys in Settings → AI keys:
- OpenRouter: always (summary, images, video clips, captions).
- Firecrawl: to read the article.
- ElevenLabs: for the voiceover. Not needed in
speakingmode (ai-videoswith a talking preset), where the video model speaks. - Gemini: when
presetSettings.aiModelisgemini_omni_flash.
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: sourceUrl: Required (the https URL of the article), sourceUrl: Must be an https URL, sourceUrl: Must be a public https URL (a private or local address), summaryType: Invalid value "…", customPrompt: Only used with summaryType "summarize", voiceId: Voice "…" not found., musicId: Music "…" not found., mediaType: "motion-graphics" is not available for this format …, media: Media is required for media type: media, media: Media "…" not found, presetSettings.preset: Invalid preset "…", presetSettings.aiModel: "…" does not generate ai-images, presetSettings.characterMode: "speaking" needs mediaType ai-videos, …, gameplaySettings.selectedGame: Invalid game "…", caption.preset: Invalid caption preset "…", articleToVideoSettings.targetDuration: … (outside 10-600), webhook: Must be a valid HTTPS URL, Invalid JSON body. |
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. |
403 | entitlement_required | The team does not have the product this endpoint needs. |
500 | { "success": false, "message": "Failed to create Article to Video project: …" } | The article could not be read (blocked, behind a login, empty) or summarised, or an unexpected error; the credits taken are refunded. |
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
Article to Video example
PDF to Video
Script to Video
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
The article: a public https page. Private or local addresses answer 400. The page is read with Firecrawl.
2000The narrator: a library voice from /v1/voice/list or one of your team's custom voices. An ID that is neither answers 400 (voiceId: Voice "<id>" not found.).
1 - 30How the text becomes the narration:
summarize: a short summary of the key points, abouttargetDurationseconds long.summarize_long: a detailed narration with supporting details, abouttargetDurationseconds long.key_as_is: the text as close to the original as possible, adapted for speech. The video is as long as the text needs;targetDurationis ignored.
summarize, summarize_long, key_as_is Approximate length of the video in seconds (10-600). Not used with key_as_is. It also sets the price of the project.
10 <= x <= 600summarize only: extra instructions for the summary, e.g. the angle or the audience (max 2,000 characters). Sent with another summaryType, it answers 400.
2000Where the visuals come from:
ai-images(default): one AI image per scene (presetSettings:preset,aiModel,characterIds,useCharacterReferenceImage).ai-videos: one AI video clip per scene (presetSettings:preset,quality,aiModel,isContinuous,characterIds,characterMode,voiceProfile).media: your own images and videos from the media library (mediarequired).gameplay: a gameplay clip in the background (gameplaySettings).
motion-graphics is not available for this format (400).
ai-images, ai-videos, media, gameplay Visual settings for ai-images and ai-videos; not used by media and gameplay. Each field says which media types read it.
Show child attributes
Show child attributes
mediaType: "gameplay" only: the background clip.
Show child attributes
Show child attributes
IDs of images or videos in your team's media library (max 50). Required for mediaType: "media"; ignored for the other media types. An ID that is not in your library answers 400 (media: Media "<id>" not found), and so does a media that is not COMPLETED yet.
50Media ID from your library
Project name (max 100 characters). Generated from the source when omitted.
100Background music: a track from /v1/music/list or your team's own uploaded music. An ID that is neither answers 400 (musicId: Music "<id>" not found.).
30Vertical, horizontal or square.
ratio_9_16, ratio_16_9, ratio_1_1 Two-letter ISO 639-1 code of the narration. The narration is written in the article's own language, detected when omitted; this does not translate it.
Burned-in captions of the narration. Not used with motion-graphics (it typesets its own copy) or a speaking cast.
Show child attributes
Show child attributes
Narrator voice settings (ElevenLabs). Any field you leave out keeps its default. Not used by a speaking cast.
Show child attributes
Show child attributes
Add emoji/GIF stickers anchored to the captions. Not used with motion-graphics or a speaking cast.
Branding.
Show child attributes
Show child attributes
HTTPS 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.