curl -X POST "https://api.hooked.so/v1/project/create/quiz-video" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"title": "Geography quiz",
"voiceId": "1004",
"questions": [
{ "question": "What is the capital of Australia?", "options": ["Sydney", "Canberra", "Melbourne"], "correctIndex": 1 },
{ "question": "Which river is the longest?", "options": ["Amazon", "Nile", "Yangtze", "Mississippi"], "correctIndex": 1 }
],
"answerSeconds": 4,
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET"
}'
const response = await fetch("https://api.hooked.so/v1/project/create/quiz-video", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Geography quiz",
voiceId: "1004",
questions: [
{ question: "What is the capital of Australia?", options: ["Sydney", "Canberra", "Melbourne"], correctIndex: 1 },
{
question: "Which river is the longest?",
options: ["Amazon", "Nile", "Yangtze", "Mississippi"],
correctIndex: 1,
},
],
answerSeconds: 4,
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/quiz-video",
headers={"x-api-key": "your_api_key_here"},
json={
"title": "Geography quiz",
"voiceId": "1004",
"questions": [
{"question": "What is the capital of Australia?", "options": ["Sydney", "Canberra", "Melbourne"], "correctIndex": 1},
{
"question": "Which river is the longest?",
"options": ["Amazon", "Nile", "Yangtze", "Mississippi"],
"correctIndex": 1,
},
],
"answerSeconds": 4,
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
},
)
print(response.json()["data"]["projectId"])
{
"success": true,
"data": {
"projectId": "cm4x9k2p10001ab12cd34ef56",
"status": "processing"
},
"message": "Quiz Video successfully created"
}
{
"success": false,
"message": "questions.1.correctIndex: correctIndex 3 is out of range for 3 options"
}
{
"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", "elevenlabs"],
"message": "Your team uses its own provider keys (BYOK) and these are missing or invalid: OpenRouter, ElevenLabs. 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 Quiz Video. Needed: 40, Available: 12",
"creditsNeeded": 40,
"creditsAvailable": 12
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
Quiz Video
Create a trivia video from your questions: each one is narrated, the answers appear, and the correct one is revealed
curl -X POST "https://api.hooked.so/v1/project/create/quiz-video" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"title": "Geography quiz",
"voiceId": "1004",
"questions": [
{ "question": "What is the capital of Australia?", "options": ["Sydney", "Canberra", "Melbourne"], "correctIndex": 1 },
{ "question": "Which river is the longest?", "options": ["Amazon", "Nile", "Yangtze", "Mississippi"], "correctIndex": 1 }
],
"answerSeconds": 4,
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET"
}'
const response = await fetch("https://api.hooked.so/v1/project/create/quiz-video", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Geography quiz",
voiceId: "1004",
questions: [
{ question: "What is the capital of Australia?", options: ["Sydney", "Canberra", "Melbourne"], correctIndex: 1 },
{
question: "Which river is the longest?",
options: ["Amazon", "Nile", "Yangtze", "Mississippi"],
correctIndex: 1,
},
],
answerSeconds: 4,
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/quiz-video",
headers={"x-api-key": "your_api_key_here"},
json={
"title": "Geography quiz",
"voiceId": "1004",
"questions": [
{"question": "What is the capital of Australia?", "options": ["Sydney", "Canberra", "Melbourne"], "correctIndex": 1},
{
"question": "Which river is the longest?",
"options": ["Amazon", "Nile", "Yangtze", "Mississippi"],
"correctIndex": 1,
},
],
"answerSeconds": 4,
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
},
)
print(response.json()["data"]["projectId"])
{
"success": true,
"data": {
"projectId": "cm4x9k2p10001ab12cd34ef56",
"status": "processing"
},
"message": "Quiz Video successfully created"
}
{
"success": false,
"message": "questions.1.correctIndex: correctIndex 3 is out of range for 3 options"
}
{
"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", "elevenlabs"],
"message": "Your team uses its own provider keys (BYOK) and these are missing or invalid: OpenRouter, ElevenLabs. 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 Quiz Video. Needed: 40, Available: 12",
"creditsNeeded": 40,
"creditsAvailable": 12
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
Overview
A quiz video plays your questions one after another. For each question a voice reads it, the answer options appear, a countdown runs, and the correct option turns green. You send the finished questions; nothing is generated from a topic on this endpoint. The background is a gameplay clip by default. It can also be AI images, AI video clips or your own media.projectId right away. The video is generated and rendered in the background.
POST https://api.hooked.so/v1/project/create/quiz-video
Only
questions and voiceId are required. Each question has 2 to 4 options and a zero-based correctIndex (correct_index is accepted too). The background defaults to the subway-s-1 gameplay clip; gameplaySettings applies to gameplay only, presetSettings to ai-images / ai-videos only, and media to media only.
curl -X POST "https://api.hooked.so/v1/project/create/quiz-video" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"title": "Geography quiz",
"voiceId": "1004",
"questions": [
{ "question": "What is the capital of Australia?", "options": ["Sydney", "Canberra", "Melbourne"], "correctIndex": 1 },
{ "question": "Which river is the longest?", "options": ["Amazon", "Nile", "Yangtze", "Mississippi"], "correctIndex": 1 }
],
"answerSeconds": 4,
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET"
}'
const response = await fetch("https://api.hooked.so/v1/project/create/quiz-video", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Geography quiz",
voiceId: "1004",
questions: [
{ question: "What is the capital of Australia?", options: ["Sydney", "Canberra", "Melbourne"], correctIndex: 1 },
{
question: "Which river is the longest?",
options: ["Amazon", "Nile", "Yangtze", "Mississippi"],
correctIndex: 1,
},
],
answerSeconds: 4,
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/quiz-video",
headers={"x-api-key": "your_api_key_here"},
json={
"title": "Geography quiz",
"voiceId": "1004",
"questions": [
{"question": "What is the capital of Australia?", "options": ["Sydney", "Canberra", "Melbourne"], "correctIndex": 1},
{
"question": "Which river is the longest?",
"options": ["Amazon", "Nile", "Yangtze", "Mississippi"],
"correctIndex": 1,
},
],
"answerSeconds": 4,
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
},
)
print(response.json()["data"]["projectId"])
{
"success": true,
"data": {
"projectId": "cm4x9k2p10001ab12cd34ef56",
"status": "processing"
},
"message": "Quiz Video successfully created"
}
{
"success": false,
"message": "questions.1.correctIndex: correctIndex 3 is out of range for 3 options"
}
{
"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", "elevenlabs"],
"message": "Your team uses its own provider keys (BYOK) and these are missing or invalid: OpenRouter, ElevenLabs. 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 Quiz Video. Needed: 40, Available: 12",
"creditsNeeded": 40,
"creditsAvailable": 12
}
{
"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): the project is priced from the number of questions, the answer and reveal times, and the background (media type, quality or model). The credits are taken up front; if the balance is short, the request answers402 INSUFFICIENT_CREDITS and nothing is created. If 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.
- ElevenLabs: for the narration.
- 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: Invalid JSON body, questions: At least 2 questions are required, questions: Cannot have more than 10 questions, questions.0.options: A question needs at least 2 answers, questions.1.correctIndex: correctIndex 3 is out of range for 3 options, voiceId: Voice "…" not found., musicId: Music "…" not found., media: Media is required for media type: media, media: Media "…" not found, presetSettings.preset: Invalid preset "…". Allowed values are: … (or Custom style "custom-…" not found for a custom style that is not your team’s), caption.preset: Invalid caption preset "…". Allowed values are: …, gameplaySettings.selectedGame: Invalid game "…". Allowed values are: …, gameplaySettings.selectedVideo: Video "…" not found in your library (with selectedGame: "custom"), mediaType: "motion-graphics" is not available for this format …, presetSettings.aiModel: "…" does not generate ai-images, webhook: Must be a valid HTTPS URL. |
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": "..." } | Unexpected error. Credits taken for this request 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
Webhooks
List Voices
Reddit Story
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
Between 2 and 10 finished questions, in the order they are played. Nothing is generated from a topic: each question is read by the voice exactly as written, so write them in the language you want them read in.
2 - 10 elementsShow child attributes
Show child attributes
Narrator voice: a library voice ID from /v1/voice/list or one of your team's custom voices. An unknown ID answers 400.
1 - 30Project name (max 100 characters). Defaults to title.
100Quiz title, shown at the top for the whole video (max 120 characters). Also the project name when name is not set.
120Seconds the options stay on screen before the reveal (1-15).
1 <= x <= 15Seconds the revealed answer stays on screen (0.5-6).
0.5 <= x <= 6End with an outro card.
Text of the outro card (max 160 characters). Only with showOutro: true.
160Look of the quiz text and answer rows.
Show child attributes
Show child attributes
Background visuals: a gameplay clip (default), AI images, AI video clips or your own library media. motion-graphics is not offered for this format and answers 400.
gameplay, ai-images, ai-videos, media mediaType ai-images / ai-videos only: the generated background. preset and aiModel apply to both; quality and isContinuous to ai-videos only.
Show child attributes
Show child attributes
mediaType gameplay only: the background clip. Games and their clips: minecraft (minecraft-1 … minecraft-9), subway-s (subway-s-1 … subway-s-11), temple-run (temple-run-1 … temple-run-8), gta (gta-1 … gta-12), fortnite (fortnite-1 … fortnite-5), roblox (roblox-1), free-fire (free-fire-1). A clip that does not exist plays the game's first clip. Use custom as selectedGame to play one of your own library videos: selectedVideo is then its media ID, and an ID that is not a video in your library answers 400.
Show child attributes
Show child attributes
mediaType media only, and required there: IDs of images or videos in your team's media library (max 50). An ID that is not in your library answers 400 (media: Media "<id>" not found). A media that is not COMPLETED yet (an import or upload in progress) answers 400 media: Media "<id>" is not ready (status PROCESSING).
50Media ID from your library
ratio_9_16, ratio_16_9, ratio_1_1 The question text on screen. The quiz draws it in its own style, so only disabled applies.
Show child attributes
Show child attributes
Narrator voice settings (ElevenLabs). Omitted fields keep their defaults.
Show child attributes
Show child attributes
Branding.
Show child attributes
Show child attributes
Background music: a track ID from /v1/music/list or your team's own uploaded music. An unknown ID answers 400.
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.