<!-- Generated from openapi/v2.json (md/faceless-videos.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)

# Faceless Videos

Create, configure, render, and retrieve faceless videos, their assets, and reusable presets.

## GET /faceless/options

**List faceless options** (operationId: `listFacelessOptions`)

**Reference data** — the slugs/ids for Step 3 (Configure).

Returns all reference data for building a faceless video in one payload, split into two layers. `mandatory` holds the inputs a typical render needs — aspect_ratios, genres, image_engines, voices. `optional` holds the styling and advanced groups — fonts, transitions, backgrounds, caption effects and positions, overlays, watermark positions, animations, sound effects, volumes, clip engines, and characters. Use the returned slugs/ids when creating, updating, or rendering a video.

### Response `200`

```json
{
  "message": "Success.",
  "status": 200,
  "data": {
    "mandatory": {
      "aspect_ratios": [
        {
          "slug": "9:16",
          "label": "Portrait"
        }
      ],
      "genres": [
        {
          "id": 3,
          "name": "Cinematic",
          "slug": "cinematic"
        }
      ],
      "image_engines": [
        {
          "id": 5,
          "name": "Hyperflux",
          "slug": "hyperflux",
          "type": "text-to-image",
          "cost": 2
        }
      ],
      "voices": [
        {
          "id": 12,
          "name": "Aria",
          "language": "english",
          "gender": "female"
        }
      ]
    },
    "optional": {
      "fonts": [
        {
          "name": "Inter",
          "slug": "inter"
        }
      ],
      "transitions": [
        {
          "name": "Fade",
          "slug": "fade"
        }
      ],
      "backgrounds": [
        {
          "id": 7,
          "name": "Studio",
          "slug": "studio"
        }
      ],
      "caption_effects": [
        {
          "name": "None",
          "slug": "none"
        }
      ],
      "caption_positions": [
        {
          "name": "Center",
          "slug": "center"
        }
      ],
      "overlays": [
        {
          "name": "None",
          "slug": "none"
        }
      ],
      "watermark_positions": [
        {
          "name": "Bottom Right",
          "slug": "bottom-right"
        }
      ],
      "animations": [
        {
          "name": "Pan In (Zoom In)",
          "slug": "pan-in"
        }
      ],
      "sfx": [
        {
          "name": "None",
          "slug": "none"
        }
      ],
      "volumes": [
        {
          "name": "Medium",
          "slug": "medium"
        }
      ],
      "clip_engines": [
        {
          "id": 9,
          "name": "Nano Banana",
          "slug": "nano-banana",
          "type": "text-to-video",
          "cost": 4
        }
      ],
      "characters": [
        {
          "id": 2,
          "uuid": "chr_123",
          "name": "Maya",
          "slug": "maya",
          "gender": "female",
          "status": "ready"
        }
      ]
    }
  }
}
```

### 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.
- `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 /faceless

**Create a faceless video** (operationId: `createFacelessVideo`)

**Step 1 of 7 · Create.**

Creates a new faceless video in draft state. Returns the created video — with its render state embedded under `data.video` — so you can configure it (script, voice, visuals) and then render it. This does not start rendering.

**Required:** `title` and `type` — name the video and choose how its visuals are sourced (`type` is fixed at creation and cannot be changed later).

Keep the returned **`id`**: it is the identifier for every follow-up call, including polling `GET /faceless/{id}` (do not use `video_id` for that).

**Requires an active subscription** — no active subscription → `403` `SUBSCRIPTION-REQUIRED` `An active subscription is required.`; the public API has no free tier, so subscribe before creating a video.

→ **Next:** Step 2 — add a script with `PUT /faceless/{faceless}/scripts` (or supply your own via `PATCH /faceless/{faceless}`).

### Request body

Optional.

- `title` (string, required) — Title of the resource.
- `type` (string, required) — Faceless video type — controls how the visuals are sourced: `b-roll` (stock footage), `url-based` (images scraped from a web page — requires `POST /faceless/{faceless}/scrape-images` before render), `ai-visuals` (AI-generated images), or `ai-clips` (AI-generated video clips). Defaults to `ai-visuals`. **The type is fixed at creation and cannot be changed later.** Allowed values: `b-roll`, `url-based`, `ai-visuals`, `ai-clips`.
- `idea_id` (integer) — Identifier of a content idea created in the Syllaby app, linking the video to it. Optional — omit it for API-only flows; the v2 API exposes no ideas endpoints, so there is no way to obtain a valid id through the API.
- `starts_at` (string) — Optional ISO-8601 timestamp that schedules the video on your Syllaby content calendar — sending it creates a calendar event for the video. It does not delay or schedule the render.
- `ends_at` (string) — Optional ISO-8601 timestamp marking the end of the scheduled calendar slot. Must be the same as or after `starts_at`; only meaningful together with it.

Example request:

```json
{
  "title": "My first faceless video",
  "type": "ai-visuals"
}
```

### 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": "faceless",
    "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` — 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).
