Developers

Developer API

Start a commercial from a brief, read its storyboard, approve it at the price you were quoted, reshoot a shot and fetch the finished files, all from your own tools. Agencies add client workspaces, set each client's brand and read a usage report to bill them.

Getting started

  1. Sign in and open API keys from your account menu.
  2. Enter a name, choose the permissions and press Create key. Copy the key straight away: it is shown once.
  3. Send it with every request in the Authorization header.
export CC_API_KEY="cc_live_…"
curl https://customcommercials.ai/api/v1/me -H "Authorization: Bearer $CC_API_KEY"

Every response is JSON. Times are ISO 8601 in UTC, and every link to a file is a full URL.

Keys and permissions

  • Personal keys act as you, across your account and (for agencies) every client workspace. Anyone can make one. They stop working if you leave the account.
  • Workspace keys belong to the account, or to one client workspace only. Owners and admins make and revoke them. They keep working when people leave.
  • Each key has the read permission, the write permission, or both. Reading needs read; starting, editing, approving and deleting need write. Approving a commercial also needs an owner or admin behind the key.
  • We only store a fingerprint (SHA-256) of each key, so we can't show it again. If you lose one, revoke it and make another.
  • Keys are secrets. Keep them on your server, never in a public web page.

Account

Base URL: https://customcommercials.ai/api/v1

GET /me read scope

Checks a key and shows who it belongs to. user is the key's owner for a personal key, or whoever made a workspace key.

curl https://customcommercials.ai/api/v1/me -H "Authorization: Bearer $CC_API_KEY"

Response

{
  "key": { "id": "…", "name": "Build server", "kind": "personal", "scopes": ["read", "write"], "rateLimitPerMinute": 120 },
  "user": { "id": "…", "name": "Sam", "email": "sam@example.com", "role": "owner" },
  "account": { "id": "…", "name": "Acme", "kind": "agency" },
  "workspace": { "id": "…", "name": "Acme", "kind": "agency" }
}

DELETE /key read scope

Revokes the key that sends the request, straight away. Works with a key of either permission. Returns 204 with no body.

curl -X DELETE https://customcommercials.ai/api/v1/key -H "Authorization: Bearer $CC_API_KEY"

GET /workspaces read scope

The workspaces this key can use. Agencies see each client workspace; pass its id as workspace to work in it.

curl https://customcommercials.ai/api/v1/workspaces -H "Authorization: Bearer $CC_API_KEY"

Response

{
  "data": [
    { "id": "ws_1", "name": "Acme", "kind": "agency", "isClient": false, "default": true },
    { "id": "ws_2", "name": "Bright Dental", "kind": "client", "isClient": true, "default": false }
  ]
}

Commercials

A commercial is made from a brief: AI spokespeople (realistic presenters who say your lines on camera), cinematic B-roll (the shots between the talking) and your product, rendered in wide 16:9, vertical 9:16 and square 1:1. The storyboard, a shot-by-shot plan with a preview still for most shots (B-roll, stills and the end card; spokesperson shots and your own product media have none), costs nothing. A commercial is paid for when it is approved, at the price of its format and finished length: the formats and their prices are in pricing from GET /commercials, never in this page.

Through the API a commercial is paid from the account's commercial balance. If the balance does not cover it, approving answers 402 commercial_time_needed with what is due and the page where an owner can pay.

GET /commercials read scope

The commercials in a workspace, newest first, and the live price: each format on sale with its base length, its price for that length, the step a longer cut is billed in and its price, and its longest length (formats); the default format; the minimum per ad; the reshoots included per shot; and how long bought commercial time stays usable. The base rate per finished minute (centsPerMinute, stepSeconds) is only what a format without a price of its own is billed at. Read pricing here rather than saving a number: it follows the settings.

curl "https://customcommercials.ai/api/v1/commercials?workspace=ws_2" -H "Authorization: Bearer $CC_API_KEY"

Response

{
  "pricing": { "enabled": true, "formats": [ { "id": "commercial_30", "name": "30-second commercial", "baseSeconds": …, "fromCents": …, "stepSeconds": …, "stepCents": …, "maxSeconds": …, … } ], "defaultFormat": "commercial_30", "minimumPriceCents": …, "includedReshootsPerShot": …, "centsPerMinute": …, "stepSeconds": …, "minSeconds": …, "maxSeconds": …, "minCents": …, "spokespeople": …, "validDays": … },
  "data": [ { "id": "…", "status": "ready", "title": "Brightdesk launch ad", … } ],
  "workspace": { "id": "ws_2", "name": "Bright Dental" }
}

