> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hooked.so/llms.txt
> Use this file to discover all available pages before exploring further.

# UGC Studio examples

> One request per UGC Studio format: product in hand, app demo, fashion, before/after and voiceover

## Overview

UGC Studio makes user-generated-style ads: an AI avatar speaks your script to camera (voice and lip movement come from the video model, no `voiceId` needed), holding your product, showing your app, wearing your outfits or presenting a before and after. The voiceover format is the exception: a narrator voice reads the script over silent scenes of the avatar.

Each format has its own endpoint, `POST /v1/project/create/ugc-studio/{format}`, and takes its own assets at the top of the body:

| Format | Endpoint | Format fields |
| - | - | - |
| [Product in Hand](/api-reference/ugc-studio/product-in-hand) | `ugc-studio/product-in-hand` | `productImageKey` (required) |
| [App Demo](/api-reference/ugc-studio/app-demo) | `ugc-studio/app-demo` | A screen recording or screenshots in `media`, or `appUrl` with `scrollMode`; `showcaseAfterSeconds` |
| [Outfit / Fashion](/api-reference/ugc-studio/fashion) | `ugc-studio/fashion` | `outfitImageKeys` (up to 4), or your outfit videos in `media` |
| [Before / After](/api-reference/ugc-studio/before-after) | `ugc-studio/before-after` | `beforeImageKey` and `afterImageKey` (both or neither) |
| [Voiceover](/api-reference/ugc-studio/voiceover) | `ugc-studio/voiceover` | `voiceId` (required), `audio`, `productImageKey` |

Get avatar IDs from `GET /v1/avatar/list` and voice IDs from `GET /v1/voice/list`. `media` takes IDs of images or videos in your media library (add them with [Upload Media](/api-reference/media/upload) or [Import Media from URL](/api-reference/media/import)); an ID that is not in your library, or not `COMPLETED` yet, answers `400`. Image fields take HTTPS URLs.

Every create call answers with the `projectId`. Poll `GET /v1/project/{projectId}` or pass a `webhook` to get the finished video.

<Note>
  The older `POST /v1/project/create/ugc-ads`, which names the format inside a `ugcStudio` object, still works but is deprecated. Use the per-format endpoints for new integrations.
</Note>

## Product in Hand

The avatar shows and uses your product. Wrap the words where the product should appear in `[product] … [/product]`. Your `media`, if any, plays as B-roll.

```javascript theme={null}
const response = await fetch('https://api.hooked.so/v1/project/create/ugc-studio/product-in-hand', {
  method: 'POST',
  headers: {
    'x-api-key': process.env.HOOKED_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Serum Review',
    script: "I've been using this serum for two weeks and my skin has never looked better. [product]This is the one.[/product]",
    avatarId: '2',
    productImageKey: 'https://yoursite.com/images/serum.png',
    videoStyle: {
      cameras: ['static-shot', 'slow-zoom-to-the-face'],
      naturalGestures: true
    },
    language: 'en',
    webhook: 'https://yoursite.com/webhook?token=YOUR_SECRET',
    metadata: { campaignId: 'q1-campaign' }
  })
});

const { data } = await response.json();
console.log('Project ID:', data.projectId);
```

## App Demo

The avatar talks about your app while a screen recording covers the frame, with the avatar in a corner card. Send the recording (or screenshots) as `media`, or let Hooked record your site from `appUrl`. `showcaseAfterSeconds` sets when the screencast enters.

```javascript theme={null}
const response = await fetch('https://api.hooked.so/v1/project/create/ugc-studio/app-demo', {
  method: 'POST',
  headers: {
    'x-api-key': process.env.HOOKED_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    script: 'This app plans my whole week in two minutes. Watch how it works.',
    avatarId: '2',
    appUrl: 'https://yourapp.com',
    scrollMode: 'once',
    showcaseAfterSeconds: 3,
    adSettings: {
      bRollType: 'rounded-right',
      avatarPresentation: 'pip',
      removeAvatarBackground: true
    },
    webhook: 'https://yoursite.com/webhook?token=YOUR_SECRET'
  })
});
```

To show your own recording instead, send it as `media` and leave out `appUrl`:

```javascript theme={null}
const response = await fetch('https://api.hooked.so/v1/project/create/ugc-studio/app-demo', {
  method: 'POST',
  headers: {
    'x-api-key': process.env.HOOKED_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    script: 'I stopped using spreadsheets the day I found this app. [app]Look how simple this is.[/app]',
    avatarId: '2',
    media: ['cm8w1r7d20001l708x9y8z7w6']
  })
});
```

## Outfit / Fashion

The avatar wears your outfits, one look per clip. Each photo in `outfitImageKeys` is dressed on the avatar and filmed; wrap the words for each look in `[look 1] … [/look]`, `[look 2] … [/look]`.