- `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 /faceless/{faceless}/scripts

**Generate a faceless script** (operationId: `generateFacelessScript`)

**Step 2 of 7 · Add a script.** *(Alternative: skip this generated-script step and supply a full custom script verbatim via `PATCH /faceless/{faceless}` — set the `script` field.)*

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

**Before you can generate a script:** create a faceless video first (`POST /faceless`). This charges credits, so make sure your balance is sufficient — `meta.credits.cost` is the exact amount charged.

Generates a narration script for the faceless video from a topic, tone, style, language, and target duration, and stores it on the video.

**Note the method:** this endpoint is a `PUT` (idempotent replace of the video's script), not a `POST` — each call regenerates the script and overwrites the previous one.

**Preconditions & common errors:**
- **No active subscription** → `403` `SUBSCRIPTION-REQUIRED` `An active subscription is required.` — checked before anything else; the public API has no free tier.
- **Video busy** (rendering or syncing) → `403` — you cannot regenerate the script while the video is processing.
- **Not your video** → `403`.
- **Insufficient credits** → `402` with code `INSUFFICIENT-CREDITS` and `required` / `available` in the error body.
- **Missing required fields** (`topic`, `tone`, `style`, `language`, `duration`) → `422`; `topic` is capped at 500 characters.

→ **Next:** Step 3 — configure voice, genre, captions, and visuals with `PATCH /faceless/{faceless}`.

### 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": "faceless",
    "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.
- `500` — Server error.

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

## POST /faceless/{faceless}/scrape-images

**Scrape images from a URL** (operationId: `scrapeFacelessImages`)

**URL-based flow · Step A · Scrape images** *(free)* — extract images from a web page and attach them to a `url-based` video. Required before rendering.

**Free — no credits charged.** Extracts images from the supplied web page URL and attaches them to the `url-based` faceless video as ordered assets. A subsequent `GET /faceless/{faceless}/assets` will list them.

**This endpoint is only valid for `url-based` videos.** Calling it on any other type returns `422`.

**Before you can render a `url-based` video:** you must call this endpoint at least once. A render attempted without scraped assets returns `422 assets`.

**Preconditions & common errors:**
- **Not a `url-based` video** → `422 faceless` — this step only makes sense for `url-based` videos; the type is fixed at creation.
- **Social-media or unsupported URL** (YouTube, TikTok, Twitter/X, Instagram, Facebook, etc.) → `422 url`.
- **Page un-parseable or unreachable** → `422`.
- **Not your video** → `403`.

→ **Next for url-based:** Step B — generate a script from the same URL with `POST /faceless/{faceless}/scrape-script` (or supply your own via `PATCH /faceless/{faceless}`), then configure (`PATCH`) and render.

### Parameters

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

### Request body

Required. The URL to extract images from.

- `url` (string, required) — Publicly reachable web page URL to extract product/page images from. Social-media URLs (YouTube, TikTok, Twitter/X, Instagram, Facebook, etc.) are rejected with `422`.

Example request:

```json
{
  "url": "https://www.amazon.com/dp/B0CXYZ1234"
}
```

### 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": "url-based",
    "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",
    "assets": [
      {
        "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"
      }
    ],
    "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` — 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).

## POST /faceless/{faceless}/scrape-script

**Scrape a script from a URL** (operationId: `scrapeFacelessScript`)

**URL-based flow · Step B · Scrape script** 💳 — generate a narration script from a web page. Charges `CONTENT_PROMPT_REQUESTED` credits (same as `PUT /faceless/{faceless}/scripts`).

💳 Charges credits (`CONTENT_PROMPT_REQUESTED` — the same amount as `PUT /faceless/{faceless}/scripts`). The response `meta.credits` shows the cost and your remaining balance.

