curl -X POST "https://api.hooked.so/v1/project/create/ugc-studio/product-in-hand" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Serum ad - Maria",
"script": "I was skeptical, but after two weeks with [product]this serum[/product] my skin has never looked better.",
"avatarId": "2",
"productImageKey": "https://example.com/images/serum.png",
"media": [
"cm8w1r7d20001l708x9y8z7w6"
],
"videoStyle": {
"cameras": [
"static-shot",
"slow-zoom-to-the-face"
]
},
"language": "en",
"caption": {
"preset": "tiktok",
"alignment": "bottom"
},
"webhook": "https://example.com/hooks/hooked?token=YOUR_SECRET",
"metadata": {
"campaign": "serum-q4"
}
}'
const response = await fetch("https://api.hooked.so/v1/project/create/ugc-studio/product-in-hand", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
script: "Okay, this blender changed my mornings. Look how fast it is.",
avatarId: "2",
productImageKey: "https://example.com/images/blender.png",
webhook: "https://example.com/hooks/hooked?token=YOUR_SECRET"
}),
});
const { data } = await response.json();
console.log(data.projectId, data.status);
import requests
response = requests.post(
"https://api.hooked.so/v1/project/create/ugc-studio/product-in-hand",
headers={"x-api-key": "your_api_key_here"},
json={
"script": "I take this bottle everywhere. It keeps water cold for a whole day.",
"avatarId": "2",
"productImageKey": "https://example.com/images/bottle.png",
"webhook": "https://example.com/hooks/hooked?token=YOUR_SECRET"
},
)
data = response.json()["data"]
print(data["projectId"], data["status"])
{
"success": true,
"data": {
"projectId": "cm8x4f2qk0007l708a1b2c3d4",
"status": "processing"
},
"message": "UGC ad successfully created"
}
{
"success": false,
"message": "productImageKey: A product photo is required for the product-in-hand format"
}
{
"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": [["gemini"]],
"message": "Your team uses its own provider keys (BYOK) and these are missing or invalid: Google AI (Gemini). 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 UGC ad creation. Estimated: 107, Available: 40",
"creditsNeeded": 107,
"creditsAvailable": 40
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
Product in Hand
UGC Studio: the avatar shows, prepares and uses your product while talking to camera
curl -X POST "https://api.hooked.so/v1/project/create/ugc-studio/product-in-hand" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Serum ad - Maria",
"script": "I was skeptical, but after two weeks with [product]this serum[/product] my skin has never looked better.",
"avatarId": "2",
"productImageKey": "https://example.com/images/serum.png",
"media": [
"cm8w1r7d20001l708x9y8z7w6"
],
"videoStyle": {
"cameras": [
"static-shot",
"slow-zoom-to-the-face"
]
},
"language": "en",
"caption": {
"preset": "tiktok",
"alignment": "bottom"
},
"webhook": "https://example.com/hooks/hooked?token=YOUR_SECRET",
"metadata": {
"campaign": "serum-q4"
}
}'
const response = await fetch("https://api.hooked.so/v1/project/create/ugc-studio/product-in-hand", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
script: "Okay, this blender changed my mornings. Look how fast it is.",
avatarId: "2",
productImageKey: "https://example.com/images/blender.png",
webhook: "https://example.com/hooks/hooked?token=YOUR_SECRET"
}),
});
const { data } = await response.json();
console.log(data.projectId, data.status);
import requests
response = requests.post(
"https://api.hooked.so/v1/project/create/ugc-studio/product-in-hand",
headers={"x-api-key": "your_api_key_here"},
json={
"script": "I take this bottle everywhere. It keeps water cold for a whole day.",
"avatarId": "2",
"productImageKey": "https://example.com/images/bottle.png",
"webhook": "https://example.com/hooks/hooked?token=YOUR_SECRET"
},
)
data = response.json()["data"]
print(data["projectId"], data["status"])
{
"success": true,
"data": {
"projectId": "cm8x4f2qk0007l708a1b2c3d4",
"status": "processing"
},
"message": "UGC ad successfully created"
}
{
"success": false,
"message": "productImageKey: A product photo is required for the product-in-hand format"
}
{
"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": [["gemini"]],
"message": "Your team uses its own provider keys (BYOK) and these are missing or invalid: Google AI (Gemini). 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 UGC ad creation. Estimated: 107, Available: 40",
"creditsNeeded": 107,
"creditsAvailable": 40
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
Overview
The avatar shows, prepares and uses your product (productImageKey) while talking to camera, and a one-line hook is written at the top of the video. Wrap the words where the product should appear in [product] … [/product]: that also adds a short silent shot of the product alone. Your media, if any, plays as B-roll: full frame in short muted bursts (up to 3 seconds each) over the middle of the video, when it runs longer than 10 seconds. The camera only uses the steady moves, so the product stays framed.
This is one of the five UGC Studio formats; each one has its own endpoint and reads only its own fields. The avatar’s voice, lip movement and gestures are generated with the picture.
curl -X POST "https://api.hooked.so/v1/project/create/ugc-studio/product-in-hand" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Serum ad - Maria",
"script": "I was skeptical, but after two weeks with [product]this serum[/product] my skin has never looked better.",
"avatarId": "2",
"productImageKey": "https://example.com/images/serum.png",
"media": [
"cm8w1r7d20001l708x9y8z7w6"
],
"videoStyle": {
"cameras": [
"static-shot",
"slow-zoom-to-the-face"
]
},
"language": "en",
"caption": {
"preset": "tiktok",
"alignment": "bottom"
},
"webhook": "https://example.com/hooks/hooked?token=YOUR_SECRET",
"metadata": {
"campaign": "serum-q4"
}
}'
const response = await fetch("https://api.hooked.so/v1/project/create/ugc-studio/product-in-hand", {
method: "POST",
headers: {
"x-api-key": "your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
script: "Okay, this blender changed my mornings. Look how fast it is.",
avatarId: "2",
productImageKey: "https://example.com/images/blender.png",
webhook: "https://example.com/hooks/hooked?token=YOUR_SECRET"
}),
});
const { data } = await response.json();
console.log(data.projectId, data.status);
import requests
response = requests.post(
"https://api.hooked.so/v1/project/create/ugc-studio/product-in-hand",
headers={"x-api-key": "your_api_key_here"},
json={
"script": "I take this bottle everywhere. It keeps water cold for a whole day.",
"avatarId": "2",
"productImageKey": "https://example.com/images/bottle.png",
"webhook": "https://example.com/hooks/hooked?token=YOUR_SECRET"
},
)
data = response.json()["data"]
print(data["projectId"], data["status"])
{
"success": true,
"data": {
"projectId": "cm8x4f2qk0007l708a1b2c3d4",
"status": "processing"
},
"message": "UGC ad successfully created"
}
{
"success": false,
"message": "productImageKey: A product photo is required for the product-in-hand format"
}
{
"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": [["gemini"]],
"message": "Your team uses its own provider keys (BYOK) and these are missing or invalid: Google AI (Gemini). 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 UGC ad creation. Estimated: 107, Available: 40",
"creditsNeeded": 107,
"creditsAvailable": 40
}
{
"code": "entitlement_required",
"error": "entitlement_required",
"message": "This endpoint requires the \"app\" product.",
"product": "app"
}
Errors
| Status | Body | When |
|---|---|---|
| 400 | { success: false, message } | Invalid JSON body; Subscription not found (no plan record: contact support); a field fails validation (script: Script must be at least 1 character); productImageKey missing (productImageKey: A product photo is required for the product-in-hand format); a media ID not in your library; an unknown avatarId or musicId; an image key of another team; an invalid caption preset; webhook: Must be a valid HTTPS URL |
| 401 | not_authenticated | Missing or invalid x-api-key |
| 402 | missing_credentials | Your team uses its own keys (BYOK) and a key this request needs is missing. Nothing is created |
| 402 | subscription_required | Your team uses Hooked’s keys and has no active subscription |
| 402 | INSUFFICIENT_CREDITS | The estimate is higher than your balance |
| 403 | entitlement_required | Your team does not have the product this endpoint belongs to |
| 500 | { success: false, message } | Internal server error or Failed to create UGC ad. Credits charged 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.Credits and your own keys
- Teams on Hooked’s keys (managed): 7 credits plus 100 credits per started 15 seconds of script (estimated at about 2.5 words per second), plus the product shot (and the silent product clip when the script marks
[product]). Charged up front when the project is created; refunded if it fails. - Teams on their own keys (BYOK): no credits are charged. The team needs OpenRouter and Google AI (Gemini) in Settings → AI keys; without them the request answers
402 missing_credentialsand nothing is created.
What happens next
The clips are generated, assembled and rendered automatically.- Poll Get Project with the
projectIduntildata.videois present anddata.video.statusisCOMPLETED, then downloaddata.video.url. The project’s ownstatusturnscompletedwhen the clips are assembled, a little before the render finishes;failed(or aFAILEDvideo) means it will not finish. - Or pass a
webhookand get the result when the render finishes. See Webhooks.
Related
List Avatars
Get Project
Webhooks
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 avatar says. Wrap the words where the product should appear in [product] … [/product]; it also adds a short silent shot of the product alone (priced on top). Marks are never spoken; unmarked, the director decides.
1 - 10000Avatar ID from /v1/avatar/list (the library's or your team's own). An unknown ID answers 400.
30Your product photo: a public https image URL or the storage key of an image in your team's library (a key of another team answers 400). The avatar shows, prepares and uses it.
1000Project name (max 100 characters). Generated from the script when omitted. Outside the voiceover format it may be replaced by the on-screen hook the director writes once the clips are planned.
100B-roll: media IDs from your team's library (max 50), shown full frame in short muted bursts (up to 3 s each) over the middle of the video, when it is longer than 10 s. Any ID not in your library answers 400. 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
B-roll. Also accepted as projectSettings.ad.
Show child attributes
Show child attributes
How the avatar is filmed and how it speaks. Every field is optional.
Show child attributes
Show child attributes
Shape of the video.
ratio_9_16, ratio_16_9, ratio_1_1 Language the avatar speaks; detected from the script when omitted. 2-letter ISO 639-1 code, e.g. en, es.
Burned-in captions.
Show child attributes
Show child attributes
Branding.
Show child attributes
Show child attributes
Background music ID from /v1/music/list. 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.