> ## 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.

# Podcast / Dualcast Example

> Create two-person podcast conversations with AI avatars

## Overview

A podcast video is two people talking. You write the dialogue; every turn becomes a clip of the person who says it, and the clips are cut together. Voice, lip movement and gestures come from the video model, so there is no `voiceId` to send: each person keeps the same voice in every clip, and both share one accent.

It comes in two shapes, picked with `mode`:

* **`podcast`** (default): each person filmed on their own, cut back and forth. The host is one of your avatars (`avatarId`); the guest is generated in the same room from a description, or is another avatar.
* **`dualcast`**: both people in the same shot. In each clip one talks while the other listens.

Get avatar IDs from `GET /v1/avatar/list`.

## Writing the Script

Wrap every turn in who says it: `[A] … [/A]` for the host (the person on the left in a dualcast) and `[B] … [/B]` for the guest (on the right). Both people must speak, and the whole dialogue must fit in 24 clips (a long turn is split into several clips between sentences). Inside a turn you can add:

* an action in brackets, in your own words: `[she nods] Totally.` (at the start it happens before speaking);
* `[product] … [/product]` around the words while your product photo (`productImageKey`) is on screen;
* `[camera] … [/camera]` around the words said to the audience instead of to the other person, such as the call to action.

Write few, full turns: while one person talks the other reacts on screen, so a one-word reply on its own wastes a clip.

## Podcast with a Generated Guest