Generates a narration script directly from the content of the supplied web page URL and stores it on the `url-based` faceless video. Use this as an alternative to `PUT /faceless/{faceless}/scripts` when the script should be grounded in page content rather than a free-form topic.

**Note:** this endpoint works on any faceless video type, not just `url-based` — it generates and stores a script regardless of type. You may also skip both scrape-script and `PUT /scripts` and supply your own script verbatim via `PATCH /faceless/{faceless}`.

**Preconditions & common errors:**
- **No active subscription** → `403` `SUBSCRIPTION-REQUIRED` `An active subscription is required.` — checked before anything else; the public API has no free tier.
- **Social-media or unsupported URL** → `422 url`.
- **Page un-parseable or unreachable** → `422`.
- **Not your video** → `403`.
- **Insufficient credits** → `402` with code `INSUFFICIENT-CREDITS` and `required` / `available` in the error body.
- **Missing required fields** (`url`, `duration`, `style`, `tone`, `language`) → `422`.

→ **Next for url-based:** Step 3 — configure voice, captions, and visuals with `PATCH /faceless/{faceless}`, then render.

### Parameters

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

### Request body

Required. The URL to generate a script from, plus generation options.

- `url` (string, required) — Publicly reachable web page URL to extract script content from. Social-media URLs are rejected with `422`.
- `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`.
- `style` (string, 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 the generated script. Examples: professional, friendly, casual, authoritative.
- `language` (string, required) — Language for the generated script (e.g. "english").

Example request:

```json
{
  "url": "https://www.amazon.com/dp/B0CXYZ1234",
  "duration": 60,
  "style": "educational",
  "tone": "friendly",
  "language": "english"
}
```

### 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": "url-based",
    "genre": {
      "id": 3,
      "name": "Cinematic",
      "slug": "cinematic",
      "active": true
    },
    "script": "This product helps you focus throughout the day.",
    "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).

## PATCH /faceless/{faceless}

**Update a faceless video** (operationId: `updateFacelessVideo`)

**Step 3 of 7 · Configure.**

**Before you can update:** the video must not be busy — you cannot edit it while it is rendering or syncing. Wait until it leaves those states.

Updates the configuration of a faceless video (script, voice, genre, captions, transitions, and more). Only the fields you send are changed.

**Render-only settings:** the caption `effect` and the `overlay` are **not** updatable here — this endpoint silently ignores them (the request succeeds with `200`, but the stored value is unchanged). Set them in the render request body (`POST /faceless/{faceless}/render`) instead.

→ **Next:** Step 4 — *(optional)* estimate the cost with `GET /faceless/{faceless}/estimate`, or skip to Step 5 — render with `POST /faceless/{faceless}/render`.

### Parameters

- `faceless` (path, integer, required) — The faceless ID 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": "faceless",
    "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 /faceless/{faceless}/estimate

**Estimate render credits** (operationId: `estimateRenderCredits`)

**Step 4 of 7 · Estimate** *(optional)*.

Estimates the number of credits required to render the given faceless video and compares it against the caller's available balance.

**Before calling:** the faceless must already have a **script** and a **voice** configured. Set them with `PUT /faceless/{faceless}/scripts` (or `PATCH /faceless/{faceless}`) and `PATCH /faceless/{faceless}` (e.g. `{ "voice_id": 12 }`). The estimate reads both from the stored faceless — a `?voice_id=` query parameter is **not** accepted and is ignored.

**Common errors:**
- **No script** (and no voiceover generated yet) → `422` `A script is required before rendering.`
- **No voice** → `422` `Voice was not provided.` (or `Voice not found.` when the stored `voice_id` does not exist).
- **Not your video** → `403`.

**Accuracy:** for per-second (clip-engine) renders the estimate is an approximation — the exact charge is computed during rendering and recorded in the credit ledger (`GET /credits/history`), which is the authoritative record of what was charged.

→ **Next:** Step 5 — render with `POST /faceless/{faceless}/render`.

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

## POST /faceless/{faceless}/render

**Render a faceless video** (operationId: `renderFacelessVideo`)

**Step 5 of 7 · Render.**

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

**Before you can render:** first create a faceless video (`POST /faceless`), then generate or supply its script (`PUT /faceless/{faceless}/scripts`) and configure its options — voice, genre, captions, and visuals (`PATCH /faceless/{faceless}`). Optionally call the estimate endpoint first to check the credit cost against your balance.

Starts rendering the faceless video asynchronously and charges the required credits — `meta.credits.cost` is the amount that will be charged once the pipeline reaches the charge step. For per-second clip-engine renders (`ai-clips`) this is an approximation; the exact charge is computed during rendering and recorded in `GET /credits/history` (the authoritative record). Returns immediately with HTTP 202 while generation runs in the background.

**To track progress:** poll `GET /faceless/{id}` — use the faceless **`id`**, not `video_id` — and read `data.video.status` (the `video` object is always embedded). It moves from `rendering` to `completed` (or `failed`). When complete, the playable file is at `data.video.url`; on failure, `data.video.failure` carries the reason. The faceless object itself has no status field; the render lifecycle lives on the embedded `video`. See the **Get your rendered video** guide in the introduction.

**Preconditions & common errors** (checked before any charge):
- **No script** → `403` `A script is required before rendering.` — set one with `PUT /faceless/{faceless}/scripts` or `PATCH /faceless/{faceless}`.
- **No voice** → `403` `Voice was not provided.` (or `Voice not found.` when `voice_id` does not exist) — set it with `PATCH /faceless/{faceless}`.
- **Insufficient credits** → `402` with code `INSUFFICIENT-CREDITS` and `required` / `available` in the error body — check first with `GET /faceless/{faceless}/estimate`.
- **Video busy** (already rendering or syncing) → `403` `The video is processing currently. Please try again once finished`.
- **Storage full** → `403` `Please remove some files first` (code `REACH-PLAN-STORAGE-LIMIT`).
- **No active subscription** → `403` `SUBSCRIPTION-REQUIRED` `An active subscription is required.` — the public API has no free tier; subscribe before generating.
- **Missing genre** on an `ai-visuals` / `ai-clips` render → `422` `A genre is required for AI visuals and AI clips.`
- **`url-based` video with no scraped assets** → `422` `assets` — call `POST /faceless/{faceless}/scrape-images` first; without scraped images the render would produce an empty video.
- **Missing required fields** (`script`, `duration`, `aspect_ratio`) → `422`.

→ **Next:** Step 6 — track progress by polling `GET /faceless/{id}` (use the faceless `id`, not `video_id`).

### Parameters

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

### Request body

Required. Render configuration for the faceless video.

- `script` (string, required) — Narration script text for the video.
- `duration` (integer, required) — Required — 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, required) — Output aspect ratio. Allowed values are listed under faceless options (aspect_ratios).
- `title` (string) — Optional title for the video.
- `voice_id` (integer) — Identifier of the narration voice (see faceless options).
- `background_id` (integer) — Identifier of the background asset (see faceless options).
- `type` (string) — Faceless video type — controls how the visuals are sourced (stock b-roll, a source URL, AI-generated images, or AI-generated clips). Allowed values: `b-roll`, `url-based`, `ai-visuals`, `ai-clips`.
- `genre_id` (integer) — Identifier of the genre/style that drives the image-prompt look. REQUIRED for image-based renders (`type` of `ai-visuals` or `ai-clips`); optional for `b-roll` / `url-based` renders that don't use a genre. Allowed values are listed under faceless options (genres).
- `character_id` (integer) — Identifier of the consistent character (see faceless options).
- `image_engine_id` (integer) — Identifier of the text-to-image engine (see faceless options).
- `clip_engine_id` (integer) — Identifier of the text-to-video (clip) engine (see faceless options).
- `transition` (string) — Transition slug applied between scenes. Allowed values are listed under faceless options (transitions).
- `animation` (string) — Per-image motion effect slug. Allowed values are listed under faceless options (animations).
- `overlay` (string) — Overlay style slug applied over the video. Allowed values are listed under faceless options (overlays). Render-body-only: set it here (or on export) — `PATCH /faceless/{faceless}` silently ignores it.
- `sfx` (string) — Sound-effect slug applied to the video. Allowed values are listed under faceless options (sfx).
- `ai_labels` (boolean) — Whether AI-content disclosure labels are applied on publish.
- `custom_description` (string) — Custom caption/description applied to published posts.
- `destination_id` (integer) — Identifier of the destination folder/resource to file the video under.
- `captions` (object) — Caption styling. Only `font_family`, `font_color`, `font_url`, `position`, and `effect` are honored; 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) — URL of a custom font file to use for captions.
  - `position` (string) — On-screen caption position slug. Allowed values are listed under faceless options (caption_positions).
  - `effect` (string) — Caption effect slug applied to on-screen text. Allowed values are listed under faceless options (caption_effects). Render-body-only: set it here (or on export) — `PATCH /faceless/{faceless}` silently ignores it.
