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

# Syllaby API — Getting started

The Syllaby API lets you build Syllaby’s content-generation capabilities
into your own apps and workflows. This **v2** release covers **faceless
videos** and the resources around them — scripts, presets, assets, and
account credits. More capabilities will be added over time.

## Typical flow (faceless video)
**Standard flow (AI-generated or stock visuals):**
1. **Create** a draft video — `POST /faceless`.
2. **Add a script** — generate one with `PUT /faceless/{faceless}/scripts`, or supply your own.
3. **Configure** voice, genre, captions, and visuals — `PATCH /faceless/{faceless}`. Browse the available options at `GET /faceless/options`.
4. *(Optional)* **Estimate** the credit cost — `GET /faceless/{faceless}/estimate`.
5. **Render** — `POST /faceless/{faceless}/render` (returns `202`).
6. **Track progress** — poll `GET /faceless/{id}` and read `data.video.status` (`rendering` → `completed`/`failed`); the `video` object is always embedded. On failure, retry with `POST /faceless/{faceless}/retry`. See **Get your rendered video** below.
7. *(Optional)* **Export** a restyled cut — `POST /faceless/{faceless}/export`.

**URL-based flow (build a video from a product/web page):**
`create (type=url-based)` → `scrape-images` *(free)* → `scrape-script` *(charges `CONTENT_PROMPT_REQUESTED`, same as `PUT /scripts`)* → configure → render *(charges)* → poll.

## Build from a URL
The `url-based` type lets you build a faceless video from the content of any publicly reachable web page (product listing, blog post, landing page) — Syllaby extracts images and can generate a matching script directly from the page.

**Steps:**
1. **Create** — `POST /faceless` with `type: "url-based"`. The `type` is fixed at creation and cannot be changed later.
2. **Scrape images** — `POST /faceless/{faceless}/scrape-images` with `{ "url": "https://…" }`. **Free** — no credits charged. Extracts product/page images and attaches them to the video as ordered assets.
3. **Scrape script** — `POST /faceless/{faceless}/scrape-script` with the page URL plus generation options (`duration`, `style`, `tone`, `language`). **💳 Charges `CONTENT_PROMPT_REQUESTED` credits** (the same amount as `PUT /faceless/{faceless}/scripts`). Generates and stores a narration script written from the page content. You may skip this step and supply your own script via `PATCH /faceless/{faceless}` instead.
4. **Configure** — `PATCH /faceless/{faceless}` (voice, captions, transitions, etc.).
5. **Render** — `POST /faceless/{faceless}/render`. **💳 Charges render credits.** Requires that scrape-images has run at least once (otherwise returns `422 assets`). The scraped images become the visual slides.
6. **Poll** — `GET /faceless/{id}` until `data.video.status` is `completed`.

> **Social and unsupported URLs are rejected.** URLs pointing to social-media platforms (YouTube, TikTok, Twitter/X, Instagram, Facebook, etc.) and any un-parseable or unreachable page return `422` on both scrape endpoints.

## Get your rendered video
Rendering is asynchronous — the render call returns before the file exists. From request to playable file:

1. **Create** the faceless video — `POST /faceless`. Note the `id` field in the response.
2. *(Optional)* **Generate a script** — `PUT /faceless/{faceless}/scripts`.
3. **Render** — `POST /faceless/{faceless}/render`. Returns `202` immediately; `meta.credits` reports the charge.
4. **Poll** — `GET /faceless/{id}` on a sensible cadence (every 5–10 seconds is plenty). Every response embeds the render state under `data.video` — no `include` parameter needed.
5. **Done** — when `data.video.status` is `completed`, the playable file is at `data.video.url`. If it is `failed`, the reason is in `data.video.failure` (`code` and `message`); fix what you can and retry with `POST /faceless/{faceless}/retry`.

> **Important — poll with the right identifier.** Use the faceless **`id`** from the create/render response, **not** `video_id`. They are different identifiers: `id` is the faceless video, `video_id` is its underlying video record. Calling `GET /faceless/{video_id}` returns `404`.