POST /commercials write scope

Start a commercial. format (optional) is one of the formats in pricing.formats (the 30-second commercial without it) and seconds is the length you want, from that format's base length to its longest; tone is friendly, premium, energetic or serious; presenters (optional) are stock presenters and voices from GET /premium/presenters. Optional: presenterEngine, clipProduct (video_premium or video_fast), productMedia (a media id of your product, so it is on screen) and notes. Returns 202 while the storyboard is written: poll GET /commercials/:id until writing is false. Nothing is charged.

curl https://customcommercials.ai/api/v1/commercials -H "Authorization: Bearer $CC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "workspace": "ws_2",
    "brief": {
      "product": "Brightdesk, a help desk that answers routine questions for small support teams",
      "audience": "support leads at small software companies",
      "seconds": 60, "tone": "friendly",
      "cta": "Start with Brightdesk", "url": "https://brightdesk.example",
      "presenters": [{ "avatarId": "HEYGEN_AVATAR_ID", "voiceId": "HEYGEN_VOICE_ID", "name": "Anna" }]
    }
  }'

GET /commercials/:id read scope

The brief, the shots (with preview stills before approval and each take after), anything stopping approval (problems), and the price (quote) or, once approved, the reshoot allowance. status is draft, generating, rendering, ready or failed. When it is ready, videoUrl is the finished wide video.

curl https://customcommercials.ai/api/v1/commercials/COMMERCIAL_ID -H "Authorization: Bearer $CC_API_KEY"

Response

{
  "id": "…", "status": "draft", "writing": false, "title": "Brightdesk launch ad",
  "shots": [
    { "id": "…", "type": "spokesperson", "line": "If you run support at a small company…", "onScreenSeconds": 4.4,
      "presenter": 0, "previewUrl": null, "take": null },
    { "id": "…", "type": "broll", "seconds": 5, "narration": "…", "prompt": "…", "title": "", "titleStyle": "none",
      "previewUrl": "https://customcommercials.ai/files/…jpg", "take": null }
  ],
  "problems": [],
  "quote": { "contentSeconds": 54.2, "billedSeconds": 60, "priceCents": …, "fromBalanceSeconds": 0, "dueSeconds": 60, "dueCents": … },
  "allowance": null, "balanceSeconds": 0,
  "videoUrl": null, "tutorialId": "…", "tutorialStatus": "draft", "needsRender": false
}

PATCH /commercials/:id write scope

Edit a draft: brief, shots (the full ordered list; a shot without an id is new), title or outro. Shots that need one get a preview still, up to the free limit per commercial.

curl -X PATCH https://customcommercials.ai/api/v1/commercials/COMMERCIAL_ID -H "Authorization: Bearer $CC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "shots": [ { "id": "SHOT_1", "type": "spokesperson", "line": "Meet Brightdesk." }, { "type": "endcard", "seconds": 3, "prompt": "A warm amber gradient", "title": "brightdesk.example", "titleStyle": "center" } ] }'

POST /commercials/:id/storyboard write scope

Rewrite the storyboard from the brief. A draft can be rewritten a few times (the number is in the settings); past that the reply says so. Returns 202; poll until writing is false.

curl -X POST https://customcommercials.ai/api/v1/commercials/COMMERCIAL_ID/storyboard -H "Authorization: Bearer $CC_API_KEY"

POST /commercials/:id/approve write scope

Approve at the price you showed and start making it. Needs an owner or admin key. expectedCents is the quote's priceCents; it refuses a price that changed (409 wrong_state). Every shot is generated, then the commercial is rendered; a shot the vendor can't make after a few tries stops the commercial and gives its time back.

curl https://customcommercials.ai/api/v1/commercials/COMMERCIAL_ID/approve -H "Authorization: Bearer $CC_API_KEY" \
  -H "Content-Type: application/json" -d '{ "expectedCents": PRICE_CENTS }'

POST /commercials/:id/reshoot write scope