- `watermark` (object) — Watermark image and placement. Provide exactly one source — `id`, `url`, or `file` (mutually exclusive).
  - `id` (integer) — Identifier of a watermark asset you own. Mutually exclusive with `url` and `file`.
  - `url` (string) — Remote URL of the watermark image. Mutually exclusive with `id` and `file`.
  - `file` (string) — Uploaded watermark image file (jpg, jpeg, png, webp; max 5 MB). Mutually exclusive with `id` and `url`.
  - `position` (string) — Watermark placement slug. Allowed values are listed under faceless options (watermark_positions).
  - `opacity` (integer) — Watermark opacity as a percentage (0–100).
- `music_id` (integer) — Identifier of a background music track (media id). Mutually exclusive with `music.url` and `music.file`.
- `music` (object) — Background music source. Provide exactly one of `url` or `file`, and only when `music_id` is omitted (all three are mutually exclusive).
  - `url` (string) — Remote URL of a music track. Mutually exclusive with `music_id` and `music.file`.
  - `file` (string) — Uploaded music file (mp3, wav, aac, m4a, ogg; max 20 MB). Mutually exclusive with `music_id` and `music.url`.
- `volume` (string) — Background-music volume level. Required when any music source is set. Allowed values: `low`, `medium`, `high`.
- `publications` (array of object) — Social posts to schedule for the rendered video.
  - `channel_id` (integer) — Identifier of the connected social channel to publish to.
  - `scheduled_at` (string) — Future timestamp to publish the post, or null to publish immediately.
