<!-- Generated from openapi/v2.json (md/url-to-video.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)

# URL-to-Video

Turn a web page into a video: scrape images and script, then render.

## POST /url-to-video

**Create a URL-to-Video video** (operationId: `createUrlToVideo`)

Creates a new URL-to-Video video in draft state (its `type` is fixed at `url-based` 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 /url-to-video/{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).
- `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 URL-to-Video video"
}
```

### 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": "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",
    "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 **paid** subscription. The v2 public API is paid-only — every faceless and preset endpoint (reads included) requires an active paid subscription, and trials are not eligible. Two codes distinguish the cause on this `403`: `PAID-SUBSCRIPTION-REQUIRED` (subscribed but on a trial — access unlocks automatically once the subscription becomes paid) and `SUBSCRIPTION-REQUIRED` (never subscribed, expired, or canceled). Rejected before any ownership, credit, or validation check. Only `GET /me`, `GET /credits/costs`, and `GET /credits/history` are reachable without a paid subscription.
- `405` — The URL exists but not for this HTTP method — e.g. `GET /presets/faceless/{preset}`, which is only defined for `PATCH` and `DELETE`. Check the verb against the endpoint reference; the `Allow` header lists the methods this path accepts.
- `409` — Email address not verified. The token is valid and the account exists, but the user has not confirmed their email address. This gate fronts every v2 endpoint. Have the user complete verification, then retry.
- `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 /url-to-video/{faceless}/scrape-images

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

**Free — no credits charged.** Extracts images from the supplied web page URL and attaches them to the URL-to-Video as ordered assets. You must call this at least once before rendering a URL-to-Video. Social-media URLs are rejected with `422`.

### Parameters

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

### Request body

Required.

- `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
- `405` — The URL exists but not for this HTTP method — e.g. `GET /presets/faceless/{preset}`, which is only defined for `PATCH` and `DELETE`. Check the verb against the endpoint reference; the `Allow` header lists the methods this path accepts.
- `409` — Email address not verified. The token is valid and the account exists, but the user has not confirmed their email address. This gate fronts every v2 endpoint. Have the user complete verification, then retry.
- `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 /url-to-video/{faceless}/scrape-script

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

💳 Charges credits. 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 video. Social-media URLs are rejected with `422`.

### Parameters

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

### Request body

Required.

- `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, 900, or 1200. Allowed values: `30`, `60`, `180`, `300`, `600`, `900`, `1200`.
- `style` (string, required) — Narrative style for the generated script (free-form text, max 255 chars). Examples: educational, storytelling, listicle, conversational, motivational.
- `language` (string, required) — Language for the generated script (e.g. "english").
- `tone` (string, required) — Tone of voice for the generated script. Examples: professional, friendly, casual, authoritative.

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
- `405` — The URL exists but not for this HTTP method — e.g. `GET /presets/faceless/{preset}`, which is only defined for `PATCH` and `DELETE`. Check the verb against the endpoint reference; the `Allow` header lists the methods this path accepts.
- `409` — Email address not verified. The token is valid and the account exists, but the user has not confirmed their email address. This gate fronts every v2 endpoint. Have the user complete verification, then retry.
- `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 /url-to-video/{faceless}/scripts

**Generate a URL-to-Video script** (operationId: `generateUrlToVideoScript`)

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

Generates a narration script for the URL-to-Video 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, 900, or 1200. Allowed values: `30`, `60`, `180`, `300`, `600`, `900`, `1200`.

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": "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",
    "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
- `405` — The URL exists but not for this HTTP method — e.g. `GET /presets/faceless/{preset}`, which is only defined for `PATCH` and `DELETE`. Check the verb against the endpoint reference; the `Allow` header lists the methods this path accepts.
- `409` — Email address not verified. The token is valid and the account exists, but the user has not confirmed their email address. This gate fronts every v2 endpoint. Have the user complete verification, then retry.
- `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 /url-to-video/{id}

**Get a URL-to-Video video** (operationId: `getUrlToVideoVideo`)

Returns a single URL-to-Video 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": "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",
    "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 **paid** subscription. The v2 public API is paid-only — every faceless and preset endpoint (reads included) requires an active paid subscription, and trials are not eligible. Two codes distinguish the cause on this `403`: `PAID-SUBSCRIPTION-REQUIRED` (subscribed but on a trial — access unlocks automatically once the subscription becomes paid) and `SUBSCRIPTION-REQUIRED` (never subscribed, expired, or canceled). Rejected before any ownership, credit, or validation check. Only `GET /me`, `GET /credits/costs`, and `GET /credits/history` are reachable without a paid subscription.
- `404` — Not found
- `405` — The URL exists but not for this HTTP method — e.g. `GET /presets/faceless/{preset}`, which is only defined for `PATCH` and `DELETE`. Check the verb against the endpoint reference; the `Allow` header lists the methods this path accepts.
- `409` — Email address not verified. The token is valid and the account exists, but the user has not confirmed their email address. This gate fronts every v2 endpoint. Have the user complete verification, then retry.
- `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 /url-to-video/{id}

**Update a URL-to-Video video** (operationId: `updateUrlToVideoVideo`)

Updates the URL-to-Video 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": "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"
  }
}
```

### 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
- `405` — The URL exists but not for this HTTP method — e.g. `GET /presets/faceless/{preset}`, which is only defined for `PATCH` and `DELETE`. Check the verb against the endpoint reference; the `Allow` header lists the methods this path accepts.
- `409` — Email address not verified. The token is valid and the account exists, but the user has not confirmed their email address. This gate fronts every v2 endpoint. Have the user complete verification, then retry.
- `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 /url-to-video/{faceless}/estimate

**Estimate URL-to-Video render credits** (operationId: `estimateUrlToVideoVideo`)

Estimates the credits required to render the URL-to-Video 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
- `405` — The URL exists but not for this HTTP method — e.g. `GET /presets/faceless/{preset}`, which is only defined for `PATCH` and `DELETE`. Check the verb against the endpoint reference; the `Allow` header lists the methods this path accepts.
- `409` — Email address not verified. The token is valid and the account exists, but the user has not confirmed their email address. This gate fronts every v2 endpoint. Have the user complete verification, then retry.
- `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 /url-to-video/{faceless}/render

**Render a URL-to-Video video** (operationId: `renderUrlToVideoVideo`)

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

Starts rendering the URL-to-Video video asynchronously and returns HTTP 202. Track progress by polling `GET /url-to-video/{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.

**Fields omitted from the render body are filled from the video's saved configuration** — a value set at creation, via `PATCH /url-to-video/{id}`, or on a previous render is reused automatically, and an explicit body value always wins.

### Parameters

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

### Request body

Optional. Render configuration for the faceless video. Every field is optional: `script`, `duration`, and `aspect_ratio` are auto-filled from the saved draft when omitted (configure it first via `PUT /faceless/{faceless}/scripts` and `PATCH /faceless/{faceless}`), so a fully-configured video can be rendered with an empty body. A `422` is only returned when a required value is missing from both the body and the draft.

- `script` (string) — Narration script text for the video. Optional — falls back to the draft's stored script when omitted.
- `duration` (integer) — Target video length in seconds (a positive integer). Optional — falls back to the draft's estimated duration when omitted. Not restricted to the script-generation presets (those fixed values apply only to PUT /faceless/{faceless}/scripts).
- `aspect_ratio` (string) — Output aspect ratio. Optional — falls back to the draft's stored aspect ratio when omitted. 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": "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"
  },
  "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
- `405` — The URL exists but not for this HTTP method — e.g. `GET /presets/faceless/{preset}`, which is only defined for `PATCH` and `DELETE`. Check the verb against the endpoint reference; the `Allow` header lists the methods this path accepts.
- `409` — Email address not verified. The token is valid and the account exists, but the user has not confirmed their email address. This gate fronts every v2 endpoint. Have the user complete verification, then retry.
- `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 /url-to-video/{faceless}/retry

**Retry a URL-to-Video render** (operationId: `retryUrlToVideoVideo`)

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

Re-attempts rendering for a URL-to-Video 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": "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",
    "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
- `405` — The URL exists but not for this HTTP method — e.g. `GET /presets/faceless/{preset}`, which is only defined for `PATCH` and `DELETE`. Check the verb against the endpoint reference; the `Allow` header lists the methods this path accepts.
- `409` — Email address not verified. The token is valid and the account exists, but the user has not confirmed their email address. This gate fronts every v2 endpoint. Have the user complete verification, then retry.
- `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 /url-to-video/{faceless}/export

**Export a URL-to-Video video** (operationId: `exportUrlToVideoVideo`)

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

Rebuilds the rendered URL-to-Video 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": "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",
    "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
- `405` — The URL exists but not for this HTTP method — e.g. `GET /presets/faceless/{preset}`, which is only defined for `PATCH` and `DELETE`. Check the verb against the endpoint reference; the `Allow` header lists the methods this path accepts.
- `409` — Email address not verified. The token is valid and the account exists, but the user has not confirmed their email address. This gate fronts every v2 endpoint. Have the user complete verification, then retry.
- `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 /url-to-video/{faceless}/assets

**List URL-to-Video assets** (operationId: `listUrlToVideoAssets`)

Lists the media assets (images, clips) that make up the URL-to-Video 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
- `405` — The URL exists but not for this HTTP method — e.g. `GET /presets/faceless/{preset}`, which is only defined for `PATCH` and `DELETE`. Check the verb against the endpoint reference; the `Allow` header lists the methods this path accepts.
- `409` — Email address not verified. The token is valid and the account exists, but the user has not confirmed their email address. This gate fronts every v2 endpoint. Have the user complete verification, then retry.
- `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 /url-to-video/{faceless}/assets/upload

**Upload a URL-to-Video asset** (operationId: `uploadUrlToVideoAsset`)

Attaches a media asset to the URL-to-Video. Provide EITHER a multipart `file` upload OR a remote `url` to transload — exactly one. Accepts image and video files, subject to your storage quota.

### Parameters

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

### Request body

Optional.

- `file` (string) — The media file to upload (multipart/form-data). Provide this OR `url`, not both.
- `url` (string) — Publicly reachable URL of a media file to transload. Provide this OR `file`, not both.

Example request:

```json
{
  "url": "https://cdn.example.com/media/clip.mp4"
}
```

### Response `201`

```json
{
  "message": "Success.",
  "status": 201,
  "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
- `405` — The URL exists but not for this HTTP method — e.g. `GET /presets/faceless/{preset}`, which is only defined for `PATCH` and `DELETE`. Check the verb against the endpoint reference; the `Allow` header lists the methods this path accepts.
- `409` — Email address not verified. The token is valid and the account exists, but the user has not confirmed their email address. This gate fronts every v2 endpoint. Have the user complete verification, then retry.
- `413` — The request body exceeded the server upload limit. This is checked before validation, so the response names no specific field. Send fewer or smaller files — a single file over the documented per-field limit returns `422` on that key instead.
- `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).
