<!-- Generated from openapi/v2.json (md/b-roll.md) — do not edit. Run: npm run generate -->

[Interactive reference](https://api-docs.syllaby.io/) · [OpenAPI 3.1 spec](https://api-docs.syllaby.io/openapi/v2.json) · [Docs index (llms.txt)](https://api-docs.syllaby.io/llms.txt)

# B-roll

Generate stock-footage videos from a narration script.

## POST /b-roll

**Create a B-roll video** (operationId: `createBrollVideo`)

Creates a new B-roll video in draft state (its `type` is fixed at `b-roll` and cannot be changed later). Returns the created video with its render state embedded under `data.video`. Keep the returned `id` — it identifies the video in every follow-up call, including polling `GET /b-roll/{id}`.

**Requires an active subscription** — the public API has no free tier.

### Request body

Required.

- `title` (string, required) — Display name of the video (max 255 characters).
- `type` (string, required) — Faceless video type — set to `b-roll` for B-roll videos. The type is fixed at creation and cannot be changed later. Allowed values: `b-roll`, `url-based`, `ai-visuals`, `ai-clips`.
- `idea_id` (integer) — Optional identifier of a Syllaby content idea to link the video to. The v2 API exposes no ideas endpoints, so omit it for API-only flows.
- `starts_at` (string) — Optional ISO-8601 timestamp that schedules the video on your Syllaby content calendar. Does not delay the render.
- `ends_at` (string) — Optional ISO-8601 end timestamp for the scheduled calendar slot. Must be the same as or after `starts_at`.

Example request:

```json
{
  "title": "My B-roll video",
  "type": "b-roll"
}
```

### Response `201`

```json
{
  "message": "Success.",
  "status": 201,
  "data": {
    "id": 1,
    "user_id": 1,
    "video_id": 10,
    "voice_id": 12,
    "music_id": null,
    "background_id": null,
    "estimated_duration": 60,
    "type": "b-roll",
    "genre": {
      "id": 3,
      "name": "Cinematic",
      "slug": "cinematic",
      "active": true
    },
    "script": "Three habits that quietly improve your focus.",
    "hash": "abc123",
    "options": {
      "aspect_ratio": "9:16"
    },
    "is_transcribed": false,
    "watermark_id": null,
    "created_at": "2026-01-01T12:00:00.000000Z",
    "updated_at": "2026-01-01T12:00:00.000000Z",
    "video": {
      "id": 10,
      "user_id": 1,
      "idea_id": null,
      "scheduler_id": null,
      "title": "Focus habits",
      "type": "faceless",
      "url": null,
      "status": "draft",
      "retries": 0,
      "hash": "abc123",
      "synced_at": null,
      "metadata": {
        "ai_labels": true,
        "custom_description": null
      },
      "failure": null,
      "created_at": "2026-01-01T12:00:00.000000Z",
      "updated_at": "2026-01-01T12:00:00.000000Z"
    }
  }
}
```

### Errors

- `401` — Unauthenticated
- `403` — No active subscription. The v2 public API has no free tier — every faceless and preset endpoint (reads included) requires an active subscription. Unsubscribed, expired, or canceled callers are rejected with this `403` (code `SUBSCRIPTION-REQUIRED`) before any ownership, credit, or validation check. Only `GET /me`, `GET /credits/costs`, and `GET /credits/history` are reachable without one.
- `422` — Validation error
- `429` — Rate limit exceeded — requests are limited per API token (30 per minute by default). The response carries `Retry-After` (seconds to wait) and `X-RateLimit-Reset` (Unix timestamp when the window resets); back off until then and retry. Successful responses include `X-RateLimit-Limit` and `X-RateLimit-Remaining` so you can pace requests proactively.

Error shapes and examples are shared across endpoints — see the **Error responses** section in [getting-started](getting-started.md).

## PUT /b-roll/{faceless}/scripts

**Generate a B-roll script** (operationId: `generateBrollScript`)

💳 Charges credits. The response `meta.credits` shows the cost and your remaining balance.

Generates a narration script for the B-roll video from a topic, tone, style, language, and target duration, and stores it on the video. This is a `PUT` — each call regenerates and overwrites the previous script.

### Parameters

- `faceless` (path, integer, required) — The faceless ID Example: `1`

### Request body

Required.

- `style` (string, required) — Required — narrative style for the generated script (free-form text, max 255 chars). Examples: educational, storytelling, listicle, conversational, motivational.
- `tone` (string, required) — Tone of voice for generated content (e.g. "professional").
- `language` (string, required) — Language of the content (e.g. "english").
- `topic` (string, required) — Topic or subject of the content.
- `duration` (integer, required) — Target length of the generated script in seconds. Must be one of 30, 60, 180, 300, 600, or 900. Allowed values: `30`, `60`, `180`, `300`, `600`, `900`.

Example request:

```json
{
  "topic": "Morning routines for better focus",
  "tone": "professional",
  "style": "educational",
  "language": "english",
  "duration": 60
}
```

### Response `200`

```json
{
  "message": "Success.",
  "status": 200,
  "data": {
    "id": 1,
    "user_id": 1,
    "video_id": 10,
    "voice_id": 12,
    "music_id": null,
    "background_id": null,
    "estimated_duration": 60,
    "type": "b-roll",
    "genre": {
      "id": 3,
      "name": "Cinematic",
      "slug": "cinematic",
      "active": true
    },
    "script": "Three habits that quietly improve your focus.",
    "hash": "abc123",
    "options": {
      "aspect_ratio": "9:16"
    },
    "is_transcribed": false,
    "watermark_id": null,
    "created_at": "2026-01-01T12:00:00.000000Z",
    "updated_at": "2026-01-01T12:00:00.000000Z",
    "video": {
      "id": 10,
      "user_id": 1,
      "idea_id": null,
      "scheduler_id": null,
      "title": "Focus habits",
      "type": "faceless",
      "url": null,
      "status": "draft",
      "retries": 0,
      "hash": "abc123",
      "synced_at": null,
      "metadata": {
        "ai_labels": true,
        "custom_description": null
      },
      "failure": null,
      "created_at": "2026-01-01T12:00:00.000000Z",
      "updated_at": "2026-01-01T12:00:00.000000Z"
    }
  },
  "meta": {
    "credits": {
      "cost": 5,
      "remaining": 875
    }
  }
}
```

### Errors

- `401` — Unauthenticated
- `402` — Insufficient credits. The request is authenticated and valid, but the account balance is too low — top up and retry.
- `403` — Forbidden — two distinct causes share this status: 1. **No active subscription** (code `SUBSCRIPTION-REQUIRED`, message "An active subscription is required.") — checked first by the active-subscription gate, before any ownership or precondition logic. The v2 public API has no free tier. 2. **Authorization / ownership or precondition failure** — the subscription is active but the action is not allowed: the resource belongs to another account, or a render precondition is unmet (e.g. `A script is required before rendering.`, `Voice was not provided.`, the video is busy, or storage is full).
- `404` — Not found
- `422` — Validation error
- `429` — Rate limit exceeded — requests are limited per API token (30 per minute by default). The response carries `Retry-After` (seconds to wait) and `X-RateLimit-Reset` (Unix timestamp when the window resets); back off until then and retry. Successful responses include `X-RateLimit-Limit` and `X-RateLimit-Remaining` so you can pace requests proactively.

Error shapes and examples are shared across endpoints — see the **Error responses** section in [getting-started](getting-started.md).

## GET /b-roll/{id}

**Get a B-roll video** (operationId: `getBrollVideo`)

Returns a single B-roll video you own. The render state is embedded under `data.video`: `url` (null until the render completes), `status`, and `failure`. Responds with `404` when the id does not exist or belongs to another account. Pass the faceless `id` (not `video_id`).

### Parameters

- `id` (path, string, required) — Identifier of the faceless video. Example: `1`

### Response `200`

```json
{
  "message": "Success.",
  "status": 200,
  "data": {
    "id": 1,
    "user_id": 1,
    "video_id": 10,
    "voice_id": 12,
    "music_id": null,
    "background_id": null,
    "estimated_duration": 60,
    "type": "b-roll",
    "genre": {
      "id": 3,
      "name": "Cinematic",
      "slug": "cinematic",
      "active": true
    },
    "script": "Three habits that quietly improve your focus.",
    "hash": "abc123",
    "options": {
      "aspect_ratio": "9:16"
    },
    "is_transcribed": false,
    "watermark_id": null,
    "created_at": "2026-01-01T12:00:00.000000Z",
    "updated_at": "2026-01-01T12:00:00.000000Z",
    "video": {
      "id": 10,
      "user_id": 1,
      "idea_id": null,
      "scheduler_id": null,
      "title": "Focus habits",
      "type": "faceless",
      "url": "https://cdn.syllaby.dev/videos/10/final.mp4",
      "status": "completed",
      "retries": 0,
      "hash": "abc123",
      "synced_at": "2026-01-01T12:00:00.000000Z",
      "metadata": {
        "ai_labels": true,
        "custom_description": null
      },
      "failure": null,
      "created_at": "2026-01-01T12:00:00.000000Z",
      "updated_at": "2026-01-01T12:00:00.000000Z"
    }
  }
}
```

### Errors

- `401` — Unauthenticated
- `403` — No active subscription. The v2 public API has no free tier — every faceless and preset endpoint (reads included) requires an active subscription. Unsubscribed, expired, or canceled callers are rejected with this `403` (code `SUBSCRIPTION-REQUIRED`) before any ownership, credit, or validation check. Only `GET /me`, `GET /credits/costs`, and `GET /credits/history` are reachable without one.
- `404` — Not found
- `429` — Rate limit exceeded — requests are limited per API token (30 per minute by default). The response carries `Retry-After` (seconds to wait) and `X-RateLimit-Reset` (Unix timestamp when the window resets); back off until then and retry. Successful responses include `X-RateLimit-Limit` and `X-RateLimit-Remaining` so you can pace requests proactively.

Error shapes and examples are shared across endpoints — see the **Error responses** section in [getting-started](getting-started.md).

## PATCH /b-roll/{id}

**Update a B-roll video** (operationId: `updateBrollVideo`)

Updates the B-roll video's configuration (script, voice, genre, captions, transitions, and more). Only the fields you send are changed. The video must not be busy (rendering or syncing).

### Parameters

- `id` (path, string, required) — Identifier of the faceless video. Example: `1`

### Request body

Optional.

- `voice_id` (integer) — Optional — identifier of the narration voice (see faceless options). Omit voice entirely to render without narration.
- `background_id` (integer) — Optional — identifier of the background asset (see faceless options).
- `music_id` (integer|null) — Optional — identifier of the background music track (media id), or null to remove music.
- `genre_id` (integer) — Optional — identifier of the genre/style (see faceless options). Required at render time for the `ai-visuals` and `ai-clips` types.
- `image_engine_id` (integer) — Optional — identifier of the text-to-image engine (see faceless options). Relevant only for the `ai-visuals` and `ai-clips` types.
- `clip_engine_id` (integer) — Optional — identifier of the text-to-video (clip) engine (see faceless options). Relevant only for clip-based types.
- `transition` (string|null) — Optional — transition slug applied between scenes (see faceless options). Allowed values: `slide-left`, `slide-right`, `slide-up`, `slide-down`, `scale-in`, `scale-out`, `zoom-in`, `zoom-out`, `rotate-left`, `rotate-right`, `fade`, `none`, `mixed`, `pop`, `dreamy`, `swing`, `spin-right`, `spin-left`, `swoosh-left`, `swoosh-right`, `glide-left`, `glide-right`, `drop`, `tumble-left`, `tumble-right`, `float`, `rise-left`, `rise-right`, `bounce`, `flash`, `crossfade`, `blur-dissolve`, `zoom-dissolve`.
- `animation` (string|null) — Optional — per-image motion effect slug (e.g. "pan-in"). See faceless options. Allowed values: `pan-in`, `pan-out`, `pan-left`, `pan-right`, `pan-up`, `pan-down`, `rotate-left-in`, `rotate-right-in`, `float`, `none`, `mixed`.
- `sfx` (string|null) — Optional — sound-effect slug applied to the video (see faceless options). Allowed values: `none`, `whoosh`.
- `volume` (string|null) — Optional — background-music volume level: "low", "medium", or "high". Allowed values: `low`, `medium`, `high`.
- `script` (string) — Optional — supply a full custom script to use verbatim instead of generating one. Provide this via PATCH /faceless/{faceless}; the script-generation step is then unnecessary.
- `duration` (integer) — Optional — target video length in seconds. Any positive integer; not restricted to the script-generation presets (those fixed values apply only to PUT /faceless/{faceless}/scripts).
- `aspect_ratio` (string) — Optional — output aspect ratio: "16:9", "9:16", or "1:1". Allowed values: `16:9`, `9:16`, `1:1`.
- `captions` (object) — Optional — caption styling. Only `font_family`, `font_color`, `font_url`, and `position` are honored on update; any other caption keys are ignored.
  - `font_family` (string) — Caption font family slug (see faceless options).
  - `font_color` (string) — Caption font color as a hex value (e.g. "#FFFFFF").
  - `font_url` (string|null) — URL of a custom font file to use for captions.
  - `position` (string) — On-screen position slug (e.g. "bottom", "center", "top"). Allowed values: `top`, `bottom`, `center`.

Example request:

```json
{
  "voice_id": 12,
  "genre_id": 3,
  "script": "Three habits that quietly improve your focus.",
  "transition": "fade",
  "captions": {
    "font_family": "inter",
    "font_color": "#FFFFFF",
    "position": "bottom"
  }
}
```

### Response `200`

```json
{
  "message": "Success.",
  "status": 200,
  "data": {
    "id": 1,
    "user_id": 1,
    "video_id": 10,
    "voice_id": 12,
    "music_id": null,
    "background_id": null,
    "estimated_duration": 60,
    "type": "b-roll",
    "genre": {
      "id": 3,
      "name": "Cinematic",
      "slug": "cinematic",
      "active": true
    },
    "script": "Three habits that quietly improve your focus.",
    "hash": "abc123",
    "options": {
      "aspect_ratio": "9:16"
    },
    "is_transcribed": false,
    "watermark_id": null,
    "created_at": "2026-01-01T12:00:00.000000Z",
    "updated_at": "2026-01-01T12:00:00.000000Z"
  }
}
```

### Errors

- `401` — Unauthenticated
- `403` — Forbidden — two distinct causes share this status: 1. **No active subscription** (code `SUBSCRIPTION-REQUIRED`, message "An active subscription is required.") — checked first by the active-subscription gate, before any ownership or precondition logic. The v2 public API has no free tier. 2. **Authorization / ownership or precondition failure** — the subscription is active but the action is not allowed: the resource belongs to another account, or a render precondition is unmet (e.g. `A script is required before rendering.`, `Voice was not provided.`, the video is busy, or storage is full).
- `404` — Not found
- `422` — Validation error
- `429` — Rate limit exceeded — requests are limited per API token (30 per minute by default). The response carries `Retry-After` (seconds to wait) and `X-RateLimit-Reset` (Unix timestamp when the window resets); back off until then and retry. Successful responses include `X-RateLimit-Limit` and `X-RateLimit-Remaining` so you can pace requests proactively.

Error shapes and examples are shared across endpoints — see the **Error responses** section in [getting-started](getting-started.md).

## GET /b-roll/{faceless}/estimate

**Estimate B-roll render credits** (operationId: `estimateBrollVideo`)

Estimates the credits required to render the B-roll video and compares them against your available balance. The video must already have a script and a voice configured.

### Parameters

- `faceless` (path, integer, required) — The faceless ID Example: `1`

### Response `200`

```json
{
  "message": "Success.",
  "status": 200,
  "data": {
    "required": 30,
    "available": 120,
    "sufficient": true
  }
}
```

### Errors

- `401` — Unauthenticated
- `403` — Forbidden — two distinct causes share this status: 1. **No active subscription** (code `SUBSCRIPTION-REQUIRED`, message "An active subscription is required.") — checked first by the active-subscription gate, before any ownership or precondition logic. The v2 public API has no free tier. 2. **Authorization / ownership or precondition failure** — the subscription is active but the action is not allowed: the resource belongs to another account, or a render precondition is unmet (e.g. `A script is required before rendering.`, `Voice was not provided.`, the video is busy, or storage is full).
- `404` — Not found
- `429` — Rate limit exceeded — requests are limited per API token (30 per minute by default). The response carries `Retry-After` (seconds to wait) and `X-RateLimit-Reset` (Unix timestamp when the window resets); back off until then and retry. Successful responses include `X-RateLimit-Limit` and `X-RateLimit-Remaining` so you can pace requests proactively.

Error shapes and examples are shared across endpoints — see the **Error responses** section in [getting-started](getting-started.md).

## POST /b-roll/{faceless}/render

**Render a B-roll video** (operationId: `renderBrollVideo`)

💳 Charges credits. The response `meta.credits` shows the cost and your remaining balance.

Starts rendering the B-roll video asynchronously and returns HTTP 202. Track progress by polling `GET /b-roll/{id}` and reading `data.video.status` (`rendering` → `completed`/`failed`); the playable file appears at `data.video.url` on completion. Common preconditions checked before any charge: a script and voice must be set, credits must be sufficient (`402`), and the video must not be busy.

### Parameters

- `faceless` (path, integer, required) — The faceless ID Example: `1`

### Response `202`

```json
{
  "message": "Success.",
  "status": 202,
  "data": {
    "id": 1,
    "user_id": 1,
    "video_id": 10,
    "voice_id": 12,
    "music_id": null,
    "background_id": null,
    "estimated_duration": 60,
    "type": "b-roll",
    "genre": {
      "id": 3,
      "name": "Cinematic",
      "slug": "cinematic",
      "active": true
    },
    "script": "Three habits that quietly improve your focus.",
    "hash": "abc123",
    "options": {
      "aspect_ratio": "9:16"
    },
    "is_transcribed": false,
    "watermark_id": null,
    "created_at": "2026-01-01T12:00:00.000000Z",
    "updated_at": "2026-01-01T12:00:00.000000Z"
  },
  "meta": {
    "credits": {
      "cost": 120,
      "remaining": 880
    }
  }
}
```

### Errors

- `401` — Unauthenticated
- `402` — Insufficient credits. The request is authenticated and valid, but the account balance is too low — top up and retry.
- `403` — Forbidden — two distinct causes share this status: 1. **No active subscription** (code `SUBSCRIPTION-REQUIRED`, message "An active subscription is required.") — checked first by the active-subscription gate, before any ownership or precondition logic. The v2 public API has no free tier. 2. **Authorization / ownership or precondition failure** — the subscription is active but the action is not allowed: the resource belongs to another account, or a render precondition is unmet (e.g. `A script is required before rendering.`, `Voice was not provided.`, the video is busy, or storage is full).
- `404` — Not found
- `429` — Rate limit exceeded — requests are limited per API token (30 per minute by default). The response carries `Retry-After` (seconds to wait) and `X-RateLimit-Reset` (Unix timestamp when the window resets); back off until then and retry. Successful responses include `X-RateLimit-Limit` and `X-RateLimit-Remaining` so you can pace requests proactively.

Error shapes and examples are shared across endpoints — see the **Error responses** section in [getting-started](getting-started.md).

## POST /b-roll/{faceless}/retry

**Retry a B-roll render** (operationId: `retryBrollVideo`)

💳 Charges credits. The response `meta.credits` shows the cost and your remaining balance.

Re-attempts rendering for a B-roll video whose embedded video previously failed (`data.video.status` is `failed`). Returns HTTP 202; track it the same way as render. Only failed renders can be retried.

### Parameters

- `faceless` (path, integer, required) — The faceless ID Example: `1`

### Response `202`

```json
{
  "message": "Success.",
  "status": 202,
  "data": {
    "id": 1,
    "user_id": 1,
    "video_id": 10,
    "voice_id": 12,
    "music_id": null,
    "background_id": null,
    "estimated_duration": 60,
    "type": "b-roll",
    "genre": {
      "id": 3,
      "name": "Cinematic",
      "slug": "cinematic",
      "active": true
    },
    "script": "Three habits that quietly improve your focus.",
    "hash": "abc123",
    "options": {
      "aspect_ratio": "9:16"
    },
    "is_transcribed": false,
    "watermark_id": null,
    "created_at": "2026-01-01T12:00:00.000000Z",
    "updated_at": "2026-01-01T12:00:00.000000Z",
    "video": {
      "id": 10,
      "user_id": 1,
      "idea_id": null,
      "scheduler_id": null,
      "title": "Focus habits",
      "type": "faceless",
      "url": null,
      "status": "rendering",
      "retries": 1,
      "hash": "abc123",
      "synced_at": null,
      "metadata": {
        "ai_labels": true,
        "custom_description": null
      },
      "failure": null,
      "created_at": "2026-01-01T12:00:00.000000Z",
      "updated_at": "2026-01-01T12:00:00.000000Z"
    }
  },
  "meta": {
    "credits": {
      "cost": 120,
      "remaining": 880
    }
  }
}
```

### Errors

- `401` — Unauthenticated
- `402` — Insufficient credits. The request is authenticated and valid, but the account balance is too low — top up and retry.
- `403` — Forbidden — two distinct causes share this status: 1. **No active subscription** (code `SUBSCRIPTION-REQUIRED`, message "An active subscription is required.") — checked first by the active-subscription gate, before any ownership or precondition logic. The v2 public API has no free tier. 2. **Authorization / ownership or precondition failure** — the subscription is active but the action is not allowed: the resource belongs to another account, or a render precondition is unmet (e.g. `A script is required before rendering.`, `Voice was not provided.`, the video is busy, or storage is full).
- `404` — Not found
- `429` — Rate limit exceeded — requests are limited per API token (30 per minute by default). The response carries `Retry-After` (seconds to wait) and `X-RateLimit-Reset` (Unix timestamp when the window resets); back off until then and retry. Successful responses include `X-RateLimit-Limit` and `X-RateLimit-Remaining` so you can pace requests proactively.

Error shapes and examples are shared across endpoints — see the **Error responses** section in [getting-started](getting-started.md).

## POST /b-roll/{faceless}/export

**Export a B-roll video** (operationId: `exportBrollVideo`)

💳 Charges credits. The response `meta.credits` shows the cost and your remaining balance.

Rebuilds the rendered B-roll video with the supplied styling (captions, watermark, music, transitions) and queues an export. Returns HTTP 202. The video must already be rendered and not currently busy.

### Parameters

- `faceless` (path, integer, required) — The faceless ID Example: `1`

### Request body

Optional.

- `transition` (string) — Transition slug applied between scenes (see faceless options). Allowed values: `slide-left`, `slide-right`, `slide-up`, `slide-down`, `scale-in`, `scale-out`, `zoom-in`, `zoom-out`, `rotate-left`, `rotate-right`, `fade`, `none`, `mixed`, `pop`, `dreamy`, `swing`, `spin-right`, `spin-left`, `swoosh-left`, `swoosh-right`, `glide-left`, `glide-right`, `drop`, `tumble-left`, `tumble-right`, `float`, `rise-left`, `rise-right`, `bounce`, `flash`, `crossfade`, `blur-dissolve`, `zoom-dissolve`.
- `overlay` (string) — Overlay style slug applied over the video (see faceless options). Allowed values: `none`, `vhs`, `rain`, `glitch`, `dust`, `sparkling-gold`, `spark-effect`, `abstract-particles`.
- `sfx` (string|null) — Sound-effect slug applied to the video (see faceless options). Allowed values: `none`, `whoosh`.
- `volume` (string) — Background-music volume level: "low", "medium", or "high". Allowed values: `low`, `medium`, `high`.
- `music_id` (integer|null) — Identifier of the background music track (media id).
- `transcriptions` (array of array of string) — Per-scene transcription overrides keyed by scene.
- `captions` (object) — Caption styling options (font, color, position, effect).
  - `font_family` (string|null) — Caption font family slug (see faceless options).
  - `font_url` (string|null) — URL of a custom font file to use for captions.
  - `font_color` (string|null) — Caption font color as a hex value (e.g. "#FFFFFF").
  - `position` (string) — On-screen position slug (e.g. "bottom", "center", "top"). Allowed values: `top`, `bottom`, `center`.
  - `effect` (string) — Caption effect slug applied to on-screen text (see faceless options). Allowed values: `karaoke`, `highlight`, `fade`, `bounce`, `slide`, `enlarge`.
- `watermark` (object) — Watermark image and placement options.
  - `id` (integer|null) — Unique numeric identifier of the resource.
  - `position` (string|null) — On-screen position slug (e.g. "bottom", "center", "top"). Allowed values: `top-left`, `top-center`, `top-right`, `middle-left`, `middle-center`, `middle-right`, `bottom-left`, `bottom-center`, `bottom-right`, `none`.
  - `opacity` (integer|null) — Watermark opacity as a percentage (0–100).

Example request:

```json
{
  "transition": "fade",
  "volume": "medium",
  "captions": {
    "font_family": "inter",
    "font_color": "#FFFFFF",
    "position": "bottom",
    "effect": "highlight"
  },
  "watermark": {
    "id": 42,
    "position": "bottom-right",
    "opacity": 80
  }
}
```

### Response `202`

```json
{
  "message": "Success.",
  "status": 202,
  "data": {
    "id": 1,
    "user_id": 1,
    "video_id": 10,
    "voice_id": 12,
    "music_id": null,
    "background_id": null,
    "estimated_duration": 60,
    "type": "b-roll",
    "genre": {
      "id": 3,
      "name": "Cinematic",
      "slug": "cinematic",
      "active": true
    },
    "script": "Three habits that quietly improve your focus.",
    "hash": "abc123",
    "options": {
      "aspect_ratio": "9:16"
    },
    "is_transcribed": false,
    "watermark_id": null,
    "created_at": "2026-01-01T12:00:00.000000Z",
    "updated_at": "2026-01-01T12:00:00.000000Z",
    "video": {
      "id": 10,
      "user_id": 1,
      "idea_id": null,
      "scheduler_id": null,
      "title": "Focus habits",
      "type": "faceless",
      "url": null,
      "status": "rendering",
      "retries": 0,
      "hash": "abc123",
      "synced_at": null,
      "metadata": {
        "ai_labels": true,
        "custom_description": null
      },
      "failure": null,
      "created_at": "2026-01-01T12:00:00.000000Z",
      "updated_at": "2026-01-01T12:00:00.000000Z"
    }
  },
  "meta": {
    "credits": {
      "cost": 50,
      "remaining": 830
    }
  }
}
```

### Errors

- `401` — Unauthenticated
- `402` — Insufficient credits. The request is authenticated and valid, but the account balance is too low — top up and retry.
- `403` — Forbidden — two distinct causes share this status: 1. **No active subscription** (code `SUBSCRIPTION-REQUIRED`, message "An active subscription is required.") — checked first by the active-subscription gate, before any ownership or precondition logic. The v2 public API has no free tier. 2. **Authorization / ownership or precondition failure** — the subscription is active but the action is not allowed: the resource belongs to another account, or a render precondition is unmet (e.g. `A script is required before rendering.`, `Voice was not provided.`, the video is busy, or storage is full).
- `404` — Not found
- `422` — Validation error
- `429` — Rate limit exceeded — requests are limited per API token (30 per minute by default). The response carries `Retry-After` (seconds to wait) and `X-RateLimit-Reset` (Unix timestamp when the window resets); back off until then and retry. Successful responses include `X-RateLimit-Limit` and `X-RateLimit-Remaining` so you can pace requests proactively.

Error shapes and examples are shared across endpoints — see the **Error responses** section in [getting-started](getting-started.md).

## GET /b-roll/{faceless}/assets

**List B-roll assets** (operationId: `listBrollAssets`)

Lists the media assets (images, clips) that make up the B-roll video, ordered by scene. Assets are produced during rendering — expect an empty list until the video has been rendered at least once.

### Parameters

- `faceless` (path, integer, required) — The faceless ID Example: `1`
- `index` (query, string) — Zero-based scene position; returns only the asset at that position. Example: `0`

### Response `200`

```json
{
  "message": "Success.",
  "status": 200,
  "data": [
    {
      "id": 1,
      "user_id": 1,
      "type": "faceless_background",
      "status": "success",
      "order": 0,
      "media": [
        {
          "id": 5,
          "name": "scene-1",
          "file_name": "scene-1.png",
          "mime_type": "image/png",
          "extension": "png",
          "download_url": "https://cdn.syllaby.dev/assets/scene-1.png"
        }
      ],
      "created_at": "2026-01-01T12:00:00.000000Z",
      "updated_at": "2026-01-01T12:00:00.000000Z"
    }
  ]
}
```

### Errors

- `401` — Unauthenticated
- `403` — Forbidden — two distinct causes share this status: 1. **No active subscription** (code `SUBSCRIPTION-REQUIRED`, message "An active subscription is required.") — checked first by the active-subscription gate, before any ownership or precondition logic. The v2 public API has no free tier. 2. **Authorization / ownership or precondition failure** — the subscription is active but the action is not allowed: the resource belongs to another account, or a render precondition is unmet (e.g. `A script is required before rendering.`, `Voice was not provided.`, the video is busy, or storage is full).
- `404` — Not found
- `429` — Rate limit exceeded — requests are limited per API token (30 per minute by default). The response carries `Retry-After` (seconds to wait) and `X-RateLimit-Reset` (Unix timestamp when the window resets); back off until then and retry. Successful responses include `X-RateLimit-Limit` and `X-RateLimit-Remaining` so you can pace requests proactively.

Error shapes and examples are shared across endpoints — see the **Error responses** section in [getting-started](getting-started.md).