- `assets` (array of object) — Explicit ordered media assets to compose the video from. Orders must start at 0 and be consecutive with no gaps.
  - `id` (integer, required) — Identifier of the media asset.
  - `order` (integer, required) — Zero-based position of the asset in the sequence.

Example request:

```json
{
  "script": "Three small habits that quietly improve your focus every day.",
  "duration": 60,
  "aspect_ratio": "9:16",
  "type": "ai-visuals",
  "title": "Focus habits",
  "voice_id": 12,
  "genre_id": 3,
  "image_engine_id": 1,
  "clip_engine_id": 2,
  "transition": "fade",
  "animation": "pan-in",
  "overlay": "none",
  "sfx": "whoosh",
  "volume": "medium",
  "music_id": 8,
  "captions": {
    "font_family": "inter",
    "font_color": "#FFFFFF",
    "position": "bottom",
    "effect": "highlight"
  },
  "watermark": {
    "id": 42,
    "position": "bottom-right",
    "opacity": 80
  },
  "publications": [
    {
      "channel_id": 5,
      "scheduled_at": "2025-02-01T18:30:00Z"
    }
  ],
  "assets": [
    {
      "id": 101,
      "order": 0
    },
    {
      "id": 102,
      "order": 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": "faceless",
    "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
- `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.
- `500` — Server error.

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

## GET /faceless/{id}

**Get a faceless video** (operationId: `getFacelessVideo`)

**Step 6 of 7 · Track progress.**

Returns a single faceless video you own. The render state is always embedded under `data.video` — no `include` needed: `url` (null until the render completes), `status`, and `failure` (`code` + `message`, non-null only when the render failed).

Use the `include` query parameter to embed further related resources; values outside the documented allowlist are rejected with `400 Bad Request`.

Responds with `404 Not Found` when the id does not exist **or** belongs to another account. Make sure you pass the faceless **`id`** (from the create/render response), not `video_id` — they are different identifiers, and polling with `video_id` is the most common cause of unexpected 404s.

→ **Next:** when `data.video.status` is `completed`, you have your video — *(optional)* Step 7, export a restyled cut with `POST /faceless/{faceless}/export`. If it is `failed`, retry with `POST /faceless/{faceless}/retry`.

### Parameters

- `id` (path, integer, required) — Identifier of the faceless video. Example: `1`
- `include` (query, string) — Comma-separated list of related resources to embed. Allowed values: `video`, `captions`, `media`, `music`, `voice`, `background`, `genre`, `watermark`, `character`, `assets`. Any other value is rejected with `400 Bad Request`. Note that `video` is always embedded in the response — you never need `include` to read render progress. Example: `captions,voice`

### 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": "faceless",
    "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:05: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