```javascript theme={null}
const response = await fetch('https://api.hooked.so/v1/project/create/podcast-interview', {
  method: 'POST',
  headers: {
    'x-api-key': process.env.HOOKED_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    script: '[A] Eight out of ten people say they struggle to make it to the end of the month, and most of them earn a decent salary. [/A]\n[B] [leans in] Quick question then. Do you actually know where your money goes every single week? [/B]\n[A] Honestly, no. It just disappears between rent, groceries and a few subscriptions I forgot about. [/A]\n[B] [camera] If you want our simple weekly budget template, comment BUDGET below and we will send it over. [/camera] [/B]',
    avatarId: '1',
    guest: {
      source: 'generated',
      description: 'a man in his 30s with a short beard, casual shirt, friendly'
    },
    webhook: 'https://yoursite.com/webhook?token=YOUR_SECRET'
  })
});

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

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

## Podcast with Two Avatars and a Set

The guest can be another avatar, shown in its own photo. `hostSet` first moves the host to a set you describe. `hostVoice`, `guest.voice` and `accent` pick the voices instead of letting them be chosen from each avatar's gender and age.

```javascript theme={null}
const createPodcast = async () => {
  const response = await fetch('https://api.hooked.so/v1/project/create/podcast-interview', {
    method: 'POST',
    headers: {
      'x-api-key': process.env.HOOKED_API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      name: 'Morning Routine Podcast',
      script: '[A] Welcome back to the show. Today we are talking about the one habit that changed my mornings completely. [/A]\n[B] I think I can guess. You stopped checking your phone the second you wake up, right? [/B]\n[A] Exactly. Thirty minutes without a screen, and my focus for the rest of the day is on a different level. [/A]\n[B] [camera] Try it for one week and tell us in the comments how it went for you. [/camera] [/B]',
      avatarId: '1',
      hostSet: 'a cozy podcast studio with acoustic panels and warm lights',
      guest: {
        source: 'avatar',
        avatarId: '3',
        voice: 'man-mid-30s'
      },
      hostVoice: 'young-woman-25-30',
      accent: 'us-neutral-general-american',
      aspectRatio: 'ratio_9_16',
      caption: {
        preset: 'tiktok',
        alignment: 'bottom',
        disabled: false
      },
      musicId: '1',
      webhook: 'https://yoursite.com/webhook?token=YOUR_SECRET',
      metadata: { episode: 12 }
    })
  });

  return await response.json();
};
```

## Dualcast from Two Avatars, with a Product

A dualcast can be made from two avatars, placed side by side on the set you describe in `dualcastSet` (`left` speaks the `[A]` turns, `right` the `[B]` turns). The product photo appears at the top of the screen on the words marked `[product]`.

```javascript theme={null}
const createDualcast = async () => {
  const response = await fetch('https://api.hooked.so/v1/project/create/podcast-interview', {
    method: 'POST',
    headers: {
      'x-api-key': process.env.HOOKED_API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      mode: 'dualcast',
      script: '[A] I have been wearing these running shoes every day for a month and my knees have never felt better. [/A]\n[B] Wait, are those the ones with the extra cushioning you kept telling me about last week? [/B]\n[A] Yes, I will leave a photo up here [product] so you can see exactly which ones they are. [/product] [/A]\n[B] [camera] The link is in our bio if you want to grab a pair before they sell out again. [/camera] [/B]',
      dualcastAvatars: { left: '1', right: '3' },
      dualcastSet: 'a bright living room with plants and a large window',
      productImageKey: 'https://yoursite.com/images/running-shoes.png',
      webhook: 'https://yoursite.com/webhook?token=YOUR_SECRET'
    })
  });

  return await response.json();
};
```

To start from your own picture of the two people instead, send `dualcastImageKey` (an HTTPS URL, or the storage key of an image in your library): it must show exactly two people, A on the left and B on the right, or the call answers `400`. `dualcastAvatarId` does the same with one of your avatars whose image already shows both people.

## Parameters

| Parameter | Type | Required | Description |
| - | - | - | - |
| `script` | string | Yes | The dialogue, `[A] … [/A]` and `[B] … [/B]` turns (max 10,000 characters, up to 24 clips) |
| `mode` | string | No | `podcast` (default) or `dualcast` |
| `avatarId` | string | Podcast | The host (A), from `/v1/avatar/list` or one of your own avatars |
| `guest` | object | Podcast | The guest (B): `{ source: 'generated', description }` (up to 300 characters) or `{ source: 'avatar', avatarId }`, plus an optional `voice`. In a dualcast only `guest.voice` is used |
| `hostSet` | string | No | Podcast: the set the host is moved to first (max 300 characters) |
| `dualcastAvatars` | object | Dualcast | `{ left, right }`: two avatar IDs placed side by side |
| `dualcastSet` | string | No | Dualcast with `dualcastAvatars`: the set they sit on (max 300 characters) |
| `dualcastImageKey` | string | Dualcast | Your image of the two people (HTTPS URL or a storage key from your library) |
| `dualcastAvatarId` | string | Dualcast | One avatar whose image shows both people |
| `hostVoice` | string | No | Voice of A. Picked from the avatar's gender and age when omitted |
| `accent` | string | No | Accent both people speak with; applies only when it matches the video's language |
| `productImageKey` | string | No | Product photo (HTTPS URL or a storage key from your library), shown on the words marked `[product]` |
| `language` | string | No | Two-letter code of the language spoken. Detected from the script when omitted |
| `aspectRatio` | string | No | `ratio_9_16` (default), `ratio_16_9` or `ratio_1_1` |
| `caption` | object | No | `{ preset, alignment, disabled }`. Defaults: `tiktok`, `bottom`, `false` |
| `content` | object | No | `{ brandingLogo: { enabled, url, position, size } }`: your logo over the video |
| `musicId` | string | No | Background music ID from `/v1/music/list` |
| `name` | string | No | Project name (max 100 characters) |
| `webhook` | string | No | HTTPS URL called when the video is ready or the project fails |
| `metadata` | object | No | Your own data (max 5KB), sent back in the webhook |

A dualcast needs one of `dualcastAvatars`, `dualcastImageKey` or `dualcastAvatarId`.

Voices for `hostVoice` and `guest.voice` are the ids of [Get Catalog](/api-reference/catalog/get) with `name=voice-presets` (for example `young-woman-25-30`, `man-mid-30s`).

Accents are the ids of [Get Catalog](/api-reference/catalog/get) with `name=accents` (for example `us-neutral-general-american`, `spanish-mexico`); each says the `language` it speaks.

<Note>
  Managed teams pay 100 credits per started 15 seconds of clips (every turn is its own clip, so a short answer still takes a whole clip), plus the images the pipeline generates: the host's still, the guest, the host's set, or the two avatars side by side. 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. 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 Podcast Videos

<Tip>**Open with a hook**: a surprising fact or a question in the first turn</Tip>
<Tip>**Full turns**: one complete thought per turn; the listener's reactions come for free</Tip>
<Tip>**One call to action**: close with it, wrapped in `[camera] … [/camera]`</Tip>
<Tip>**Few actions**: one short bracketed action now and then is enough</Tip>
<Tip>**Use webhooks**: Always use webhooks in production instead of polling for video status</Tip>

## Use Cases

* **Talking-head podcasts**: Short clips of an interview for TikTok, Reels and Shorts
* **Product conversations**: Two people talking about a product, with its photo on screen
* **Educational content**: A host asking the questions your audience would ask
* **Testimonials**: A conversation instead of a monologue

## 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('Podcast completed:', projectId, videoId);
    console.log('Download URL:', url);
    console.log('Episode:', metadata?.episode);
  } 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.