---
name: adrecast-api
description: Remake a proven video ad for the user's product through the AdRecast MCP server or REST API v1 — find a template ad, upload product images, review/edit the script, estimate credits, generate (only after explicit user approval of the cost), poll jobs, export and return the download URL. Use when the user wants to clone/remake/recreate a video ad, UGC ad or TikTok/Meta ad for their product programmatically.
---

# AdRecast API / MCP

Turn a winning video ad into the same ad for the user's product: same shots, framing and structure, the
user's product and selling points, new voice and captions.

- **MCP (preferred for agents):** streamable HTTP at `https://adrecast.com/api/mcp`, header `Authorization: Bearer rk_live_…`.
- **REST:** `https://adrecast.com/api/v1`, same Bearer key. OpenAPI: `https://adrecast.com/api/v1/openapi.json`.
- Keys: user creates them in **Settings → API keys** with scopes `read`, `write`, `spend`. Never print, log or
  commit the key; read it from an env var such as `ADRECAST_API_KEY`.

MCP client config example:

```json
{ "mcpServers": { "adrecast": { "url": "https://adrecast.com/api/mcp", "headers": { "Authorization": "Bearer ${ADRECAST_API_KEY}" } } } }
```

## Cost rules (read before spending)

- 100 credits = $1. Analysis, script, storyboard stills and script edits before video are free.
- Video is billed per generated second; customer resolutions are **720p, 1080p, 2K** only (fixed at project
  creation). 9:16 export is free; 1:1 / 16:9 re-frame costs 20 credits/s.
- Credits are **held** when generation starts and settled on completion; failed clips are refunded automatically.
- `generate_video` and `export_video` **refuse to spend unless `confirm: true`**. Without it you get
  `402 confirmation_required` with the estimate. Always call `estimate_cost`, show the user the total
  credits / USD and current balance, and only then retry with `confirm: true`. Use `max_credits` as a hard cap.
- Trial projects: 720p, watermark, trial credits usable only on that project. Unpaid accounts stop at the
  script stage (`402 script_only`) — tell the user to top up in the web app.

## End-to-end flow

| Step | MCP tool | REST |
|---|---|---|
| 1. Find a template ad | `search_templates` | `POST /templates/search` |
| 2. Upload 1–6 product images | `create_upload_url` → PUT bytes | `POST /uploads` → PUT `upload_url` |
| 2b. (Optional) custom presenter | `create_character`, `list_characters` | `POST`/`GET /characters` |
| 2c. (Optional) presenter voice | `list_voices`, `update_character`, `clone_character_voice` (spend) | `GET /voices`, `PATCH /characters/{id}`, `POST /characters/{id}/voice-clone` |
| 3. Create project | `create_project` | `POST /projects` |
| 4. Wait for the script (phase `script_review`) | `get_project` (poll) | `GET /projects/{id}` |
| 5. Review / edit script lines | `get_script`, `update_script` | `GET`/`PATCH /projects/{id}/script` |
| 5a. (Optional) rewrite by instruction | `rewrite_script` → `get_rewrite` | `POST /projects/{id}/script/rewrite`, `GET /rewrites/{id}` |
| 5c. (Optional) add / delete / reorder lines | `add_line`, `delete_line`, `reorder_lines` | `POST /projects/{id}/script/lines`, `DELETE /projects/{id}/script/lines/{clip_id}`, `PUT /projects/{id}/script/lines/order` |
| 5d. Confirm script → stills | `confirm_script` | `POST /projects/{id}/script/confirm` |
| 5b. (Optional) change presenter | `set_project_character` | `PUT /projects/{id}/character` |
| 6. Estimate | `estimate_cost` | `POST /projects/{id}/estimate` |
| 7. Generate (confirm) | `generate_video` | `POST /projects/{id}/generate` |
| 8. Poll | `get_job`, `list_clips` | `GET /jobs/{id}`, `GET /projects/{id}/clips` |
| 9. Export + download | `export_video`, `get_export` | `POST /projects/{id}/exports`, `GET /exports/{id}` |
| Hook A/B with your own hook | `create_hook_variant` | `POST /projects/{id}/variants/hook` |
| Balance | `get_balance` | `GET /balance` |

Details:
1. `search_templates {query, country?, status?, limit?}` → pick a result with a non-null `remake_url`.
   Or use any https TikTok / YouTube / Facebook (incl. Ad Library) / Instagram URL the user gives you.