- `400` — Bad request — e.g. an `include` value outside the endpoint's allowlist.
- `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 /faceless/{faceless}/retry

**Retry a faceless render** (operationId: `retryFacelessRender`)

**Step 6 of 7 · Track progress — retry a failed render.**

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

**Before you can retry:** the embedded video must have **failed** — fetch `GET /faceless/{id}` and check that `data.video.status` is `failed` (the reason is in `data.video.failure`). The video must also not be busy (rendering or syncing). Retry does nothing for drafts or already-completed videos.

Re-attempts rendering for a faceless video that previously failed. Returns HTTP 202 with the video state embedded under `data.video` while the retry runs asynchronously; track it the same way as render.

**Credits:** for a video that had already been exported, `meta.credits.cost` is the exact export-fix amount charged — `0` when nothing qualified for a charge. For a video that has not been exported, the charge runs asynchronously inside the render pipeline, so `cost` is the preflight estimate that will be charged.

**Preconditions & common errors:**
- **Video not in a failed state** → `403` `Only failed videos can be re-tried. Please create a new video instead.` — only a `failed` render can be retried.
- **Video busy** (rendering or syncing) → `403` `The video is still being processed. Please wait until it is finished.`
- **Insufficient credits** → `402` with code `INSUFFICIENT-CREDITS` and `required` / `available` in the error body.
- **Not your video** → `403` `You are not allowed to re-generate this video`.

→ **Next:** keep polling `GET /faceless/{id}` until `data.video.status` is `completed`.

### 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": "faceless",
    "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 /faceless/{faceless}/export

**Export a faceless video** (operationId: `exportFacelessVideo`)

**Step 7 of 7 · Export a restyled cut** *(optional)*.

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

**Before you can export:** the video must already be rendered, and must not currently be busy (rendering or syncing).

Rebuilds the rendered faceless video source with the supplied styling (captions, watermark, music, transitions) and queues an export. Returns HTTP 202 with the video state embedded under `data.video` while the export runs asynchronously.

**Credits:** `meta.credits.cost` is the exact amount charged — `0` when nothing changed enough to charge (unchanged options, music, and watermark).

**Preconditions & common errors:**
- **Video busy** (rendering or syncing) → `403` `The video is still being processed`.
- **Not your video** → `403` `You are not allowed to export this video`.
- **Insufficient credits** (only when the restyle qualifies for a charge) → `402` with code `INSUFFICIENT-CREDITS`.

→ This is the final step. Track the export the same way — poll `GET /faceless/{id}`.

### Parameters

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

### Request body

Optional. Restyling options for the export. All fields are optional; omit a field to keep the rendered video's current styling. Note: unlike render, the watermark accepts only an existing asset `id` (no upload), and music is selected by `music_id` only.

- `transition` (string) — Transition slug applied between scenes. Allowed values are listed under faceless options (transitions).
- `overlay` (string) — Overlay style slug applied over the video. Allowed values are listed under faceless options (overlays).
- `sfx` (string) — Sound-effect slug applied to the video. Allowed values are listed under faceless options (sfx).
- `music_id` (integer) — Identifier of a background music track (media id).
- `volume` (string) — Background-music volume level. Required when `music_id` is set. Allowed values: `low`, `medium`, `high`.
- `captions` (object) — Caption styling. Only `font_family`, `font_color`, `font_url`, `position`, and `effect` are honored; 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) — URL of a custom font file to use for captions.
  - `position` (string) — On-screen caption position slug. Allowed values are listed under faceless options (caption_positions).
  - `effect` (string) — Caption effect slug applied to on-screen text. Allowed values are listed under faceless options (caption_effects).
- `watermark` (object) — Watermark asset and placement. References an existing asset you own.
  - `id` (integer) — Identifier of a watermark asset you own.
  - `position` (string) — Watermark placement slug. Allowed values are listed under faceless options (watermark_positions).
  - `opacity` (integer) — 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": "faceless",
    "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.
- `500` — Server error.

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

## GET /faceless/{faceless}/assets

**List faceless assets** (operationId: `listFacelessAssets`)

**Reference** — inspect rendered assets (populated after Step 6).

Lists the media assets (images, clips) that make up the faceless video, ordered by scene. Pass `index` to fetch the asset at a single scene position.

**Note:** 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).

## GET /faceless/{faceless}/assets/{asset}

**Get a faceless asset** (operationId: `getFacelessAsset`)

**Reference** — inspect a single rendered asset (populated after Step 6).

Returns a single media asset belonging to the faceless video.

### Parameters

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

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

## GET /presets/faceless

**List faceless presets** (operationId: `listFacelessPresets`)

**Reusable presets** — saved defaults you can apply at Step 1 (Create).

Lists the authenticated user's saved faceless presets, newest first. A preset bundles reusable defaults (voice, genre, captions, watermark, etc.) you can apply when creating videos.

Use the `include` query parameter to embed related resources; values outside the documented allowlist are rejected with `400 Bad Request`.

### Parameters

- `include` (query, string) — Comma-separated list of related resources to embed. Allowed values: `music`, `voice`, `background`, `watermark`, `genre`. Any other value is rejected with `400 Bad Request`. Example: `music,voice`

### Response `200`

```json
{
  "message": "Success.",
  "status": 200,
  "data": [
    {
      "id": 1,
      "user_id": 1,
      "name": "My default preset",
      "voice_id": 12,
      "genre_id": 3,
      "orientation": "portrait",
      "font_family": "inter",
      "font_color": "#FFFFFF",
      "volume": "medium",
      "created_at": "2026-01-01T12:00:00.000000Z",
      "updated_at": "2026-01-01T12:00:00.000000Z"
    }
  ]
}
```

### Errors

- `400` — Bad request — e.g. an `include` value outside the endpoint's allowlist.
- `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.
- `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 /presets/faceless

**Create a faceless preset** (operationId: `createFacelessPreset`)

**Reusable presets** — saved defaults you can apply at Step 1 (Create).

Creates a reusable faceless preset for the authenticated user from the supplied defaults.

**`name` is required on create** — omitting it returns `422`. (On update, `PATCH /presets/faceless/{preset}`, `name` is optional.) All other fields are optional; `duration` must be a positive integer (≥ 1) and `font_color` must be either `default` or a hex value (e.g. `#ffffff`).