Reshoot one shot with an edited prompt (or line for a spokesperson). Reshoots are included up to the commercial's allowance (in GET /commercials/:id); past it the reply is { result: { queued: false, needsCredits } }, and sending that number as acceptCredits goes ahead in credits. A finished reshoot replaces its shot and sets needsRender: render again (free) to see it in the finished video.

curl https://customcommercials.ai/api/v1/commercials/COMMERCIAL_ID/reshoot -H "Authorization: Bearer $CC_API_KEY" \
  -H "Content-Type: application/json" -d '{ "shotId": "SHOT_2", "prompt": "The same office at night, warm desk lamps" }'

POST /tutorials/:id/render write scope

Render the finished video again after a reshoot, free. TUTORIAL_ID is the commercial's tutorialId: the render record keeps the path name it had before this product was its own, and it is the only place that name shows. Returns 202; poll GET /commercials/:id until tutorialStatus is ready and videoUrl has changed.

curl -X POST https://customcommercials.ai/api/v1/tutorials/TUTORIAL_ID/render -H "Authorization: Bearer $CC_API_KEY"

DELETE /commercials/:id write scope

Delete a draft, or cancel an approved commercial before any shot has started (its time goes back to the balance).

curl -X DELETE https://customcommercials.ai/api/v1/commercials/COMMERCIAL_ID -H "Authorization: Bearer $CC_API_KEY"

GET /premium/presenters read scope

The AI spokespeople (kind=presenters) or their voices (kind=voices), filtered by q, gender and language, a page at a time from offset. kind=defaults is the pair used when you don't choose. Each presenter lists the engines it can be made with. Put a presenter's id and a voice's id in the brief's presenters.

curl "https://customcommercials.ai/api/v1/premium/presenters?kind=presenters&gender=female&language=English" -H "Authorization: Bearer $CC_API_KEY"

Clients and brand

Agency accounts keep each client in its own workspace with its own brand kit (colors, logo, font and voice), its own commercials and its own usage lines. Adding and deleting clients needs a key for the whole account; a key made for one client can only change that client's brand.

POST /workspaces write scope

Add a client workspace to an agency account. With a website, we read the client's name, colors and logo from it; if that fails, the client is still made and warning says why. Your plan's client limit applies. Returns 201.

curl https://customcommercials.ai/api/v1/workspaces -H "Authorization: Bearer $CC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Bright Dental", "website": "https://brightdental.example" }'

Response

{
  "workspace": { "id": "ws_2", "name": "Bright Dental", "kind": "client", "brandColor": "#1a7f64", "logoUrl": "…", … },
  "warning": null
}

GET /workspaces/:id read scope

One workspace and its whole brand kit.

curl https://customcommercials.ai/api/v1/workspaces/ws_2 -H "Authorization: Bearer $CC_API_KEY"

Response

{
  "id": "ws_2", "name": "Bright Dental", "kind": "client", "parentId": "ws_1",
  "brandColor": "#1a7f64", "logoUrl": "https://customcommercials.ai/files/…", "website": "https://brightdental.example",
  "palette": ["#1a7f64", "#f4b400"], "voice": "", "font": { "kind": "builtin", "id": "inter", "family": "Inter" },
  "writing": { "tone": "friendly", "guidelines": "Say patients, not customers.", "glossary": [{ "term": "SaaS", "say": "sass" }] },
  "music": { "track": "builtin:calm", "volume": 0.2 }
}

PATCH /workspaces/:id write scope

Change any of name, brandColor, voice, writing (tone, guidelines and a pronunciation glossary), font and music (track and volume). Leave out what you're not changing, including inside writing and music. A glossary you send replaces the whole list. The choices come from GET /workspaces/:id/options; for a voice from the voice library, add it first with POST /workspaces/:id/voices. Returns the updated workspace.

curl -X PATCH https://customcommercials.ai/api/v1/workspaces/ws_2 -H "Authorization: Bearer $CC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "brandColor": "#1a7f64",
    "voice": "elevenlabs:VOICE_ID",
    "writing": { "tone": "friendly", "guidelines": "Say patients, not customers.", "glossary": [{ "term": "SaaS", "say": "sass" }] },
    "font": { "kind": "builtin", "id": "inter" }
  }'

GET /workspaces/:id/options read scope

