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

# Create a UGC video

> Render the 30 second UGC voiceover video from an audience, concept and voice.

[← UGC videos](/ugc-videos/overview)

**Endpoint:** `POST https://makelocalads.com/api/v1/videos`
**Permission:** `videos:generate`

<Card title="Open in LocalAds" icon="arrow-up-right" href="https://makelocalads.com/app/a1516a75-9be5-461a-93af-c1896a0a3127?tab=studio&type=video-ads&section=create">
  The create-video dialog in the product's Video Ads studio.
</Card>

Render the UGC voiceover recipe: 30 seconds, 60 credits. `concept_id` and
`voice_id` are optional; leave one out and the render chooses, the same as the
app's defaults. Concepts and voices are free to generate and list; see
[Concepts and voices](/ugc-videos/concepts-and-voices).

## Usage notes

* `audience_id` is required: a saved audience of the product, or `null` for a
  deliberately broad video. Omitting it is an error.
* The product's plan must include Video Ads.
* `max_credits` refuses the request with `422` before anything is charged if
  the video would cost more. A render that fails is refunded.
* Send an `Idempotency-Key` so a retry after a network failure never starts
  and charges for a second video.

## Quick start

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl --request POST \
    --url https://makelocalads.com/api/v1/videos \
    --header "Authorization: Bearer $LOCALADS_API_KEY" \
    --header "Content-Type: application/json" \
    --header "Idempotency-Key: video-order-2201" \
    --data '{
      "type": "ugc",
      "product_id": "a1516a75-9be5-461a-93af-c1896a0a3127",
      "recipe_id": "ugc-30s-voiceover",
      "audience_id": "0f0c5a4e-54f7-4f0e-9d4e-1a8c3a6b5d11",
      "concept_id": "5b7e0f7a-6a55-4f0b-9f6c-8f0a2c1d3e4f",
      "voice_id": "9c3b7a1e-2d4f-4e6a-8b0c-1d2e3f4a5b6c",
      "max_credits": 60
    }'
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const response = await fetch("https://makelocalads.com/api/v1/videos", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.LOCALADS_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": "video-order-2201",
    },
    body: JSON.stringify({
      type: "ugc",
      product_id: "a1516a75-9be5-461a-93af-c1896a0a3127",
      recipe_id: "ugc-30s-voiceover",
      audience_id: "0f0c5a4e-54f7-4f0e-9d4e-1a8c3a6b5d11",
      concept_id: "5b7e0f7a-6a55-4f0b-9f6c-8f0a2c1d3e4f",
      voice_id: "9c3b7a1e-2d4f-4e6a-8b0c-1d2e3f4a5b6c",
      max_credits: 60,
    }),
  });

  const video = await response.json();
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import os
  import requests

  response = requests.post(
      "https://makelocalads.com/api/v1/videos",
      headers={
          "Authorization": f"Bearer {os.environ['LOCALADS_API_KEY']}",
          "Content-Type": "application/json",
          "Idempotency-Key": "video-order-2201",
      },
      json={
          "type": "ugc",
          "product_id": "a1516a75-9be5-461a-93af-c1896a0a3127",
          "recipe_id": "ugc-30s-voiceover",
          "audience_id": "0f0c5a4e-54f7-4f0e-9d4e-1a8c3a6b5d11",
          "concept_id": "5b7e0f7a-6a55-4f0b-9f6c-8f0a2c1d3e4f",
          "voice_id": "9c3b7a1e-2d4f-4e6a-8b0c-1d2e3f4a5b6c",
          "max_credits": 60,
      },
  )

  video = response.json()
  ```
</CodeGroup>

## Input fields

<ParamField body="type" type="string" required>
  `ugc` for a UGC voiceover video. Use `animation` to
  [animate a photo](/ugc-videos/animate-a-photo) instead.
</ParamField>

<ParamField body="product_id" type="string" required>
  The product the video is for.
</ParamField>

<ParamField body="recipe_id" type="string">
  The recipe, from `GET /video-recipes`. Today there is one, the 30 second
  UGC voiceover: `ugc-30s-voiceover`. Defaults to it.
</ParamField>

<ParamField body="audience_id" type="string" required>
  A saved audience of the product, from
  `GET /products/{product_id}/audiences`, or `null` for a deliberately broad
  video.
</ParamField>

<ParamField body="concept_id" type="string">
  A concept from `GET /video-concepts` for this product. Optional: the render
  chooses.
</ParamField>

<ParamField body="voice_id" type="string">
  A voice from `GET /voices`. Optional: the render chooses.
</ParamField>

<ParamField body="max_credits" type="integer">
  Refuse the request with `422 credit_limit_exceeded`, before anything is
  charged, if it would cost more than this many credits.
</ParamField>

<Accordion title="Complete JSON schema">
  ```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "type": "object",
    "additionalProperties": false,
    "required": ["type", "product_id", "audience_id"],
    "properties": {
      "type": { "type": "string", "const": "ugc" },
      "product_id": { "type": "string", "format": "uuid" },
      "recipe_id": { "type": "string", "default": "ugc-30s-voiceover" },
      "audience_id": { "type": ["string", "null"], "format": "uuid" },
      "concept_id": { "type": "string", "format": "uuid" },
      "voice_id": { "type": "string", "format": "uuid" },
      "max_credits": { "type": "integer", "minimum": 1 }
    }
  }
  ```
</Accordion>

## Response

`202 Accepted`:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "e2d4c6a8-1b3f-4d5e-9a7c-0f1e2d3c4b5a",
  "type": "ugc",
  "product_id": "a1516a75-9be5-461a-93af-c1896a0a3127",
  "status": "in_progress",
  "title": "The 6 a.m. calm",
  "recipe_id": "ugc-30s-voiceover",
  "audience_id": "0f0c5a4e-54f7-4f0e-9d4e-1a8c3a6b5d11",
  "source_creative_id": null,
  "video_url": null,
  "thumbnail_url": null,
  "duration_seconds": 30,
  "aspect_ratio": "9:16",
  "created_at": "2026-09-25T10:00:00.000Z",
  "credits_charged": 60
}
```

A video is usually ready in about five minutes. Poll
`GET /videos/{video_id}` with backoff until `status` is `completed` or
`failed`; see [Polling](/concepts/polling). A completed video carries
`video_url` and `thumbnail_url`.

## Errors

| Status | Code | Meaning |
| - | - | - |
| `400` | `invalid_request` | `audience_id` is missing (send an audience or `null`) |
| `404` | `product_not_found`, `concept_not_found`, `voice_not_found` | Something you referenced is not available to you |
| `422` | `plan_required` | Your plan does not include Video Ads |
| `422` | `insufficient_credits` | Not enough credits. Nothing was charged |
| `422` | `credit_limit_exceeded` | The video would cost more than `max_credits` |

See [Errors and retries](/concepts/errors) for the error format.
