---
name: promoat-partner-api
description: Operate the Promoat Partner API end to end — create AI presenter characters, clone or pick voices, synthesize speech, generate first-frame images, and produce talking-head or multi-scene videos, with credit estimates, job polling, billing, and dashboard guidance. Use whenever the user wants to generate presenter/UGC-style videos via Promoat, asks about their partner API key, credits, job status, or any /api/partner/v1 endpoint.
version: 1.0.0
---

# Promoat Partner API — agent operating manual

You are operating the **Promoat Partner API**: a REST API that turns a script into finished vertical marketing videos fronted by an AI presenter. This file is the complete source of truth — endpoints, prices, limits, playbooks, dashboard flows. Prefer it over guesses; when something here disagrees with an API response, trust the API response and tell the user.

## Quick facts

| Fact | Value |
|---|---|
| Base URL | `https://platform.promoat.ai` (override with env `PROMOAT_BASE_URL` if the user gives a different deployment) |
| API root | `{BASE}/api/partner/v1/` |
| Auth | `Authorization: Bearer $PROMOAT_KEY` on every request |
| Key format | `pmat_<12 hex>_<48 hex>` — the whole string is the credential |
| Content type | JSON in, JSON out. No SDK needed — plain `curl`/`fetch` |
| Async model | Video jobs return a `workflow_id`; **poll** `GET /jobs/{id}/status`. No webhooks |
| Human docs | `{BASE}/docs` |
| Swagger playground | `{BASE}/docs/playground` — full spec with try-it-out |
| OpenAPI spec | `{BASE}/openapi.yaml` |
| Partner dashboard | `{BASE}/partner` (sign in at `{BASE}/sign-in`, create account at `{BASE}/sign-up`) |

Set up your shell once per session:

```bash
BASE="${PROMOAT_BASE_URL:-https://platform.promoat.ai}"
# PROMOAT_KEY must come from the environment — see "Key handling" below.
```

## Hard rules (always apply)

**Key handling — never violate these:**
1. Read the key from the `PROMOAT_KEY` environment variable (or a gitignored `.env`). If it's not set, do NOT ask the user to paste it into the chat — chats get logged and synced. Instead, tell them to run `export PROMOAT_KEY="pmat_..."` in their shell (or add it to `.env` + `.gitignore`), then continue.
2. Never print, echo, log, or commit the key. In any output you show the user, redact it to `pmat_…`. Never write it into source files, scripts, or docs — only `.env` files that are gitignored.
3. If the user pastes the full key into the chat anyway, use it for this session but recommend they rotate it afterward in the dashboard (`{BASE}/partner` → API key → rotate). Rotation kills the old key instantly.
4. This is a server-side key that spends real prepaid credits. Never embed it in frontend code, mobile apps, or anything a third party can read.

**Spending — credits are real money:**
5. Before every `POST /videos`, call `POST /videos/estimate` with the **same body** (free, no side effects), and tell the user the quote in credits before firing. The reserve will equal the quote. For anything over ~300 credits, get an explicit go-ahead unless the user already told you to proceed without asking.
6. On `402 INSUFFICIENT_CREDITS`, report `credits_required` vs `credits_balance` and point the user to the dashboard's **Buy credits** card. Don't loop-retry a 402.

**Retries — protect the user from double-charges:**
7. GETs (status polls, lists, summaries) are always safe to retry.
8. Never blind-retry a `POST /videos` that timed out on your side — it may have queued. First check `GET /usage/jobs?limit=5`; if a new job is there, it's running and reserved. A duplicate POST reserves credits for a second job.
9. A `failed` job auto-refunds its reservation. Re-queueing the same request once is reasonable; if it fails twice, stop and show the user the `error_message`.

**Polling etiquette:**
10. Poll every 8–10 seconds. Talking-head jobs typically finish well inside 10 minutes; multi-scene inside 25. `completed`/`failed` are terminal — stop polling and cache the result.