The voices (including the account's cloned voices and the ones it added from the voice library), fonts, writing tones and music beds you can choose, and what the plan allows.

curl https://customcommercials.ai/api/v1/workspaces/ws_2/options -H "Authorization: Bearer $CC_API_KEY"

GET /voices/library read scope

Search the ElevenLabs voice library. Filter with q, gender, age, accent, language, locale and useCase (for example advertisement or social_media); sort with sort; page with page and pageSize while hasMore is true. Each voice's terms say what its owner allows; a voice that isn't cleared for commercial use says so in refusal. Add one with POST /workspaces/:id/voices (voiceId and ownerId); GET the same path lists the ones added, and DELETE /workspaces/:id/voices/:voiceId removes one.

curl "https://customcommercials.ai/api/v1/voices/library?accent=american&gender=female&useCase=advertisement&language=en" -H "Authorization: Bearer $CC_API_KEY"

POST /workspaces/:id/brand-import write scope

Read the name, colors and logo from a website again.

curl https://customcommercials.ai/api/v1/workspaces/ws_2/brand-import -H "Authorization: Bearer $CC_API_KEY" \
  -H "Content-Type: application/json" -d '{ "website": "https://brightdental.example" }'

DELETE /workspaces/:id write scope

Delete a client workspace with every commercial, image, clip and voiceover in it. Returns 204. This can't be undone. While the client has generations queued or running you get 409 conflict: wait for them, or cancel the queued ones. Its usage stays in GET /usage.

curl -X DELETE https://customcommercials.ai/api/v1/workspaces/ws_2 -H "Authorization: Bearer $CC_API_KEY"

Usage and credits

GET /usage read scope

Every piece of work, one line each, oldest first: AI writing, voice, rendering and every generated shot. Bill your own clients from it: each line's id never changes, so record each line once. Parameters: from and to (ISO times, default the last 30 days, up to a year apart), workspace (default every workspace the key can use), limit (up to 1,000) and after (the next cursor from the previous page).

Personal keys need an owner or admin, like the usage export in the app. A key for the whole account also sees lines from client workspaces deleted since.

curl "https://customcommercials.ai/api/v1/usage?workspace=ws_2&from=2026-10-01T00:00:00Z&to=2026-11-01T00:00:00Z" -H "Authorization: Bearer $CC_API_KEY"

Response

{
  "data": [
    { "id": "p9f2…", "workspaceId": "ws_2", "agent": "fal.ai · AI clip", "group": "Premium media",
      "kind": "video_fast", "amount": 1, "credits": 30, "chargedUsd": 3.0, "at": "…", … }
  ],
  "totals": { "lines": 1, "credits": 30, "chargedUsd": 3.0 },
  "next": null
}

GET /credits read scope

Credits pay for reshoots past the included ones and for dubbing. available is the balance (credits held for generations still running are already taken out). spendable is what the next generation in that workspace can use once the account's and the workspace's monthly limits are counted.

curl "https://customcommercials.ai/api/v1/credits?workspace=ws_2" -H "Authorization: Bearer $CC_API_KEY"

Response

{
  "balance": { "available": 1840, "held": 30, "spendable": 380, "plan": { … }, "bought": { … } },
  "account": { "spentThisMonth": 4620, "monthlyCap": 5000 },
  "workspace": { "id": "ws_2", "name": "Bright Dental", "spentThisMonth": 120, "monthlyCap": 500 }
}

Webhooks

Instead of polling, set one webhook for the account and we POST to it when something finishes. Webhooks need a key for the whole account, held by an owner or admin (or a workspace key).

PUT /webhook write scope

Making the webhook needs its url (https, on the public internet) and, optionally, which events to send; the default is all of them. Changing it later keeps whatever you leave out. The signing secret is shown only when the webhook is first made, or when you send "rotateSecret": true. Send "enabled": false to pause it. GET /webhook shows it and DELETE /webhook removes it.

curl -X PUT https://customcommercials.ai/api/v1/webhook -H "Authorization: Bearer $CC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/hooks/customcommercials", "events": ["premium.completed", "tutorial.rendered"] }'

Response

{ "webhook": { "url": "https://example.com/hooks/customcommercials", "events": ["premium.completed", "tutorial.rendered"], "enabled": true, … }, "secret": "whsec_…" }

POST /webhook/test write scope

Send a signed ping event now and see what your server answered.

curl -X POST https://customcommercials.ai/api/v1/webhook/test -H "Authorization: Bearer $CC_API_KEY"

Response

{ "ok": true, "status": 200, "error": null, "deliveryId": "…" }

Events:

  • tutorial.rendered (the finished video was rendered, or the render failed)
  • tutorial.failed (the finished video was rendered, or the render failed)
  • premium.completed (a generated shot, presenter or dub)
  • premium.failed (a generated shot, presenter or dub)

Every request is JSON with the headers X-CustomCommercials-Event, X-CustomCommercials-Delivery (the same on every retry, so you can ignore repeats) and X-CustomCommercials-Signature: sha256= and the hex HMAC-SHA256 of the raw body with your secret. Check the signature before trusting the body. data is read again each time a delivery is sent, so its links are always fresh.

{
  "id": "DELIVERY_ID",
  "type": "premium.completed",
  "createdAt": "2026-10-28T15:04:05.000Z",
  "accountId": "ws_1",
  "workspace": { "id": "ws_2", "name": "Bright Dental" },
  "data": { … }
}

Answer with any 2xx status within 10 seconds. Redirects aren't followed. Otherwise we try again 8 more times over about a day, waiting longer each time. GET /webhook/deliveries lists recent deliveries with what your server answered, and POST /webhook/deliveries/:id/retry sends one again.

Errors

Errors use the HTTP status code and the same JSON shape every time. Write your code against the code; the message is for people and may change.

{ "error": { "code": "invalid_api_key", "message": "That API key isn't valid. It may have been revoked. Create a new one in Settings." } }
Error codes, by status
WhatStatusMeaning
invalid_request400Something in the request is missing or the wrong shape. The message says what.
not_an_agency400Only agency accounts have client workspaces.
limit_reached400A limit in the settings is reached: too many drafts, too many storyboard rewrites, or a length past the longest. The message says which.
missing_api_key401No Authorization: Bearer header was sent.
invalid_api_key401The key is wrong, revoked, or its owner has left the account.
commercial_time_needed402Approving a commercial needs more commercial time than the account has. The error says how much (dueSeconds, dueCents) and links to the page where an owner can pay.
insufficient_credits402Not enough credits for a reshoot past the included ones. The error includes a link to buy more.
workspace_cap_reached402The client workspace's monthly credit limit is reached. An owner or admin can raise it.
account_cap_reached402The account's monthly credit limit is reached. An owner or admin can raise it.
email_unverified402Confirm the account's email address first.
insufficient_scope403The key doesn't have the read or write permission this call needs.
insufficient_role403The person behind this personal key isn't allowed to do this (for example, an editor approving a commercial).
account_key_required403This needs a key for the whole account, and this key only covers one client workspace.
plan_limit403Your plan's client limit is reached, or the plan doesn't include this feature. The error includes an upgrade link.
not_found404No commercial with that id that this key can see.
workspace_not_found404No workspace with that id that this key can use.
commercials_off409Commercials aren't taking new orders right now (switched off on the admin side).
wrong_state409The commercial can't do that in its current state: the price changed since your quote, it is already approved, or shots have started so it can't be cancelled. The message says which.
conflict409A client workspace can't be deleted while it has generations running.
payload_too_large413The logo is over 5 MB.
unsupported_media_type415The logo isn't a PNG, JPEG, SVG or WebP image.
website_unreadable422We couldn't read the website you sent.
logo_unreadable422We couldn't download the logo from the address you sent.
rate_limited429More than 120 requests in a minute from one key, or too many new commercials at once. Wait for the Retry-After seconds.
internal_error500Something went wrong on our side. Try again in a moment.
unavailable503A vendor this call needs isn't set up on this server right now. The message says which.

Limits

  • Each key can make 120 requests a minute. Over that, you get 429 rate_limited with a Retry-After header in seconds. New commercials are also limited to a few an hour per key.
  • Logos can be up to 5 MB.
  • The API answers cross-origin requests (CORS) from any site, because keys travel in a header, never in cookies. Even so, don't put a key in a public web page.

Commercials open soon

We are not taking new orders yet. Email us and we will tell you when they open, and what the price is before anyone pays.