### Request body

Optional.

- `name` (string, required) — Display name of the preset. **Required when creating** (`POST /presets/faceless`); **optional when updating** (`PATCH /presets/faceless/{preset}`) — OpenAPI cannot make `required` depend on the HTTP verb, so it is listed in `required` here and the create-only rule is documented in this note and the create operation's description.
- `volume` (string|null) — Background-music volume level: "low", "medium", or "high".
- `language` (string|null) — Language of the content (e.g. "english").
- `font_color` (string|null) — Caption font color — either the literal `default` or a hex value (3- or 6-digit, e.g. `#ffffff` or `#fff`).
- `font_family` (string|null) — Caption font family slug (see faceless options).
- `duration` (integer|null) — Target video length in seconds — a positive number of seconds.
- `orientation` (string|null) — Video orientation. One of "landscape", "portrait", or "square". Allowed values: `landscape`, `portrait`, `square`.
- `position` (string|null) — On-screen position slug (e.g. "bottom", "center", "top").
- `caption_animation` (string|null) — Caption animation/effect slug applied to on-screen captions.
- `sfx` (string|null) — Sound-effect slug applied to the video (see faceless options). Allowed values: `none`, `whoosh`.
- `music_id` (integer|null) — Identifier of the background music track (media id).
- `music_category_id` (integer|null) — Identifier of the music category tag.
- `background_id` (integer|null) — Identifier of the background asset (see faceless options).
- `genre_id` (integer|null) — Identifier of the genre/style (see faceless options).
- `voice_id` (integer|null) — Identifier of the narration voice (see faceless options).
- `transition` (string|null) — 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) — Per-image motion effect slug (e.g. "zoom-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`.
- `overlay` (string|null) — Overlay style slug applied over the video (see faceless options). Allowed values: `none`, `vhs`, `rain`, `glitch`, `dust`, `sparkling-gold`, `spark-effect`, `abstract-particles`.
- `watermark_id` (integer|null) — Identifier of the watermark asset (must be owned by the caller).
- `watermark_position` (string|null) — Watermark placement slug (e.g. "bottom-right"). Allowed values: `top-left`, `top-center`, `top-right`, `middle-left`, `middle-center`, `middle-right`, `bottom-left`, `bottom-center`, `bottom-right`, `none`.
- `watermark_opacity` (integer|null) — Watermark opacity as a percentage (0–100).
- `resource_id` (integer|null) — Identifier of the destination resource/folder.
- `image_engine_id` (integer|null) — Identifier of the text-to-image engine (see faceless options).
- `clip_engine_id` (integer|null) — Identifier of the text-to-video (clip) engine (see faceless options).

Example request:

```json
{
  "name": "My default preset",
  "voice_id": 12,
  "genre_id": 3,
  "orientation": "portrait",
  "font_family": "inter",
  "font_color": "#FFFFFF",
  "volume": "medium"
}
```

### Response `201`

```json
{
  "message": "Success.",
  "status": 201,
  "data": {
    "id": 1,
    "user_id": 1,
    "name": "My default preset",
    "voice_id": 12,
    "genre_id": 3,
    "orientation": "portrait",
    "font_family": "inter",
    "font_color": "#FFFFFF",
    "volume": "medium",
    "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).

## PATCH /presets/faceless/{preset}

**Update a faceless preset** (operationId: `updateFacelessPreset`)

**Reusable presets.**

Updates a faceless preset you own. Only the fields you send are changed.

### Parameters

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

### Request body

Optional.

- `name` (string, required) — Display name of the preset. **Required when creating** (`POST /presets/faceless`); **optional when updating** (`PATCH /presets/faceless/{preset}`) — OpenAPI cannot make `required` depend on the HTTP verb, so it is listed in `required` here and the create-only rule is documented in this note and the create operation's description.
- `volume` (string|null) — Background-music volume level: "low", "medium", or "high".
- `language` (string|null) — Language of the content (e.g. "english").
- `font_color` (string|null) — Caption font color — either the literal `default` or a hex value (3- or 6-digit, e.g. `#ffffff` or `#fff`).
- `font_family` (string|null) — Caption font family slug (see faceless options).
- `duration` (integer|null) — Target video length in seconds — a positive number of seconds.
- `orientation` (string|null) — Video orientation. One of "landscape", "portrait", or "square". Allowed values: `landscape`, `portrait`, `square`.
- `position` (string|null) — On-screen position slug (e.g. "bottom", "center", "top").
- `caption_animation` (string|null) — Caption animation/effect slug applied to on-screen captions.
- `sfx` (string|null) — Sound-effect slug applied to the video (see faceless options). Allowed values: `none`, `whoosh`.
- `music_id` (integer|null) — Identifier of the background music track (media id).
- `music_category_id` (integer|null) — Identifier of the music category tag.
- `background_id` (integer|null) — Identifier of the background asset (see faceless options).
- `genre_id` (integer|null) — Identifier of the genre/style (see faceless options).
- `voice_id` (integer|null) — Identifier of the narration voice (see faceless options).
- `transition` (string|null) — 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) — Per-image motion effect slug (e.g. "zoom-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`.
- `overlay` (string|null) — Overlay style slug applied over the video (see faceless options). Allowed values: `none`, `vhs`, `rain`, `glitch`, `dust`, `sparkling-gold`, `spark-effect`, `abstract-particles`.
- `watermark_id` (integer|null) — Identifier of the watermark asset (must be owned by the caller).
- `watermark_position` (string|null) — Watermark placement slug (e.g. "bottom-right"). Allowed values: `top-left`, `top-center`, `top-right`, `middle-left`, `middle-center`, `middle-right`, `bottom-left`, `bottom-center`, `bottom-right`, `none`.
- `watermark_opacity` (integer|null) — Watermark opacity as a percentage (0–100).
- `resource_id` (integer|null) — Identifier of the destination resource/folder.
- `image_engine_id` (integer|null) — Identifier of the text-to-image engine (see faceless options).
- `clip_engine_id` (integer|null) — Identifier of the text-to-video (clip) engine (see faceless options).

Example request:

```json
{
  "name": "Updated preset",
  "voice_id": 14,
  "volume": "high"
}
```

### Response `200`

```json
{
  "message": "Success.",
  "status": 200,
  "data": {
    "id": 1,
    "user_id": 1,
    "name": "My default preset",
    "voice_id": 12,
    "genre_id": 3,
    "orientation": "portrait",
    "font_family": "inter",
    "font_color": "#FFFFFF",
    "volume": "medium",
    "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).

## DELETE /presets/faceless/{preset}

**Delete a faceless preset** (operationId: `deleteFacelessPreset`)

**Reusable presets.**

Permanently deletes a faceless preset you own. Responds with HTTP 204 on success.

### Parameters

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

### Response `204`

Empty body (`204 No Content`).

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