## Before you render — checklist
Rendering charges credits and has a few preconditions. The render call validates them **before** anything is charged, so a missed step fails fast with a `402`/`403` (or `422`) and costs nothing. Before calling `POST /faceless/{faceless}/render`:

1. **Script set** — generate one with `PUT /faceless/{faceless}/scripts`, or supply your own via `PATCH /faceless/{faceless}`. Missing → **`403` `A script is required before rendering.`**
2. **Voice chosen** — set `voice_id` via `PATCH /faceless/{faceless}`. Missing → **`403` `Voice was not provided.`** (or `Voice not found.` for an unknown id).
3. **Options configured** — genre, captions, and visuals. Genre is **required** for `ai-visuals` / `ai-clips`; missing it → **`422` `A genre is required for AI visuals and AI clips.`** Browse valid ids/slugs at `GET /faceless/options`.
4. **Credits sufficient** — check the cost against your balance with `GET /faceless/{faceless}/estimate`. Too few → **`402` `INSUFFICIENT-CREDITS`** with `required` and `available` in the error body.
5. **For `url-based` videos** — run `POST /faceless/{faceless}/scrape-images` at least once before rendering. A url-based render with no scraped assets → **`422` `assets`** (the images become the visual slides; without them the render would produce an empty video).

The video must also not be **busy** (already rendering or syncing) → **`403` `The video is processing currently. Please try again once finished`**. See each endpoint's *Preconditions & common errors* for the full list.

## Generate a clip from text (text-to-video)
Short AI video clips from a text prompt, with optional reference frames. From prompt to playable file:

1. **Pick a model** — `GET /text-to-video/models`. Models are gated by your plan; each entry lists its modifier menus (duration, resolution, aspect ratio, audio, reference media) and per-second pricing.
2. *(Optional)* **Estimate** — `GET /text-to-video/estimate?model=<slug>` (plus any modifiers you will send). The figure is **exactly** what the create call charges for the same parameters — same defaults, same formula. Unlike the faceless render estimate, this is not an approximation.
3. **Create** — `POST /text-to-video` with the model, a prompt (5–1500 words), and any modifiers. Returns `202` immediately; `meta.credits.cost` is the exact amount charged.
4. **Poll** — `GET /text-to-video/{id}` with the **`id` from the create response**, every 5–10 seconds.
5. **Done** — when `data.asset.status` is `success`, the clip is at `data.asset.url`. If it is `failed`, the reason is in `data.asset.failure` and the charge is refunded automatically.