**Bookkeeping:**
11. Keep every id the API returns (`character_id`, `voice_id`, `workflow_id`, `asset_id`) in your working notes — ids are shown once at creation. If one is lost, recover it from `GET /characters`, `GET /voices`, `GET /usage/jobs`, or `GET /assets`.
12. When a job completes, always hand the user the `video_url` (or `image_url`/`audio_url`) **and** `credits_consumed`.

## First-run setup (walk the user through this)

If the user has no key or no credits yet, guide them:

1. **Create an account** — `{BASE}/sign-up` (email + password, organization name). Then open the dashboard at `{BASE}/partner`.
2. **Get the API key** — the workspace card on the dashboard has the API key panel: reveal it (signed-in only; it's stored encrypted) and rotate it any time. Keys look like `pmat_1a2b3c4d5e6f_…`.
3. **Store it securely** — `export PROMOAT_KEY="pmat_..."` in their shell profile, or in a gitignored `.env`. Not in chat, not in code.
4. **Buy credits** — dashboard → **Buy credits** → Stripe checkout. Credits are sold in multiples of 100 at **$1.15 per 100 credits** (itemized at checkout), minimum purchase 100 credits:

   | Credits | Price |
   |---|---|
   | 100 | $1.15 |
   | 1,000 | $11.50 |
   | 5,000 | $57.50 |
   | 10,000 | $115.00 |

   Credits land within a few seconds of payment; receipts appear in the dashboard's payment history. For scale: a short talking-head video runs ~150–300 credits, a 30-second multi-scene video ~550–700 credits (exact numbers below).
5. **Verify** — run the free balance check:

```bash
curl -s "$BASE/api/partner/v1/usage/summary" -H "Authorization: Bearer $PROMOAT_KEY"
# → { "credits_balance": 1131, ... }   ✓ key works
```

A `401` means the key is wrong or truncated; re-copy it from the dashboard.

## How the API composes

Two reusable building blocks, then videos on top:

- **Character** (`character_id`) — the on-screen presenter. Clone a real person from 1–3 selfies, or invent one from a text description. Create once, reuse forever.
- **Voice** (`voice_id`) — clone a real voice from audio samples (30 credits), or pick a free ready-made library voice by language.
- **Videos** — `mode: "talking_head"` (one continuous shot, presenter talks to camera, optional dynamic captions, 480p/720p) or `mode: "scenes"` (the character *acts the script out* across several scenes — walking, showing, doing — same face and voice throughout, delivered as one continuous 720p vertical video).
- **Standalone outputs** — TTS audio and first-frame stills, usable on their own or fed back into video jobs.

Everything generated lands in the **asset ledger** (`GET /assets`) with a durable URL. All ids are scoped to the key's account; requesting someone else's resource returns the same `404` as a nonexistent one.

Typical first session: character → voice → estimate → video → poll → deliver URL. Steps 1–2 are one-time setup.

## Social video script workflow

When the user arrives with an idea rather than a finished script, guide the creative work before touching a paid
endpoint. The goal is an approved, duration-safe script and the right Promoat video mode. Do not render immediately.

1. **Collect the smallest useful brief.** Establish:
   - the objective: sell, explain, announce, or tell a behind-the-scenes story;
   - the topic, product, offer, or update;
   - the intended audience;
   - the target platform, duration, language, and tone;
   - the action the viewer should take after watching.
   Do not interrogate the user for details they already provided. If a non-critical detail is missing, use a sensible
   default and state it briefly.
2. **Offer three genuinely different hooks.** Keep each hook short enough to say naturally in the opening seconds.
   Vary the angle. Use a direct benefit, a curiosity gap, and a relatable problem instead of rewriting the same
   sentence three times. Ask the user to choose one or combine elements.
3. **Recommend the format.**
   - Use `talking_head` for direct promotions, announcements, explainers, expert takes, and calls to action where one
     presenter speaking to camera is the clearest treatment.
   - Use `scenes` for tours, day-in-the-life stories, product-in-hand demonstrations, or ideas that benefit from the
     presenter walking, showing, and doing things across locations.
   Explain the recommendation in one sentence and let the user override it.
4. **Write for the requested duration.** Use conversational spoken language, not article prose. Open with the chosen
   hook, deliver one coherent idea, and end with one clear CTA. As a planning target, use roughly 35 words for 15s,
   55–65 words for 25s, 90–100 words for 40s, and no more than ~140 words for the platform maximum. The API estimate
   remains authoritative.
5. **Make multi-scene scripts filmable.** Write concrete actions and transitions that the same presenter can perform.
   When forming `video_idea`, follow the identity-safety and single-presenter rules in P7. Use the user's real product,
   setting, or prop images as `scene_elements` when available.
6. **Get script approval.** Show the complete script and proposed format before calling `/videos/estimate`. Offer useful
   revisions such as “stronger hook,” “more conversational,” “shorter,” or a language/tone change.
7. **Estimate, then get spending approval.** After the script is approved, assemble the exact request body and call the
   free estimate. Report the quote and material choices. Follow the spending rules above before calling `/videos`.

Never claim that Promoat itself researched facts used in a script unless a connected research source was actually used.
For factual, medical, legal, financial, or current-events claims, verify them through an appropriate source or ask the
user to provide approved copy before rendering.

## Playbooks

All bodies below show the maximum useful parameter set; optional fields are marked. Input URLs (selfies, audio samples, scene stills) must be **public https** links.

### P1 — Create a character

**From selfies (reproduces the exact person):**
```bash
curl -s -X POST "$BASE/api/partner/v1/characters" \
  -H "Authorization: Bearer $PROMOAT_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "Omar",
    "selfie_image_urls": ["https://your-cdn.com/selfies/omar-1.jpg",
                          "https://your-cdn.com/selfies/omar-2.jpg",
                          "https://your-cdn.com/selfies/omar-3.jpg"],
    "edit_instructions": "professional attire, studio lighting"
  }'
```
1–3 selfies; `edit_instructions` (optional, ≤500 chars) tweaks styling while keeping identity. `422` means a selfie URL couldn't be loaded.

**From a description (invents a person):**
```bash
curl -s -X POST "$BASE/api/partner/v1/characters" \
  -H "Authorization: Bearer $PROMOAT_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "Ava",
    "description": "a friendly, upbeat presenter in her early 30s, warm smile",
    "reference_image_urls": ["https://your-cdn.com/style/mood-lighting.jpg"],
    "recast_attempt": 1
  }'
```
`reference_image_urls` (≤3, optional) are loose style inspiration, never an identity to copy. If the user dislikes the invented face, re-run with `recast_attempt` incremented (1, 2, …) to force a noticeably different person.

Both cost **10 credits** and return `{ character_id, mode, sheet_image_url, credits_charged }`. Show the user `sheet_image_url` so they approve the look before spending on videos. Manage with `GET /characters`, `GET /characters/{id}`, `DELETE /characters/{id}`.

> **`sheet_image_url` is a multi-pose reference sheet, not a video frame.** Show it to the user, and pass `character_id` to the video endpoints — never pass `sheet_image_url` as `first_frame_url`. Video generation animates whatever single image it is given, so a sheet produces a video of the grid of poses instead of the presenter. To control the opening look, generate a real frame with `POST /images/first-frame` (P4) and pass that.

### P2 — Get a voice

**Free library pick by language** (best default — costs nothing):
```bash
curl -s -X POST "$BASE/api/partner/v1/voices" \
  -H "Authorization: Bearer $PROMOAT_KEY" -H "Content-Type: application/json" \
  -d '{ "name": "Spanish narrator", "language": "spanish", "gender": "female" }'
```
`language` takes a name or ISO code ("english", "british english", "arabic", "es"…); `gender` is `female` (default) or `male`. The pick is verified with a tiny speech probe before saving. `404` = no match for that language; try a broader name. Cap: 10 library voices.

**Clone a real voice** (30 credits):
```bash
curl -s -X POST "$BASE/api/partner/v1/voices" \
  -H "Authorization: Bearer $PROMOAT_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "Ava voice",
    "sample_urls": ["https://your-cdn.com/audio/ava-sample-1.mp3",
                    "https://your-cdn.com/audio/ava-sample-2.mp3"]
  }'
```
1–25 sample URLs, each at least ~5 seconds of clean speech; more/cleaner samples → better clone. Cap: 3 cloned voices (separate from the library cap); `409 VOICE_LIMIT_REACHED` means delete an unused one first (`DELETE /voices/{id}` frees the slot).

Both return `{ voice_id, kind: "cloned"|"library", credits_charged }`.

### P3 — Text-to-speech (standalone audio)

```bash
curl -s -X POST "$BASE/api/partner/v1/tts" \
  -H "Authorization: Bearer $PROMOAT_KEY" -H "Content-Type: application/json" \
  -d '{
    "voice_id": "VOICE_ID",
    "text": "Welcome back! Here is what is new this week.",
    "language_code": "en",
    "voice_settings": { "stability": 0.5, "similarity_boost": 0.75, "style": 0.3, "speed": 1.1 }
  }'
```
`text` ≤5,000 chars; `language_code` and `voice_settings` optional (partial override: `stability`, `similarity_boost`, `style`, `speed`, `use_speaker_boost`). **10 credits per 1,000 characters (minimum 10).** Returns `{ audio_url, duration_sec, asset_id, credits_charged }`. Note: for videos you normally do NOT pre-generate TTS — pass `voice_id` + `script` to `/videos` and speech is synthesized inside the job.

### P4 — First-frame image (standalone still)

```bash
curl -s -X POST "$BASE/api/partner/v1/images/first-frame" \
  -H "Authorization: Bearer $PROMOAT_KEY" -H "Content-Type: application/json" \
  -d '{
    "character_id": "CHARACTER_ID",
    "scene_description": "standing behind the counter of a bright specialty coffee shop, morning light"
  }'
```
**6 credits** → `{ image_url, asset_id, credits_charged }`. Pro move for talking-head: generate one frame, let the user approve the look, then pass it to the video as `first_frame_url` — the job then skips its own frame generation (and its 6-credit line item), and the look is locked before the expensive render.

Only ever pass an `image_url` from THIS endpoint (a single-subject scene frame) as `first_frame_url`. Do not pass the character's `sheet_image_url`: it is a grid of poses, and the renderer would animate the grid.

### P5 — Talking-head video (script → finished video)

Always estimate first, then queue with the identical body:

```bash
BODY='{
  "mode": "talking_head",
  "character_id": "CHARACTER_ID",
  "voice_id": "VOICE_ID",
  "script": "Hi! Three quick updates from the shop this week. First, we extended our opening hours.",
  "video_idea": "standing in a bright modern kitchen, smiling at the camera",
  "resolution": "480p",
  "use_dynamic_captions": true,
  "first_frame_url": "https://your-cdn.com/frames/opening-shot.jpg"
}'

curl -s -X POST "$BASE/api/partner/v1/videos/estimate" \
  -H "Authorization: Bearer $PROMOAT_KEY" -H "Content-Type: application/json" -d "$BODY"
# → { "credits_required": 256, "credits_balance": 961, "sufficient_credits": true,
#     "estimated_seconds": 7.4, "breakdown": { ... } }

# User approves the price → same body to /videos:
curl -s -X POST "$BASE/api/partner/v1/videos" \
  -H "Authorization: Bearer $PROMOAT_KEY" -H "Content-Type: application/json" -d "$BODY"
# → { "workflow_id": "partner_thv_…", "status": "queued", "credits_reserved": 256 }
```

Parameter rules:
- Audio source is exactly one of `{voice_id + script}` **or** `{audio_url}` (see P6). Script is spoken verbatim, never trimmed.
- **Length cap: 59 seconds of measured audio.** Pre-check scripts at ~2.4 words/second (≈140 words max); if over, shorten the script *before* submitting rather than burning a 400.
- `video_idea` (≤2,000 chars) is required unless `first_frame_url` is given; both is fine (`first_frame_url` wins the opening look).
- `first_frame_url` must be a **single-subject scene frame** — an `image_url` from `POST /images/first-frame`, or your own equivalent still. Never the character's `sheet_image_url` (a multi-pose grid): the renderer animates the image it is handed, so a sheet yields a video of the grid, not the presenter. Identity comes from `character_id`, so you never need to pass the sheet.
- `resolution`: `480p` (default, cheapest) or `720p`.
- `use_dynamic_captions` optional (boolean) — set `true` to burn in **dynamic captions**: bold captions that reveal word by word as the character speaks, key words popped in colour, rendered heavy-weight on a solid background band so they stay readable over any footage. Renders both Latin and Arabic script. Flat +10 credits when on; omit or `false` for none.
  - `captions_language` optional — speech-language hint, e.g. `"en"` or `"ar"`; omit to auto-detect. Both scripts render either way.
  - `captions_position` optional — `"bottom"` (default) or `"top"`: which edge the caption band sits on. **Making them visible:** the captions fill a band at that edge and emphasized words pop larger toward the centre, so pick/generate a first frame whose subject leaves that edge clear (keep the face upper-to-middle, nothing critical along the bottom); switch to `"top"` if your composition needs the bottom.
- Every talking-head is finished with a studio lipsync pass automatically; there is no lipsync knob.

### P6 — Talking-head from pre-recorded audio

Replace `voice_id`/`script` with `audio_url` — the duration is probed server-side (no duration param needed):

```bash
-d '{
  "mode": "talking_head",
  "character_id": "CHARACTER_ID",
  "audio_url": "https://your-cdn.com/audio/narration.mp3",
  "video_idea": "seated at a desk, softly lit studio",
  "resolution": "720p"
}'
```
Same 59-second measured cap; `422` on estimate means the audio URL couldn't be probed (must be public https audio).

### P7 — Multi-scene video (the character acts the script out)

The flagship: same face and voice across several scenes — walking, showing, doing — delivered as one continuous 720p vertical (9:16) video.

> **Prompt discipline for scenes (identity safety) — read before writing `video_idea`.**
> Scenes has **no gender parameter**: identity is carried by the character-sheet image, and the on-screen gender/person is inferred from your `video_idea`. A vague idea lets the model drift the presenter's gender or invent extra people. So for any cloned presenter:
> - **State the gender and lock to one person** — e.g. `one male presenter, the same man throughout; do not add, show, or cut to any other people; do not restyle or recast the presenter.`
> - **Describe the presenter *doing* things, not a scene full of people** — phrases like "showing athletes training" invite b-roll humans that hijack the frame.
> - **Template:** `<gender> <role>, the same person in every scene, <2–3 concrete actions>. Single presenter only — no other people, no recasting.`

```bash
BODY='{
  "mode": "scenes",
  "character_id": "CHARACTER_ID",
  "voice_id": "VOICE_ID",
  "script": "Ever wondered how we roast our beans? Come on, I will show you. This is where every batch starts — green, dense, and full of potential.",
  "video_idea": "a barista giving a behind-the-scenes tour of a small coffee roastery",
  "narration_mode": "auto",
  "scene_elements": [
    { "url": "https://your-cdn.com/products/retail-bag.jpg", "role": "product", "caption": "our 250g retail bag" },
    { "url": "https://your-cdn.com/shots/storefront.jpg",    "role": "setting", "caption": "the shop exterior" },
    { "url": "https://your-cdn.com/props/scoop.jpg",         "role": "prop",    "caption": "brass coffee scoop" }
  ],
  "fast": false,
  "topic": "specialty coffee roasting",
  "audience": "coffee enthusiasts",
  "language": "english"
}'
# estimate → confirm with user → POST /videos with the same body (workflow_id: partner_scn_…)
```

Parameter rules:
- `voice_id` + `script` are **required** (no `audio_url` in this mode). **Length cap: 60.5 seconds of estimated narration** (~145 words) — a 400 up front, before anything is charged.
- `resolution` and `captions` are ignored — scenes always renders 720p vertical, uncaptioned.
- `narration_mode`: `auto` (default — decides on-camera speech vs voiceover per scene) or `voiceover` (forces narration everywhere; use for product-heavy videos).
- `scene_elements` (≤6, optional): real stills woven into the video — `role` is `product`, `setting`, or `prop`; `caption` ≤200 chars tells the director what it is. Use these whenever the user has real product shots — it grounds the video in their actual business.
- `fast: true`: lower latency, ~25% cheaper per second, slightly lower quality — offer it when the user is price-sensitive or iterating.
- `topic` / `audience` / `language` (optional) steer the creative direction and spoken language.
- Expect ~550–700 credits for a ~30s standard-tier video and 15–25 minutes of render time; while running, the job's row in `GET /usage/jobs` shows `metadata.phase` (`generating` → `finalizing`) you can surface as progress.

### P8 — Poll and deliver

```bash
while :; do
  R=$(curl -s "$BASE/api/partner/v1/jobs/$WORKFLOW_ID/status" -H "Authorization: Bearer $PROMOAT_KEY")
  S=$(echo "$R" | jq -r .status)
  [ "$S" = "completed" ] || [ "$S" = "failed" ] && break
  sleep 10
done
echo "$R" | jq '{status, video_url, credits_consumed, error_message}'
```
On `completed`, deliver `video_url` + `credits_consumed` to the user. On `failed`, show `error_message`; the reservation was refunded automatically (job's `credits_consumed` becomes `0` unless partial work was billed).

### P9 — Account, spend, and history

```bash
# Live balance + job counts (free; date range optional, defaults to current UTC month)
curl -s "$BASE/api/partner/v1/usage/summary?from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z" \
  -H "Authorization: Bearer $PROMOAT_KEY"

# Job history, newest first (limit ≤100)
curl -s "$BASE/api/partner/v1/usage/jobs?limit=30&offset=0" -H "Authorization: Bearer $PROMOAT_KEY"

# Everything ever generated, with durable URLs (kind: character_sheet | first_frame | tts_audio | video)
curl -s "$BASE/api/partner/v1/assets?kind=video&limit=30&offset=0" -H "Authorization: Bearer $PROMOAT_KEY"
```

Use these to recover lost ids, audit spend (`credits_estimated` vs `credits_actual` per job), or re-fetch old output URLs.

## Endpoint reference

| Method & path | What it does | Cost |
|---|---|---|
| `POST /characters` | Create presenter (selfies or description mode) | 10 |
| `GET /characters` · `GET /characters/{id}` | List / fetch (max 100, newest first) | free |
| `DELETE /characters/{id}` | Delete | free |
| `POST /voices` | Clone from `sample_urls`, or free library pick via `language` | 30 / free |
| `GET /voices` · `DELETE /voices/{id}` | List / delete (delete frees a clone slot) | free |
| `POST /tts` | Speech from your voice | 10 per 1,000 chars (min 10) |
| `POST /images/first-frame` | Still of a character in a scene | 6 |
| `POST /videos` | Queue `talking_head` or `scenes` job → `workflow_id` | reserve, see below |
| `POST /videos/estimate` | Exact quote for the same body — no side effects | free |
| `GET /jobs/{workflowId}/status` | Poll status / result / settled cost | free |
| `GET /assets` · `GET /assets/{id}` | Output ledger | free |
| `GET /usage/summary` | Balance + job counts in range | free |
| `GET /usage/jobs` | Paginated job history (has `metadata.phase`) | free |

Video per-second rates (sum of line items = job total; the estimate returns the exact breakdown):

| Line item | Credits / second |
|---|---|
| Talking-head render @ 480p | 8 |
| Talking-head render @ 720p | 15 |
| Studio lipsync (always included in talking-head) | ~13.4 |
| Multi-scene standard — 1st scene / later scenes | 31 / 19 |
| Multi-scene fast — 1st scene / later scenes | 24 / 15 |

Plus flat add-ons where applicable: speech synthesis when `script` is passed, first-frame generation when the job creates its own (skipped when `first_frame_url` is provided), captions +10. Sync endpoints return `credits_charged` (what you see is what you paid). `POST /videos` returns `credits_reserved`; completion settles to the **measured actual** and refunds any over-reserve.

## Limits

| Limit | Value | Signal |
|---|---|---|
| Active video jobs (queued + running) | 5 | `429 TOO_MANY_ACTIVE_JOBS` |
| Cloned voices | 3 | `409 VOICE_LIMIT_REACHED` |
| Library voices (separate cap) | 10 | same `409` |
| Talking-head audio | ≤59s measured | `400` at queue time |
| Multi-scene narration | ≤60.5s estimated | `400` at queue time |
| TTS text | 5,000 chars | `400` |
| Selfies per character | 1–3 | `400` |
| Voice samples per clone | 1–25 (≥~5s each) | `400` |
| Scene elements | ≤6 | `400` |
| List page size | ≤100 | `400` |

## Errors

Every error is JSON `{ "error": "..." }` with an honest status code. Nothing is charged on a request that fails validation, auth, or the balance check.

| Code | Meaning | Your move |
|---|---|---|
| 400 | Validation — message names the exact field | Fix and resend |
| 401 | Bad/missing key | Re-check `PROMOAT_KEY`; test with `/usage/summary` |
| 402 | `INSUFFICIENT_CREDITS` + `credits_required` + `credits_balance` | Tell user the gap; dashboard → Buy credits |
| 404 | Not found — or not yours (existence never leaks); also "no library voice for language" | Verify the id came from this account |
| 409 | `VOICE_LIMIT_REACHED` + `max_voices` | Delete an unused voice |
| 422 | Input media unreachable (selfies / audio probe) | Make URLs public https serving real media |
| 429 | `TOO_MANY_ACTIVE_JOBS` + `max_active_jobs` | Wait for a job to finish, re-queue |
| 5xx | Server side | Retry GETs freely; for POSTs follow hard rule 8 |

## Troubleshooting

- **Estimate looks high** → check `breakdown`; drop to `480p`, shorten the script, omit captions, or use `fast: true` (scenes).
- **User hates the generated face** → description mode: bump `recast_attempt`. Photo mode: better-lit selfies, or `edit_instructions`.
- **Wrong language/accent in speech** → set `language` (scenes) / `language_code` (tts), or pick a library voice for that language (free) instead of forcing a clone.
- **`404` on a voice you just used** → voice ids are account-scoped strings from `POST /voices`; run `GET /voices` to list what actually exists.
- **Job stuck in `running`** → within the 10/25-minute envelopes it's genuinely working. Beyond that, keep the `workflow_id`, check `GET /usage/jobs` for `metadata.phase`, and have the user contact support from their dashboard account if it never terminates.
- **Same request, want another take** → just re-queue; each job is an independent generation. Vary `video_idea` for a different setting.

## URL recap for humans

- Docs: `{BASE}/docs` · Playground: `{BASE}/docs/playground` · Spec: `{BASE}/openapi.yaml`
- Dashboard (key, balance, buy credits, receipts): `{BASE}/partner`
- New account: `{BASE}/sign-up`

This skill ships from `{BASE}/skill/SKILL.md` — re-download occasionally to pick up API changes.
