curl -X POST "https://api.hooked.so/v1/project/create/music-to-video" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"audioUrl": "https://example.com/music/summer-nights.mp3",
"mediaType": "ai-images",
"presetSettings": { "preset": "anime" },
"visualGuidelines": "A road trip along the coast at sunset, warm colors",
"addSoundWave": true,
"aspectRatio": "ratio_9_16",
"caption": { "preset": "tiktok" },
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
"metadata": { "trackId": "summer-nights" }
}'
const response = await fetch("https://api.hooked.so/v1/project/create/music-to-video", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
audioUrl: "https://example.com/music/summer-nights.mp3",
mediaType: "ai-images",
presetSettings: { preset: "anime" },
visualGuidelines: "A road trip along the coast at sunset, warm colors",
addSoundWave: true,
aspectRatio: "ratio_9_16",
caption: { preset: "tiktok" },
webhook: "https://example.com/hooked/webhook?token=YOUR_SECRET",
metadata: { trackId: "summer-nights" },
}),
});
const { data } = await response.json();
console.log(data.projectId, data.status); // "processing"
import requests
response = requests.post(
"https://api.hooked.so/v1/project/create/music-to-video",
headers={"x-api-key": "your_api_key_here"},
json={
"audioUrl": "https://example.com/music/summer-nights.mp3",
"mediaType": "ai-images",
"presetSettings": {"preset": "anime"},
"visualGuidelines": "A road trip along the coast at sunset, warm colors",
"addSoundWave": True,
"aspectRatio": "ratio_9_16",
"caption": {"preset": "tiktok"},
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
"metadata": {"trackId": "summer-nights"},
},
)
print(response.json()["data"]["projectId"])
{
"success": true,
"data": {
"projectId": "cm4x9k2p10001ab12cd34ef56",
"status": "processing"
},
"message": "Music to Video successfully created"
}
{
"success": false,
"message": "audioUrl: The file is not an MP3, M4A, AAC, WAV or OGG audio file"
}
{
"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: OpenRouter. Add them in Settings → AI keys.",
"errorCode": "MISSING_CREDENTIALS",
"missingProviders": ["openrouter"],
"details": {}
}
{
"success": false,
"errorCode": "INSUFFICIENT_CREDITS",
"message": "Not enough credits for Music to Video. Needed: 10, Available: 4",
"creditsNeeded": 10,
"creditsAvailable": 4
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
Music to Video
Turn a song into a music video whose visuals follow the lyrics, cut to the beat
curl -X POST "https://api.hooked.so/v1/project/create/music-to-video" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"audioUrl": "https://example.com/music/summer-nights.mp3",
"mediaType": "ai-images",
"presetSettings": { "preset": "anime" },
"visualGuidelines": "A road trip along the coast at sunset, warm colors",
"addSoundWave": true,
"aspectRatio": "ratio_9_16",
"caption": { "preset": "tiktok" },
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
"metadata": { "trackId": "summer-nights" }
}'
const response = await fetch("https://api.hooked.so/v1/project/create/music-to-video", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
audioUrl: "https://example.com/music/summer-nights.mp3",
mediaType: "ai-images",
presetSettings: { preset: "anime" },
visualGuidelines: "A road trip along the coast at sunset, warm colors",
addSoundWave: true,
aspectRatio: "ratio_9_16",
caption: { preset: "tiktok" },
webhook: "https://example.com/hooked/webhook?token=YOUR_SECRET",
metadata: { trackId: "summer-nights" },
}),
});
const { data } = await response.json();
console.log(data.projectId, data.status); // "processing"
import requests
response = requests.post(
"https://api.hooked.so/v1/project/create/music-to-video",
headers={"x-api-key": "your_api_key_here"},
json={
"audioUrl": "https://example.com/music/summer-nights.mp3",
"mediaType": "ai-images",
"presetSettings": {"preset": "anime"},
"visualGuidelines": "A road trip along the coast at sunset, warm colors",
"addSoundWave": True,
"aspectRatio": "ratio_9_16",
"caption": {"preset": "tiktok"},
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
"metadata": {"trackId": "summer-nights"},
},
)
print(response.json()["data"]["projectId"])
{
"success": true,
"data": {
"projectId": "cm4x9k2p10001ab12cd34ef56",
"status": "processing"
},
"message": "Music to Video successfully created"
}
{
"success": false,
"message": "audioUrl: The file is not an MP3, M4A, AAC, WAV or OGG audio file"
}
{
"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: OpenRouter. Add them in Settings → AI keys.",
"errorCode": "MISSING_CREDENTIALS",
"missingProviders": ["openrouter"],
"details": {}
}
{
"success": false,
"errorCode": "INSUFFICIENT_CREDITS",
"message": "Not enough credits for Music to Video. Needed: 10, Available: 4",
"creditsNeeded": 10,
"creditsAvailable": 4
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
Overview
Music to Video listens to a song, transcribes its lyrics and builds a music video around them: a visual for each part of the song, cut to the beat, with the lyrics as captions and, if you want, an audio-reactive sound wave. The song plays as the soundtrack. Sending the song. Send one of:audioUrl: a publichttps://link to the audio file itself (MP3, M4A, AAC, WAV or OGG), up to 10 MB, the same limit as an upload in the dashboard. Hooked downloads it and reads its type from the file. A private or local address (or a redirect to one), a bigger file, a page instead of a file (a Spotify, YouTube or Suno song page) or anything that is not audio answers400. A Suno CDN link (https://cdn1.suno.ai/<id>.mp3) is a file and works.musicId: a track fromGET /v1/music/listor one of your team’s own audio files.
403 before anything is created.
webhook gets the failure.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. visualGuidelines steers the AI visuals (a setting, a palette, what to avoid) and characterIds keeps the same people across the video. With ai-videos, isContinuous chains each clip to the last frame of the one before; it is off by default, as in the dashboard.
projectId and status processing once the song is downloaded and checked. The transcription, the beat analysis, the media and the render happen after that. What can be refused up front (an invalid body, a link that is not audio, missing keys, too few credits, no storage left) is answered with a 400, 402 or 403 and nothing is created.POST https://api.hooked.so/v1/project/create/music-to-video
curl -X POST "https://api.hooked.so/v1/project/create/music-to-video" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"audioUrl": "https://example.com/music/summer-nights.mp3",
"mediaType": "ai-images",
"presetSettings": { "preset": "anime" },
"visualGuidelines": "A road trip along the coast at sunset, warm colors",
"addSoundWave": true,
"aspectRatio": "ratio_9_16",
"caption": { "preset": "tiktok" },
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
"metadata": { "trackId": "summer-nights" }
}'
const response = await fetch("https://api.hooked.so/v1/project/create/music-to-video", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
audioUrl: "https://example.com/music/summer-nights.mp3",
mediaType: "ai-images",
presetSettings: { preset: "anime" },
visualGuidelines: "A road trip along the coast at sunset, warm colors",
addSoundWave: true,
aspectRatio: "ratio_9_16",
caption: { preset: "tiktok" },
webhook: "https://example.com/hooked/webhook?token=YOUR_SECRET",
metadata: { trackId: "summer-nights" },
}),
});
const { data } = await response.json();
console.log(data.projectId, data.status); // "processing"
import requests
response = requests.post(
"https://api.hooked.so/v1/project/create/music-to-video",
headers={"x-api-key": "your_api_key_here"},
json={
"audioUrl": "https://example.com/music/summer-nights.mp3",
"mediaType": "ai-images",
"presetSettings": {"preset": "anime"},
"visualGuidelines": "A road trip along the coast at sunset, warm colors",
"addSoundWave": True,
"aspectRatio": "ratio_9_16",
"caption": {"preset": "tiktok"},
"webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
"metadata": {"trackId": "summer-nights"},
},
)
print(response.json()["data"]["projectId"])
More request bodies
{
"audioUrl": "https://example.com/music/midnight-drive.m4a",
"mediaType": "ai-videos",
"presetSettings": { "preset": "cinematic", "quality": "pro", "isContinuous": true },
"aspectRatio": "ratio_16_9",
"language": "es"
}
{
"audioUrl": "https://example.com/music/tour-anthem.mp3",
"mediaType": "media",
"media": ["cm4x9k2p10001ab12cd34ef56", "cm4x9k2p10002ab12cd34ef56"],
"addSoundWave": false
}
{
"audioUrl": "https://example.com/music/level-up.mp3",
"mediaType": "gameplay",
"gameplaySettings": { "selectedGame": "subway-s", "selectedVideo": "subway-s-3" },
"caption": { "preset": "beast", "alignment": "middle" }
}
{
"success": true,
"data": {
"projectId": "cm4x9k2p10001ab12cd34ef56",
"status": "processing"
},
"message": "Music to Video successfully created"
}
{
"success": false,
"message": "audioUrl: The file is not an MP3, M4A, AAC, WAV or OGG audio file"
}
{
"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: OpenRouter. Add them in Settings → AI keys.",
"errorCode": "MISSING_CREDENTIALS",
"missingProviders": ["openrouter"],
"details": {}
}
{
"success": false,
"errorCode": "INSUFFICIENT_CREDITS",
"message": "Not enough credits for Music to Video. Needed: 10, Available: 4",
"creditsNeeded": 10,
"creditsAvailable": 4
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
What happens next
- Keep the
projectId. The project staysprocessingwhile the song is transcribed and analysed and the visuals are made; if a step 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): every music video costs a base (the beat analysis and the assembly). Withmedia or gameplay that is all, taken when the project is created. With ai-images and ai-videos the media is priced once the song has been split into sections: per image by the model, or per clip by its length and model, plus the base. A team without the base gets 402 INSUFFICIENT_CREDITS and nothing is created; if the balance is short for the media later, the project fails and nothing is taken. If the project fails, 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 (transcription, project name, images, video clips).
- 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. The song: audioUrl: Required: the https URL of the song (or musicId for a track already in Hooked), audioUrl: Send either audioUrl or musicId, not both, audioUrl: Must be an https URL, audioUrl: Must be a public https URL (…) (a private or local address, or a redirect to one), audioUrl: The audio file is larger than 10 MB, audioUrl: The audio file could not be downloaded (HTTP 404), audioUrl: The file is not an MP3, M4A, AAC, WAV or OGG audio file, musicId: Music "<id>" not found.. The rest: mediaType, media, presetSettings, gameplaySettings, caption, addSoundWave: Must be true or false, visualGuidelines: Must be a string of up to 2,000 characters, language: Must be a two-letter ISO 639-1 code, 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. |
403 | { "success": false, "message": "Insufficient storage. …" } | Your team’s storage has no room for the song. |
500 | { "success": false, "message": "Failed to create Music to Video project: …" } | 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
Music to Video example
List Music
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
A public https link to the song file itself (MP3, M4A, AAC, WAV or OGG, up to 10 MB), e.g. a file in your own storage or a Suno CDN link (https://cdn1.suno.ai/<id>.mp3). Hooked downloads it and reads its type from the file: a private or local address (or a redirect to one), a file over 10 MB, a page (Spotify, YouTube, a Suno song page) or anything that is not audio answers 400. Send this or musicId, not both.
2000Instead of audioUrl: a track from /v1/music/list or one of your team's own audio files (up to 25 MB). The file is copied for this video. An ID that is neither answers 400 (musicId: Music "<id>" not found.). Most library tracks are instrumental: the video follows the lyrics, so use a song with vocals.
30Where the visuals come from:
ai-images(default): one AI image per lyric section (presetSettings:preset,aiModel,characterIds).ai-videos: one AI video clip per section (presetSettings:preset,quality,aiModel,isContinuous,characterIds).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 song when omitted.
100Vertical, horizontal or square.
ratio_9_16, ratio_16_9, ratio_1_1 Two-letter ISO 639-1 code of the song's lyrics. It guides the transcription; detected from the song when omitted.
^[a-zA-Z]{2}$Burned-in captions of the lyrics.
Show child attributes
Show child attributes
Add emoji/GIF stickers anchored to the lyrics.
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.
Overlay an audio-reactive sound wave synced to the song (its style can be changed in the editor).
Extra direction for the AI visuals (ai-images / ai-videos): a setting, a palette, what to avoid. Max 2,000 characters.
2000