`asset.status` is the field to branch on, and these are its **exact strings**: `draft` (just created — the create call's `202` response carries this) → `processing` (the provider is generating) → terminal `success` (then, and only then, `asset.url` is non-null) or `failed` (`asset.failure` carries the reason).
## Transcribe audio & video (speech-to-text)
Accurate transcripts with word-level timestamps from any audio/video source — and transcript editing to correct captions before a render. Two flows:

**Standalone transcription:**
1. **Create** — `POST /transcriptions` with a public https `url` (or an uploaded `file` via `multipart/form-data`). Returns `201` with a `pending` record; **charges 1 credit per started minute** once the source is probed (an unreadable or silent source fails before anything is charged).
2. **Poll** — `GET /transcriptions/{id}` every 5–10 seconds. `pending` → `processing` → terminal `completed` or `failed` (failures after the charge are refunded automatically).
3. **Done** — `text` carries the transcript and `words` the word-level timestamps.

**Correct captions before render (faceless videos):**
1. **Read** — `GET /faceless/{faceless}/captions` once the video is transcribed. Segments carry their `index`, timings, and words; silence gaps are flagged (`gap: true`) and not editable.
2. **Edit** — `PUT /faceless/{faceless}/captions` with per-segment edits: `words` (one-for-one replacement, timestamps preserved) or `text` (full rewrite, words retimed within the segment window).
3. **Render/export** — the corrected captions are used by `POST /faceless/{faceless}/render` and `/export`.
## Generate speech from text (text-to-speech)
Standalone narration: turn any script into a studio-quality mp3 with word-level timestamps. From script to audio:

1. **Pick a voice** — `GET /voices`. System voices plus your own voice clones (see the Voice Clones module), each with a free `preview` sample url.
2. **Create** — `POST /speech` with `voice_id` and `text` (up to 10,000 characters). **Charges 7 credits per started 1,000 characters.** Returns `201` immediately; `meta.credits.cost` is the exact amount charged.
3. **Poll** — `GET /speech/{id}` with the **`id` from the create response**, every 3–5 seconds. Short scripts resolve in seconds; long ones can take a few minutes.
4. **Done** — when `data.status` is `success`, the mp3 is at `data.url` and `data.words` carries word-level timestamps (ready-made caption input). On `failed`, `data.error` has the reason and the charge is refunded automatically.
## Create a launch clip from an idea
A 20-second, 1080×1920 launch video from three inputs: you supply the product name, domain, and idea — the API writes the creative brief and renders the MP4. From idea to clip:

1. *(Optional)* **Estimate** — `GET /launch-clips/estimate`. The cost is a **flat 5 credits per clip**, independent of the idea.
2. **Create** — `POST /launch-clips` with `product_name`, `domain`, and `idea` (plus an optional `accent_color`). Returns `201` immediately; `meta.credits.cost` is the exact amount charged.
3. **Poll** — `GET /launch-clips/{id}` with the **`id` from the create response**, every 5–10 seconds. `planning` → `briefed` → `rendering` → terminal `completed` or `failed`.
4. **Done** — when `data.status` is `completed`, the MP4 is at `data.video.url`. On `failed`, `data.error` has the reason and the 5-credit charge is refunded automatically.
## Generate a stock-footage video (B-roll)
B-roll videos assemble stock footage against a narration script. The flow mirrors the generic faceless flow with the `type` fixed to `b-roll`:

1. **Create** — `POST /b-roll` with a `title`. Keep the returned `id`.
2. **Add a script** — `PUT /b-roll/{faceless}/scripts` (or supply your own via `PATCH /b-roll/{id}`).
3. **Configure** — `PATCH /b-roll/{id}` to set voice, captions, transitions, and more.
4. *(Optional)* **Estimate** — `GET /b-roll/{faceless}/estimate`.
5. **Render** — `POST /b-roll/{faceless}/render`, then poll `GET /b-roll/{id}` until `data.video.status` is `completed`.
## Generate an AI-Visuals video
AI-Visuals videos use AI-generated imagery for each scene. Same lifecycle as B-roll (`type` fixed to `ai-visuals`), plus per-scene asset control:

1. **Create → script → configure → render** — as for B-roll, under `/ai-visuals`. A `genre` is required at render time.
2. **Swap imagery** — `POST /ai-visuals/{faceless}/assets/upload` to add your own media (file or url).
3. **Regenerate a scene** — `POST /ai-visuals/{faceless}/assets/{asset}/regenerate` to re-roll a single scene image.
## Generate a video from a URL (URL-to-Video)
URL-to-Video builds a video from a web page (`type` fixed to `url-based`). Scrape the page first, then render:

1. **Create** — `POST /url-to-video` with a `title`.
2. **Scrape images** — `POST /url-to-video/{faceless}/scrape-images` (free; required before rendering).
3. **Add a script** — `POST /url-to-video/{faceless}/scrape-script` (from the page) or `PUT /url-to-video/{faceless}/scripts` (from a topic).
4. **Configure → render** — `PATCH /url-to-video/{id}`, then `POST /url-to-video/{faceless}/render` and poll `GET /url-to-video/{id}`.
## Build a storyboard video (Text-to-Scene)
Text-to-Scene is a storyboard editor: a video is a sequence of scenes you can generate, reorder, and chain for continuous motion.

1. **Create** — `POST /text-to-scene` (`mode` `wizard` or `manual`). Keep the returned `id`.
2. **Populate** — in `wizard` mode, `POST /text-to-scene/{storyboard}/prompt` generates scenes; in `manual` mode, add them with `POST /text-to-scene/{storyboard}/scenes`.
3. **Refine** — reorder (`scenes/sort`), assign frames (`scenes/{scene}/frames`), chain scenes (`scenes/{scene}/chain`), and steer style with the moodboard endpoints.
4. **Generate clips** — `POST /text-to-scene/{storyboard}/clips`, polling each scene's status.
5. **Render** — `POST /text-to-scene/{storyboard}/render`, then poll `GET /text-to-scene/{id}` until `data.video.status` is `completed`.
## Clone a voice
Turn 1–3 audio samples into a reusable narration voice.

1. **Check slots** *(optional)* — `GET /voice-clones/slots`. If `available` is 0, you can buy another when `can_buy` is true (costs `cost` credits).
2. **Create** — `POST /voice-clones` (`multipart/form-data`) with `terms`, `name`, `gender`, and up to 3 `samples[]`. Add `purchase_addon: true` to buy a slot when yours are full.
3. **Poll** — `GET /voice-clones/{id}` until `data.status` is `completed`; the sample preview is at `data.preview_url`.
4. **Use it** — the clone appears among your narration voices for faceless renders.
## Discover content ideas
Find and rank content ideas for a keyword.

1. **Discover** — `POST /ideas/discover` with a `keyword` and `network`. **Charges 15 credits** on any fruitful search (including cached re-searches); synchronous and can take up to ~2 minutes.
2. **List** — `GET /ideas?keyword=…&network=…` returns the ranked ideas. **Free**, paginated, and sortable (`sort=-volume` by default).

> If a discover call times out, do **not** blindly retry — there is no idempotency key and a retry can charge another 15 credits. Reconcile with the free `GET /ideas` first.
## Create a consistent character
Build a reusable AI character from a reference photo.

1. **Pick a genre** *(optional)* — `GET /characters/genres`.
2. **Create** — `POST /characters` (`multipart/form-data`) with an `image`. Keep the returned `id`.
3. **Preview** — `POST /characters/{id}/preview` with a `genre_id` (synchronous, up to ~90s, 5/min). Pick a candidate from `data.previews[]`.
4. **Train** — `POST /characters/{id}/train` with that `preview_id` plus `name`, `age`, `gender`. **Charges 10 credits.** Poll `GET /characters/{id}` until `data.status` is `ready`.

## Authentication
All endpoints require a **Bearer token**. Send it on every request:

```http
Authorization: Bearer <your-api-token>
```

Generate a token from your Syllaby account settings. Treat it like a
password — never expose it in client-side code.

The v2 API is token-only and stateless: authenticate with `Authorization: Bearer <token>` on every request. Do **not** send cookies or a CSRF token — they are ignored.

## Base URL
Production requests go to `https://api.syllaby.io/v2`. Every path in this
reference is relative to that base.

## Subscription required
Every generative endpoint requires an **active paid subscription** — reads
included. Two distinct `403` codes tell you exactly why a caller was
rejected, before any ownership, credit, or validation check runs:

| Code | Who gets it | Message |
|------|-------------|---------|
| `PAID-SUBSCRIPTION-REQUIRED` | Subscribed but **on a trial** | `API access requires an active paid subscription. Trials are not eligible — start a paid subscription to unlock the API.` |
| `SUBSCRIPTION-REQUIRED` | No subscription (never subscribed, expired, or canceled) | `An active subscription is required.` |

The public API has no free tier and trials are not eligible; access is
restored automatically the moment the subscription becomes paid. Only
`GET /me`, `GET /credits/costs`, and `GET /credits/history` are reachable
without a paid subscription.

## Credits
Generative actions (rendering, script generation) consume account credits.
Use the credit-cost and estimate endpoints to check the price before you
spend, and the credit history endpoint to audit usage.
Credits consumed through the public API are tagged at the ledger level, so
API-originated spend is distinguishable from in-app usage. Audit it with
`GET /credits/history` — each entry records the action and the credits charged —
and look up the per-action price list with `GET /credits/costs`.

## Responses & errors
Responses are JSON wrapped in a `{ message, status, data }` envelope.
Errors use standard HTTP status codes — `400` (bad request, e.g. an
`include` value outside the endpoint's allowlist), `401` (unauthenticated),
`402` (payment required — insufficient credits), `404` (not found),
`422` (validation), and `429` (rate limited).

Error payloads include a stable machine-readable `error.code`:

| Code | Meaning |
|------|---------|
| `GEN-UNAUTHORIZED` | Missing, invalid, or revoked API key (`401`). |
| `GEN-FORBIDDEN` | Authenticated but not allowed to act on this resource (`403`). |
| `GEN-NOT-FOUND` | Resource does not exist or is not yours (`404`). |
| `GEN-WRONG-ARGS` | Malformed request arguments (`400`). |
| `GEN-API-ERROR` | Unexpected server-side error (`5xx`). |
| `SUBSCRIPTION-REQUIRED` | No active subscription (`403`). |
| `PAID-SUBSCRIPTION-REQUIRED` | Trial subscription — the API is paid-only (`403`). |
| `INSUFFICIENT-CREDITS` | Not enough credits for the requested action (`402`). |
| `EMAIL-NOT-VERIFIED` | Account email address not verified yet (`403`). |
| `FEATURE-NOT-ALLOWED` | Feature not included in the current plan (`403`). |
| `REACH-PLAN-LIMIT` / `REACH-PLAN-STORAGE-LIMIT` / `REACH-PLAN-PUBLISH-LIMIT` | A plan quota was exhausted (`403`). |

## Rate limiting
Requests are rate limited **per API token** — 30 requests per minute by
default. Every response carries `X-RateLimit-Limit` and
`X-RateLimit-Remaining` headers so you can pace requests proactively.
When you exceed the limit the API responds with `429 Too Many Requests`
plus `Retry-After` (seconds to wait) and `X-RateLimit-Reset` (Unix
timestamp when the window resets) — back off until then and retry.
The `scrape-script` and `scrape-images` endpoints additionally draw from a
shared scraping capacity pool; when it is exhausted they return `429`
independently of your per-token allowance — retry later.

## CLI
Prefer the terminal? The official
[`@syllaby-ai/cli`](https://www.npmjs.com/package/@syllaby-ai/cli)
wraps this entire API in curated commands — faceless videos, text-to-video,
storyboards, ideas, characters, credits — plus a raw
`syllaby api <method> <path>` escape hatch for every endpoint.

```bash
npm install -g @syllaby-ai/cli
syllaby auth login          # paste an API key from ai.syllaby.io/settings/api-tokens
syllaby faceless create --topic "5 facts about Roman aqueducts" \
  --render --wait --download video.mp4
```

It authenticates with the same Bearer API keys and is subject to the same
per-token rate limits described above; `--json` on any command prints the
raw API envelope for scripting.

**[Full CLI documentation →](/cli.html)** — installation, authentication and
profiles, the complete command reference, waiting/download behavior, exit
codes, and scripting recipes.

## MCP
Prefer working from an AI agent? Syllaby runs a hosted remote MCP server at
`https://api.syllaby.io/mcp` that exposes the same functionality as native tools
for Claude and other MCP clients — no local process, no npm install. It
authenticates over OAuth or a bearer API key; the client walkthroughs below
cover both.

Prefer a local process instead? The official
[`@syllaby-ai/mcp-server`](https://www.npmjs.com/package/@syllaby-ai/mcp-server)
package wraps this REST API as MCP tools for Claude, Cursor, and any
MCP-compatible client — installable via npm, the MCP Registry, Smithery, or
as a desktop extension. Set `SYLLABY_API_KEY` and point
`SYLLABY_API_BASE_URL` at this API's base URL. Everything below is about the
hosted remote server, not this wrapper.

### Tools & billing
The server exposes **42 tools**: **31 are free** (reads, estimates, drafts,
scene edits, scene chaining, scheduler edits, publishing, uploads, tool
discovery) and **11 charge credits**. The
charging list is closed — these eleven and no others: `create_video`,
`generate_script`, `scrape_script`, `regenerate_video_asset`, `render_video`,
`export_video`, `generate_text_to_video`, `generate_storyboard`,
`generate_storyboard_media`, `create_scheduler` and `run_scheduler`.

Bulk scheduling is available as tools too (`create_scheduler`,
`get_scheduler`, `update_occurrence`, `estimate_scheduler`, `run_scheduler`)
— the lifecycle is documented alongside the Bulk Scheduler REST endpoints
when they publish. Two of them charge, and
neither is obvious from its name: `create_scheduler` renders nothing but its
`topic` path writes and bills one script per planned post (pass `scripts`
instead and it is free), and `run_scheduler` bills a full video render for
every planned post in a single call — price it with `estimate_scheduler`
first.

`chain_storyboard_scene` is free on every branch: it links two scenes and
moves a frame, and never generates or charges for a clip. Generating the
chained scene's clip is a separate, separately priced
`generate_storyboard_media` call, and the tool's `chain_status` field says
whether the frame is ready (`resolved`), on its way (`extracting`), or not
yet available (`deferred`).

The remote server requires an active **paid** Syllaby subscription — trial
accounts can connect but every tool call is rejected with
`PAID-SUBSCRIPTION-REQUIRED` until the subscription becomes paid.

Renders bill a **minimum of one minute** — a 20-second and a 50-second render
on the same engine cost the same.

**Credits are purchased at [syllaby.io](https://syllaby.io), never inside a
chat.** No tool on this server sells credits, tops up a balance, or upgrades
a plan — if your balance is short, the agent will say so and point you to
your account instead.

### Publish a single video
Four free tools cover the web app's Publish page for one video:
`publish_video` (now, or at `scheduled_at`), `list_publications`,
`reschedule_publication` and `cancel_publication`. `publish_video` posts
**publicly** on your channel and is not retry-safe, so the agent is told to
state the channel, caption and time and wait for your OK before calling it.
Channel ids come from `list_social_channels`; connect a channel in the web
app or with `connect_social_account`.

### Bring your own video
A video you made outside Syllaby — edited in ChatGPT or Claude, exported from
a desktop editor, shot on your phone — can be scheduled and published too. It
is imported as a private asset (free; it counts against your plan's storage),
and `publish_video` takes the returned `asset_id`. Which tool brings it in
depends on where the file is:

| Where the file is | Tool | What happens |
|---|---|---|
| At a **public URL** | `upload_custom_video` with `url` | Syllaby downloads it. The URL must be reachable without login and serve an mp4, jpg, png or webp. |
| **In the chat** (the assistant made or edited it) or **on your device** | `create_upload`, then `upload_custom_video` with `upload_id` | The agent gets a private upload link and either sends the file itself or hands the link to you. |

MCP has no way to carry a file inside a tool call, and a file in an
assistant's sandbox has no public URL, so `create_upload` moves the bytes
out of band:

1. The agent calls `create_upload` and receives an `upload_id` and an
   `upload_url`. The link is **private, single-use and valid for 60 minutes**,
   and accepts one mp4, jpg, png or webp up to **250 MB**.
2. The file goes to the link one of two ways:
   - **You upload it.** The agent shows you the link; open it, choose the
     file, wait for "Uploaded", and tell the agent you are done. This is the
     path in ChatGPT and in claude.ai, whose sandboxes cannot reach the
     internet on their own. If the assistant made the file, download it from
     the chat first.
   - **The agent uploads it.** A client with a shell and internet access
     (Claude Code, Cursor, VS Code) posts the file directly:
     ```bash
     curl -F "file=@/path/to/video.mp4" "<upload_url>"
     ```
3. The agent calls `upload_custom_video` with the `upload_id` and gets the
   `asset_id` back. Calling it before the file has arrived returns a
   "not uploaded yet" message rather than failing the flow.
4. `publish_video` with that `asset_id` and a `channel_id` posts or schedules
   it.

Treat the upload link like a password until it is used: anyone holding it can
add **one** file to your account during its 60 minutes. It can never read,
list or delete anything, and it stops working the moment a file is accepted.
The same file-type, size and storage-quota rules as the web app's upload
apply; a refused file leaves the link usable so you can retry with another.

### A tool is missing — refresh your client
MCP clients fetch a server's tool list when you add the connector, and most
keep that copy. When Syllaby ships a new tool, a client connected earlier
may not see it, and the assistant will say the action "is not exposed" or
"is not available" even though the server has it. Refresh the client's copy:

| Client | How to refresh |
|---|---|
| ChatGPT (developer mode) | Open the Syllaby connection in ChatGPT's app/connector settings ([chatgpt.com/plugins](https://chatgpt.com/plugins)), select **Refresh**, then start a **new conversation**. Existing conversations keep the old list. |
| claude.ai / Claude Desktop | Tools are re-read on the next connection: **Settings → Connectors**, disconnect Syllaby and connect it again, then start a new chat. Claude Desktop may also need a full quit and reopen. |
| Claude Code | Run `/mcp`, pick `syllaby`, choose **Reconnect**. |
| Cursor | Quit Cursor completely and reopen it. |
| VS Code | Run **MCP: Reset Cached Tools** from the Command Palette. |

Until you refresh, two tools that never change give the assistant a way in:
`discover_tools` returns the server's **live** catalog (all tools, one by
`name` with its input schema, or filtered by a `query` word), and `run_tool`
calls a tool from that catalog by name. Both are free. `run_tool`
deliberately **refuses the eleven charging tools and `publish_video`**: their
price and consent rules are attached to the real tool definitions, so those
must be called directly — which means refreshing the client as above. You can
prompt it yourself: *"Use discover_tools to check whether Syllaby has a tool
for this."*

The [Changelog](#changelog) lists every tool added to the server, with the
date, so you can tell whether a refresh is due.

### Tool approval prompts
Most MCP clients (Claude included) ask you to approve each tool call the
first time it runs. A full video run is 15–25 sequential calls, so with
per-call approval the flow pauses at every step. To let multi-step runs
flow, choose **"Always allow"** for the Syllaby connector's tools when the
first prompt appears.

Read-only tools (status checks, lists, estimates) are marked read-only in
the server's tool metadata, so clients that honor MCP annotations can skip
prompting for them — including the repeated "is the render done yet?" polls.

Know what "Always allow" covers: eleven tools charge credits (see Tools &
billing), and blanket approval lets the agent call them without a
click-through. The server instructs agents to state the price and get your
explicit OK in chat before every charging call, but that is guidance to the
model, not a hard gate — grant "Always allow" only in clients and accounts
you trust with spending.

### claude.ai — custom connector
1. Go to **Settings → Connectors → Add custom connector**, or use this
   one-click link:
   [Add Syllaby to claude.ai](https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=Syllaby&connectorUrl=https%3A%2F%2Fapi.syllaby.io%2Fmcp)
2. Adding manually instead? Set **Name** to `Syllaby` and **URL** to
   `https://api.syllaby.io/mcp`.
3. Authorize via the OAuth prompt — you'll be redirected to sign in to your
   Syllaby account.

### Claude Desktop — custom connector
1. Open **Settings → Connectors → Add custom connector**.
2. Set **Name** to `Syllaby` and **URL** to `https://api.syllaby.io/mcp`.
3. Complete the OAuth sign-in when prompted.

### Claude Code — OAuth
```bash
claude mcp add --transport http syllaby https://api.syllaby.io/mcp
```
Claude Code opens a browser to complete the OAuth flow; approve it and the
server is available in that session.

### Claude Code — headless (bearer token)
For CI or headless environments, skip OAuth with an API key instead:
```bash
claude mcp add --transport http syllaby https://api.syllaby.io/mcp \
  --header "Authorization: Bearer slb_..."
```
Generate the key from your Syllaby account settings.

### ChatGPT — developer mode
1. Enable **developer mode** in ChatGPT settings (Settings → Connectors →
   Advanced).
2. Add a custom connector with server URL `https://api.syllaby.io/mcp`.
3. Complete the OAuth sign-in when prompted. ChatGPT asks for a
   per-conversation confirmation before any write action.

ChatGPT keeps the tool list it read when you added the connector. After
Syllaby ships new tools, open the connection, select **Refresh** and start a
new conversation — see "A tool is missing" above.

### OAuth consent notes
Resubmitting a stale consent form (back button, duplicate tab) shows one of
two expected pages: **"You're already connected"** means the approval went
through — close the tab; **"This authorization page has expired"** means you
should restart the connection from the client. Neither is a failure.

## Changelog
- **2026-09-18** — MCP: `create_upload` brings in a video that has no public
  URL (made in the chat, or on your device) through a private single-use
  upload link, and `upload_custom_video` accepts the resulting `upload_id`.
  `discover_tools` and `run_tool` let a client holding an outdated tool list
  reach newly shipped free tools. 42 tools in total. Refresh your client to
  see them.
- **2026-09-17** — MCP: `upload_custom_video` imports a finished video from a
  public URL, and `publish_video` accepts its `asset_id`.
- **2026-09-06** — MCP: single-video publishing — `publish_video`,
  `list_publications`, `reschedule_publication`, `cancel_publication`.
- **2026-08-19** — Dedicated [CLI documentation page](/cli.html) added to this
  docs site.
- **2026-08-15** — Official CLI released:
  [`@syllaby-ai/cli`](https://www.npmjs.com/package/@syllaby-ai/cli) on npm.
- **2026-07-04** — Trial subscriptions are rejected with the dedicated
  `PAID-SUBSCRIPTION-REQUIRED` code; scene `duration` validates against the
  clip engine's supported values; `POST /text-to-scene/{storyboard}/scenes`
  persists `raw_video_prompt` and `duration` on create; `GET /faceless/options`
  accepts both CSV and array syntax for `only`.
- **2026-07-02** — Public API restricted to paid subscriptions (trials excluded).
  Render duration is floored at 1 second.
- **2026-07-01** — Dedicated B-roll, AI-Visuals, URL-to-Video, and Text-to-Scene modules.

## Error responses

Every endpoint shares these error shapes:

### `400`

Bad request — e.g. an `include` value outside the endpoint's allowlist.

```json
{
  "message": "Requested include(s) `foo` are not allowed. Allowed include(s) are `video, captions, media, music, voice, background, genre, watermark, character, assets`."
}
```

### `401`

Unauthenticated

```json
{
  "message": "Unauthenticated.",
  "error": {
    "code": "GEN-UNAUTHORIZED",
    "status": 401
  }
}
```

### `402`

Insufficient credits. The request is authenticated and valid, but the account balance is too low — top up and retry.

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

```json
{
  "message": "An active subscription is required.",
  "error": {
    "code": "SUBSCRIPTION-REQUIRED",
    "status": 403
  }
}
```

### `404`

Not found

```json
{
  "message": "Resource Not Found",
  "error": {
    "code": "GEN-NOT-FOUND",
    "status": 404
  }
}
```

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

```json
{
  "message": "The GET method is not supported for this route.",
  "error": {
    "code": "GEN-METHOD-NOT-ALLOWED",
    "status": 405
  }
}
```

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

```json
{
  "message": "Your email address is not verified.",
  "error": {
    "code": "EMAIL-NOT-VERIFIED",
    "status": 409
  }
}
```

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

```json
{
  "message": "The POST data is too large.",
  "error": {
    "code": "GEN-WRONG-ARGS",
    "status": 413
  }
}
```

### `422`

Validation error

```json
{
  "message": "The given data was invalid.",
  "errors": {
    "voice_id": [
      "The selected voice id is invalid."
    ]
  },
  "error": {
    "code": "GEN-VALIDATION-FAILED",
    "status": 422
  }
}
```

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

```json
{
  "message": "Too Many Attempts.",
  "error": {
    "code": "RATE-LIMIT-EXCEEDED",
    "status": 429
  }
}
```

### `500`

Server error.