```javascript theme={null}
const response = await fetch('https://api.hooked.so/v1/project/create/ugc-studio/fashion', {
  method: 'POST',
  headers: {
    'x-api-key': process.env.HOOKED_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    script: "Three looks from the new drop. [look 1]This one is for weekend coffee runs.[/look] [look 2]And this is what I wear to the office.[/look]",
    avatarId: '2',
    outfitImageKeys: [
      'https://yoursite.com/images/look-casual.jpg',
      'https://yoursite.com/images/look-office.jpg'
    ],
    adSettings: { bRollType: 'down' },
    webhook: 'https://yoursite.com/webhook?token=YOUR_SECRET'
  })
});
```

Already have outfit videos? Send them as `media` instead of `outfitImageKeys` (photos in `media` alone answer `400`).

## Before / After

A transformation ad: the before and the after enter on the lines that name them. Send your own photos as `beforeImageKey` and `afterImageKey`, or leave both out and they are generated from the avatar.

```javascript theme={null}
const response = await fetch('https://api.hooked.so/v1/project/create/ugc-studio/before-after', {
  method: 'POST',
  headers: {
    'x-api-key': process.env.HOOKED_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    script: "[before]This was my living room in January.[/before] [after]And this is it now, after one weekend with this kit.[/after]",
    avatarId: '2',
    beforeImageKey: 'https://yoursite.com/images/room-before.jpg',
    afterImageKey: 'https://yoursite.com/images/room-after.jpg',
    webhook: 'https://yoursite.com/webhook?token=YOUR_SECRET'
  })
});
```

## Voiceover

Nobody talks to camera: the narrator `voiceId` reads the script over silent scenes of the avatar. With `productImageKey`, one scene shows the product alone.

```javascript theme={null}
const response = await fetch('https://api.hooked.so/v1/project/create/ugc-studio/voiceover', {
  method: 'POST',
  headers: {
    'x-api-key': process.env.HOOKED_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    script: 'Mornings used to be chaos. Then I found a coffee maker that has my cup ready before I am out of bed.',
    avatarId: '2',
    voiceId: '1000',
    audio: { speed: 1, stability: 0.5, similarityBoost: 0.75 },
    productImageKey: 'https://yoursite.com/images/coffee-maker.png',
    webhook: 'https://yoursite.com/webhook?token=YOUR_SECRET'
  })
});
```

## Shared fields

Every format also takes `script`, `avatarId`, `name`, `language`, `aspectRatio`, `caption`, `content`, `musicId`, `webhook` and `metadata`. All formats but voiceover take `videoStyle`; app demo and fashion also take the picture-in-picture `adSettings` (`bRollType`, `avatarPresentation`, `removeAvatarBackground`). Each format's reference page lists exactly which fields it reads.

<Note>
  Managed teams pay 7 credits plus 100 per started 15 seconds of speech (estimated from the script, about 2.5 words per second), plus what the format generates on top (product, before/after or dressed images, outfit videos). The voiceover format is priced by its narration and generated scenes instead. The estimate is charged when the project is created and refunded if the project fails.

  Teams in bring-your-own-keys mode are not charged credits for generation, but need their own OpenRouter and Gemini keys in Settings → AI keys, plus Bria when the avatar background is removed in a picture-in-picture format. The voiceover format needs OpenRouter and ElevenLabs instead. Without them the call answers `402` with `code: "missing_credentials"` and the `missingProviders` list, before anything is created or charged. To retry a create call after a timeout or 5xx, send it with an [`Idempotency-Key`](/guides/idempotency): the same key never creates or charges twice.
</Note>

## Tips for UGC Studio

<Tip>**Be authentic**: Write scripts that sound natural and conversational, not salesy</Tip>
<Tip>**Hook in 2 seconds**: Start with attention-grabbing words like "Stop scrolling!" or "POV:"</Tip>
<Tip>**Use 9:16 format**: Vertical videos perform better on TikTok, Reels, and Shorts</Tip>
<Tip>**Keep it short**: 15-30 seconds works best for social media ads</Tip>

## Effective Script Formulas

| Formula | Example |
| - | - |
| **Problem → Solution** | "I used to struggle with X... then I found this!" |
| **Skeptic → Believer** | "I didn't believe the hype, but after trying it..." |
| **Before → After** | "My mornings used to be chaos. Now look at this!" |
| **Social Proof** | "Everyone's been asking about my secret. Here it is!" |
| **Urgency** | "They're almost sold out! I had to share before it's gone" |

## Handling the Webhook Response

Webhooks are signed: check the `Hooked-Signature` header as shown in [Webhooks](/guides/webhooks#verify-the-signature).

```javascript theme={null}
// Express.js webhook handler
app.post('/webhook', (req, res) => {
  if (req.query.token !== process.env.HOOKED_WEBHOOK_TOKEN) return res.sendStatus(401);

  const { status, message, data } = req.body;
  const { videoId, projectId, url, metadata } = data;

  if (status === 'COMPLETED') {
    console.log('Ad completed:', projectId, videoId);
    console.log('Download URL:', url);
    console.log('Campaign:', metadata?.campaignId);
  } else if (status === 'FAILED') {
    // videoId is null when the project failed before it rendered
    console.error(videoId === null ? 'Project failed:' : 'Render failed:', message);
  }

  res.status(200).send('OK');
});
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.