2. For each image: `create_upload_url {kind:"product_image", filename, content_type:"image/png", size}` →
   `PUT upload_url` with `Content-Type` header and raw bytes → keep `path`.
   Optional custom presenter (free, reusable across projects): `create_character {name?, description?,
   image_paths?:[…], likeness_consent}`. Give an appearance description and/or 1–3 photos of ONE adult uploaded with
   `create_upload_url {kind:"person_image", …}`. Photos need `likeness_consent:true`: only send it after the user
   confirms it is them or they have the person's permission. Each character is moderated automatically before its
   first use (`moderation_status` pending → approved | rejected). Minors and celebrities or public figures are
   rejected, and the project then uses an automatic presenter. Reuse it later via `list_characters`.
   Optional voice for the presenter's voiceover: `list_voices` → `update_character {character_id, voice:"preset",
   voice_id}` (free), or clone the user's own voice: `create_upload_url {kind:"voice_sample", …}` with ≥30 s of one
   speaker → `clone_character_voice {character_id, sample_path, voice_consent:true}`. Only set `voice_consent` after
   the user confirms it is their own voice or they are authorized by the speaker. It costs a fixed 1000 credits: the
   first call returns `confirmation_required` with the price; show it, then resend with `confirm:true`. Poll
   `list_characters` until `voice.status` is `ready` (or `failed`, credits released; `voice_slots_full` = temporarily
   full, nothing charged). Unused clones are deleted after 60 days (`voice.expires_at`; each voiceover extends it);
   `keep_character_voice {character_id, confirm}` adds 60 days for 1000 credits. Song-mode projects ignore it.
3. `create_project {source:{type:"url", url}, resolution:"720p", product:{name, selling_points, image_paths:[path…]}, character_id? | character?, script_mode?, script_brief?, auto_confirm?}`.
   All materials go in this one call:
   - Presenter (optional): `character_id` from the library, or an inline `character {name?, description?,
     image_paths?, likeness_consent}` with the same rules as `create_character` (saved to the library; the response
     returns its `character_id`). Omit both for an automatic presenter.
   - Script mode: `script_mode:"replica"` (default) remakes the source script 1:1 for the product.
     `script_mode:"brief"` with `script_brief {instruction?, tone?, target_duration_s?}` (at least one) writes a new
     script from the user's requirements in one pass; the source ad is only a style / pacing reference.
   By default the project stops at phase `script_review` so the user can check the script first. Pass
   `auto_confirm:true` only when the user explicitly wants a one-shot run straight to stills.
4. Poll `get_project` every 15–30 s. Phase goes `analyzing → scripting → script_review → storyboard → generating → editing`.
5. In `script_review`, show the script lines (`get_script`) to the user. Everything here is free:
   - `update_script {project_id, lines:[{clip_id, line_text?, visual_desc?}]}` edits lines.
   - `rewrite_script {project_id, instruction?, tone?, target_duration_s?}` rewrites the whole script; poll
     `get_rewrite` until `applied`, then re-read `get_script`. Tones: friendly, energetic, premium, humorous, urgent,
     calm, authentic. The line count stays the same unless the target length needs more or fewer. Limit 5 per
     project per day (`429 rewrite_daily_limit`).
   - `add_line {project_id, after_clip_id|null, line_text?, visual_desc?, structure_tag?}`, `delete_line {project_id,
     clip_id}` (at least one must remain) and `reorder_lines {project_id, clip_ids:[…all lines in the new order]}`.
     Max 12 lines. Each call returns the updated video `estimate`.
   When the user approves the script: `confirm_script {project_id}`. Phase becomes `storyboard` and the look and
   stills are generated (free). Poll `list_clips` until clips are `still_ready`. Unpaid accounts get `402 script_only`.
   After confirm, structure changes and rewrites return `409 script_locked`. Single-line edits with `update_script`
   still work for scenes without video; ONLY that scene's still is regenerated (3 free per scene, then 20 credits;
   `402 confirmation_required` with an `estimate` → show it, then resend with `confirm:true`). Scenes that already
   have video return `409 clip_locked`; change them with the editor's revise flow (revise with an instruction).
   To change the presenter before any video: `set_project_character {project_id, character_id | null}`. This
   regenerates the look: 3 free per character, then 20 credits. After video starts it returns `409 video_started`.
6. `estimate_cost {project_id}` → `total_credits`, `total_usd`, `balance`, `shortfall`.
7. After the user says yes: `generate_video {project_id, confirm:true, max_credits:<total>}`.
   Ready stills are auto-confirmed. Optional `pilot:true` renders the riskiest clip first, `partial:true`
   generates as many clips as the balance covers.
8. Poll `list_clips` until every clip is `done`/`flagged`, or subscribe to webhooks.
   Hook A/B test: `create_hook_variant {project_id, hook_text}` creates a new sequence. Only the opening clip is
   regenerated (charged like normal generation); everything else is reused.
9. `export_video {project_id, aspect:"9:16", confirm:true}` (resolution defaults to the project's; no upscaling) → `export_id`; poll
   `get_export` until `download_url` is set. Give the user the URL.

## Examples

curl (key in env, never inline):

```bash
API=https://adrecast.com/api/v1
H="Authorization: Bearer $ADRECAST_API_KEY"
up=$(curl -s -X POST $API/uploads -H "$H" -H 'Content-Type: application/json' \
  -d '{"kind":"product_image","filename":"bottle.png","content_type":"image/png","size":'$(stat -c%s bottle.png)'}')
curl -s -X PUT "$(echo $up | jq -r .upload_url)" -H 'Content-Type: image/png' --data-binary @bottle.png
proj=$(curl -s -X POST $API/projects -H "$H" -H 'Content-Type: application/json' -H "Idempotency-Key: $(uuidgen)" \
  -d '{"source":{"type":"url","url":"https://www.tiktok.com/@brand/video/123"},"resolution":"720p",
       "product":{"name":"Solea Serum","selling_points":"vitamin C, 30-day glow","image_paths":["'$(echo $up | jq -r .path)'"]}}')
PID=$(echo $proj | jq -r .project_id)
curl -s -X POST $API/projects/$PID/estimate -H "$H" -H 'Content-Type: application/json' -d '{}'
# …after the user approves:
curl -s -X POST $API/projects/$PID/generate -H "$H" -H 'Content-Type: application/json' \
  -H "Idempotency-Key: gen-$PID-1" -d '{"confirm":true,"max_credits":600}'
```

TypeScript:

```ts
const API = 'https://adrecast.com/api/v1'
const key = process.env.ADRECAST_API_KEY!
async function call<T>(method: string, path: string, body?: unknown, idem?: string): Promise<T> {
  const res = await fetch(API + path, {
    method,
    headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', ...(idem ? { 'Idempotency-Key': idem } : {}) },
    body: body ? JSON.stringify(body) : undefined,
  })
  const json = await res.json()
  if (!res.ok) throw Object.assign(new Error(json.error?.message), { status: res.status, code: json.error?.code, details: json.error })
  return json as T
}

const est = await call<{ total_credits: number; total_usd: number; balance: number }>('POST', `/projects/${pid}/estimate`, {})
if (!(await askUser(`Generate for ${est.total_credits} credits ($${est.total_usd})?`))) return
const gen = await call('POST', `/projects/${pid}/generate`, { confirm: true, max_credits: est.total_credits }, `gen-${pid}`)
```

## Error handling

Errors are `{ "error": { "code", "message", … } }`. MCP tools return the same body with `isError: true`.

| Status / code | What to do |
|---|---|
| 400 `bad_request`, `url_not_supported`, `resolution_unavailable` | Fix input; don't retry blindly. |
| 400 `consent_required`, `character_empty`, `character_rejected`, `character_not_owned` | Ask the user for consent, a description or photo, or a different character. |
| 401 `unauthorized` | Key missing/revoked — ask the user for a new key. |
| 402 `confirmation_required` | Show `estimate`, get approval, resend with `confirm:true`. |
| 402 `insufficient_credits` / `over_budget` | Show `shortfall`; user tops up in the web app or raises the cap. |
| 402 `script_only` | Account must top up before video. |
| 403 `insufficient_scope` | Key lacks `write`/`spend`; ask for a key with that scope. |
| 404 `not_found` | Wrong id or not the user's project. |
| 409 `clip_locked`, `not_ready`, `nothing_to_generate`, `video_started`, `rewrite_in_progress`, `rewrite_not_ready`, `scene_limit`, `last_scene` | Wait (poll) or use the web editor's revise flow. |
| 409 `idempotency_in_flight`, 422 `idempotency_mismatch` | Wait and retry / use a new key for a different request. |
| 429 `rate_limited`, `daily_project_limit`, `daily_budget`, `rewrite_daily_limit`, `scene_add_daily_limit` | Respect `Retry-After`; back off. |
| 5xx | Retry with the same `Idempotency-Key` and exponential backoff. |

Always send an `Idempotency-Key` on POSTs that create or spend, so retries never double-charge.

## Webhooks

Settings → API keys → Webhooks. Events `job.completed`, `job.failed`; body
`{id, type, created_at, data:{job_id, job_type, status, project_id, clip_id, export_id, error}}`.
Verify `AdRecast-Signature: t=<unix>,v1=<hex>` (legacy receivers may read the identical `Replica-Signature`) where `v1 = HMAC_SHA256(secret, "<t>.<raw body>")`; reject if
`|now - t| > 300 s`. Retries with backoff (1 m, 5 m, 30 m, 2 h, 12 h). Deduplicate by `id`.

## Limits

120 requests/min per key; 20 spend calls/min per key; template search shares a small daily budget per user.
Polling every 15–30 s is plenty.
