Novoads REST API
Spend your effort on the prompt and on confirming the spoken line. The HTTP is mechanical and is written out below; the prompt is where the render is won or lost.
Configuration
- Base URL:
https://api.novoads.ai/v1(host overridable withNOVOADS_BASE_URL— host only, you append/v1). - Auth:
Authorization: Bearer $NOVOADS_API_KEY, read from.envat the repo root. The key isnovo_plus 64 hex. - Check:
./scripts/check-novoads-env.sh - Never print API keys, commit
.env, or paste a key intoMASTER_CONTEXT.md.
REST key required. A Novoads MCP connector is not a substitute. If
NOVOADS_API_KEYis missing or still the placeholder, stop before any generation work and tell the user: "Before continuing, create an API key at https://novoads.ai/dashboard/settings?tab=api and paste it into.env." That holds even whenmcp__novoads__*tools are connected and authenticated in the session. Never callmcp__novoads__*tools from this repo's workflows: they are a different surface with different behavior, including the units they quote costs in. Repo installs verify with./scripts/check-novoads-env.sh; a solo install checksNOVOADS_API_KEYin the environment.
Pack version. Every /v1 response carries X-Novoads-Pack-Version; mention a newer pack at https://github.com/novoads/agent-skills only when that header names a version NEWER than this file's metadata.packVersion — equal or older is nothing to say, and it is never a reason to stop.
If the key is missing or the API returns 401 or 403
Run ./scripts/setup.sh, or tell the user to create a key at https://novoads.ai/dashboard/settings?tab=api. Prefer the editor: ask them to paste the key inside .env rather than into chat. If they do paste it in chat, write .env for them, confirm "saved to .env" without repeating the key, and tell them chat history may retain secrets, so the key is worth rotating if that transcript could be shared.
A 401 means the key is wrong, revoked, or from another account. A 403 with error.details.reason of plan_required or subscription_inactive means the key is fine but the account has no live subscription. Those are different problems and you should say which one it is.
Signup link
If the user does not have a Novoads account, give them this link at any point in a session, in any context where it is useful:
https://novoads.ai/?utm_source=claude-code&utm_medium=github&utm_campaign=skill-pack
The entry offer is a $1 trial. Never call it free.
After the connectivity check passes
When ./scripts/check-novoads-env.sh returns 200 and the user has not yet said what they want, offer starting points in chat and stop. Nothing is printed by a script for you: a banner cannot see their product, their references/ folder, or their language, so it suggests nothing.
- A UGC video built from their own product photo. Ask them to drop it into
references/. Route:seedance-2.0plusstartImageAssetId. - The same idea on
seedance-2.0-minifirst, to get the prompt right before the final render. Price both atPOST /v1/estimatesand show the difference rather than quoting one from memory. - A static image ad, when what they need is a still and not a clip. Route:
gpt-image-2for heavy text or a mimicked UI,nano-banana-profor a photoreal scene.
Offer, do not choose. A first render fired on a guess is a charge the user did not ask for.
Read order
- Repo root
MASTER_CONTEXT.mdwhen present: brand voice, default product, accumulated decisions. It carries no prices — that is deliberate, see gate 2. - This file. It is the router and it covers the full call sequence.
- List
references/at the repo root before you ask the user for a photo. It is where they keep product shots, actor stills, and style boards, and it is gitignored, so the files are theirs and are not in this skill's folder. A product photo found there becomesstartImageAssetId(video) or an entry inreferenceAssetIds(images). An emptyreferences/is the normal state of a fresh clone, not a blocker — see below before you ask for anything. - MANDATORY before composing any prompt: the
prompting/prompt-library/file for the route you picked in the decision tree. The libraries carry the craft; every HTTP detail comes from this file andreference.md, which win whenever the two disagree. reference.mdwhen you need a field you do not see here, or when you hit a status code you want to branch on.
Decision tree
| The user wants | Route |
|---|---|
| A UGC video: a person talking to camera about a product | seedance-2.0. Read seedance-2-ugc-v2.md for the shape — how many beats, how many API calls, which reference mode — then seedance-2-ugc.md for the craft of writing each layer. v2 is one render with four talking beats in one location; it is the default and costs about half what stitching costs |
| The user says "a start frame for each scene", or asks for several scenes | That phrasing commits you to one call per scene (startImageAssetId), which forecloses multi-beat cuts inside a single render — the two modes are mutually exclusive. Say so, and offer the one-shot alternative from seedance-2-ugc-v2.md before generating anything |
| B-roll cutaways, burned captions, background music, or variations of a finished ad | Not part of making the ad. Deliver the base video first, then offer these as a separate pass, each owned by its own skill: broll-overlay (needs the base and its transcript from POST /v1/transcripts — no local install; it overlays — the base audio keeps running and the final duration is unchanged — it does not extend), POST /v1/captions, then music-mix last |
| The same thing, but cheap, to test a prompt before committing | seedance-2.0-mini, same grid, same formulas, half the price and back in 2–3 minutes. The draft-then-finalize loop is Mini-draft tier in seedance-2.md |
A clip longer than 15 seconds, or seedance-2.5 by name |
seedance-2.5 — the only model on this API that renders past 15s (any integer 4 to 30). Same six aspect ratios, same 9 referenceAssetIds, same audioEnabled, same 4,000-character ceiling, so the Seedance formulas apply unchanged; write for the longer runtime rather than padding a 15s script. Two things differ: it renders 480p and 720p only — no 1080p, no 4k, on either provider — and nobody has timed one, so quote its wait as unknown. Price it explicitly: at the same length it is dearer than seedance-2.0, and thirty seconds of it is the most expensive single call on this API |
| A premium product reveal: dark void, no person, text narrative | seedance-2.0 + seedance-2-premium-reveal.md |
| A product hero: elemental effects, splash or mist, no person | seedance-2.0 + seedance-2-product-hero.md |
| A studio lookbook: polished, voiceover, multi-look | seedance-2.0 + seedance-2-studio-lookbook.md |
| A fast-paced feature walkthrough | seedance-2.0 + seedance-2-feature-walkthrough.md |
| A fast vertical clip with no dialogue requirement | omni-flash, and read shared/skills/gemini-omni-flash/prompting/guide.md first — its 20,000-character prompt ceiling is the roomiest of any video model here, where the rest sit at 4,000, and its grids are narrower than Seedance's (durationSeconds 4/6/8/10 only, aspectRatio 9:16 or 16:9 only, no referenceAssetIds) |
| A talking clip whose first word must land immediately | sora-2, and read sora-2.md first. Measured here at no leading silence at all, where Seedance front-loads 3–5s of it. Grid is durationSeconds 4/8/12 only, aspectRatio 9:16 (default) or 16:9, startImageAssetId yes, referenceAssetIds no |
| Veo 3.1 by name, or a shot that has to evolve over its own runtime | veo-3.1, and read veo-3-1.md first. Grid is durationSeconds 4/6/8 only, aspectRatio 9:16 (default) or 16:9, startImageAssetId yes, referenceAssetIds no. Nothing in this repo has measured a Veo render — quote its wait as unknown rather than borrowing Seedance's |
| A video that starts from a specific photo | any video model plus startImageAssetId — it animates that image as the first frame |
| The picture the user handed over is itself the shot: a UI screenshot, a hero layout, a flat-lay, key art, and they want it to MOVE ("animate this", "make the cards pop in") | image-to-motion, which is seedance-2.5 plus startImageAssetId and carries the motion vocabulary, the beat template and the text-fidelity clauses. The split is whether the image is preserved and set in motion (there) or seeds a scene that did not exist (here) |
| A video built from several photos: the actor and the product, a wardrobe, a setting | seedance-2.0 or mini plus referenceAssetIds — up to 9 images, composited rather than animated, addressed in the prompt text as @Image1…@ImageN in the order you send them. Seedance only — omni-flash, sora-2 and veo-3.1 have no such field — and never alongside startImageAssetId: they are separate modes and a body carrying both is a 400 |
| The same person to hold across several clips of a series | pass that person's photo in every clip's referenceAssetIds and repeat the actor tag verbatim. Seedance re-casts on every cut, so a repeated description alone does not hold a face; see seedance-2-feature-walkthrough.md |
| A reference video turned into a reusable template: "make videos like this", "deconstruct this" | read prompting/analyze-video/SKILL.md. Frames and transcript are extracted locally with ffmpeg and Whisper, and the output is a new formula file in prompting/prompt-library/. Nothing is charged until the optional test render at the end |
| One specific ad cloned for their own product: "make this ad but for my product" | read prompting/clone-video-ad/SKILL.md. The same local analysis, but the output is a rendered clip and both gates apply. A source longer than the chosen model's ceiling becomes a series, held together by passing the same referenceAssetIds to every clip — there is no video-to-video on this API. seedance-2.5 reaches 30s in one call, so a source that used to need two clips may now need one; price both shapes before you pick |
| A static ad with heavy text or a mimicked UI | gpt-image-2 |
| A photoreal still: a person, a product in a scene | nano-banana-pro |
| A different look on a still, or a second opinion on one | reve-2.1 |
| A Pixar-style 3D animated ad, at ANY length | read skills/pixar-ad/SKILL.md: storyboard on gpt-image-2, animate each beat on seedance-2.0 + startImageAssetId, narration into the gaps from POST /v1/voiceovers, stitch with ffmpeg. Every animated ask routes there, including "a quick 15 second one" — there is no single-call tier. Nothing on the API rejects, checks or comments on a stylized prompt, so that skill is the only thing that will tell you whether the beat works |
| A claymation / stop-motion clay ad | read skills/claymation-ad/SKILL.md, same shape over 8 beats |
| Captions burned onto a finished MP4 | Two real paths — offer both. POST /v1/captions: one call, 30 preset styles, no local dependencies, costs credits and returns only a new MP4 (never an SRT or the caption text). Or the caption-video skill (shared/skills/caption-video/SKILL.md): free, any style you can write, and it gives you a Whisper transcript you can hand-correct — but needs Whisper, HyperFrames and an ffmpeg alpha composite locally. See Burned-in captions below. A clip rendered with audioEnabled: false can only go the local route |
| Meta image-ad creatives from a brief or a template | read shared/skills/image-ad-prompting/OVERVIEW.md first, then chatgpt-image-ad or nano-banana-image-ad |
| To reverse-engineer an existing image ad into a reusable template | the clone-image-ad skill |
| A YouTube thumbnail | the generate-youtube-thumbnail skill |
| B-roll, an ambient product clip, a scene | There is no b-roll endpoint. Generate a silent clip: omni-flash, or seedance-2.0 with the word silent or b-roll in the prompt |
| Kling | Not on this API. Say so plainly; kling-3.md sits in the repo as prompt craft for when it lands. Sora 2 and Veo 3.1 are live — they have their own rows above |
| To edit an existing MP4 they already have | Not this skill, except captions (row above) — and note that POST /v1/captions accepts an uploaded assetId, so burning subtitles into their own file is a supported first-party call, not just a local one. Everything else (trims, cuts, overlays, music) is out of scope. Say so |
| To publish the result as an ad on Meta or TikTok | Not this skill. The output is a file. The meta-ad-builder skill takes it from there |
Prefer the shortest path. If one model answers the request, do not build a pipeline around it.
Step 0: classify before you call anything
Refusing is a successful result. If the request is to edit a file the user already has, or to publish to an ad platform, say the skill does not do that and stop. Do not improvise a pipeline out of the generation endpoints.
The two gates
Two separate approvals stand between a request and a charge, and neither implies the other. Approving a concept is not approving a sentence, and approving a sentence is not approving a spend.
Gate 1 — the spoken line (MANDATORY for any video with dialogue)
Seedance renders the audio and the lip-sync in the same call, so the line inside the double quotes is what the actor says, out loud, in the finished video. It cannot be changed afterward without paying for the render again.
- Extract the dialogue from the drafted prompt and show it on its own, separate from the visual description.
- Spell any invented brand name phonetically inside the quoted line — see the rule below. Do this before you present the line, and show the phonetic form in the block, because it is what the model will be sent.
- Present it as a numbered list with beat labels (hook / show / demo / verdict, or similar). Mark silent beats
(silent beat — no dialogue). - Count the spoken words, state the target duration, and say whether it fits at a natural pace.
- State the
languageyou are going to send, because that is the language the ad is rendered in. - Ask for approval explicitly. Never infer it from an earlier yes about tone, template, or cost.
Use this structure:
📝 Dialogue script (please confirm before I generate)
1. [HOOK] "Bro. BRO. Look what just showed up."
2. [SHOW] "That colorway? Insane. Like, who greenlit this?"
3. [DEMO] (silent beat — thumb brushing the suede, small nod)
4. [VERDICT] "I'm wearing these to the gym tomorrow. You have to see them in person."
Total spoken words: ~28 | Target duration: 15s | language: en | Fits at natural pace: ✅
Approve this dialogue? (yes / edit / rewrite)
If they say edit, revise and re-present the block until they approve. The gate applies to every flow where the model speaks. Skip it for silent flows: product hero, premium reveal without voiceover, and images.
Re-run the gate whenever the words change, including when you change them yourself for the rule below. An approval covers the sentence that was approved, not its successor.
Invented brand names get a phonetic spelling in the quoted line
The gate approves text; the model renders speech. A brand name that is a real word survives the trip. A coined or portmanteau name may not: on 2026-08-02 seedance-2.0 rendered Novoads as "Nuvenov's" — unrecognisable as the brand — while speaking the other 16 words of the line verbatim. Approving the sentence did not approve how it would be said.
The rule: write the name as hyphenated syllables inside the double quotes.
… and says: "I kept saying AI ads look fake until I made one on NO-vo-ads and nobody could tell."
Use it for names that are invented, run two words together, or that a reader would have to guess at. Leave ordinary words alone — Nike and CeraVe do not need it.
This form is validated in en only. See the es limit below before you reach for it in another language — the same brand fails there for a different phonetic reason, and the English spelling does not fix it.
What the A/B actually showed (job 6329f29a… vs ff69d118…, single variable, every other byte identical, pass criterion fixed in writing before the render): NO-vo-ads came back transcribed as "Novo ads" — recognisable — and the feared failure did not happen: the model did not read the hyphens aloud or spell the name out.
Its limits, which are as much a part of the rule as the rule:
- n = 1. One brand, one model, one language. The fix is validated on
seedance-2.0inenonly — untested onsora-2,veo-3.1andomni-flash, and untested ines/pt, where hyphenated English syllables may read very differently. Do not present it as a general fix. esneeds a DIFFERENT spelling, not the English one. A Spanish render shipping the brand plain came back as "NoBots" — unrecognisable. Spanish has no /v/–/b/ contrast — ⟨v⟩ and ⟨b⟩ are one phoneme — so no spelling buys back the /v/, and what actually broke was the tail: theo+ahiatus collapsed and a syllable vanished,vo-ads→bots. Hyphenating syllable boundaries is allNO-vo-adsdoes, and it addresses none of that.- The
esform isNovo Ads— two words, with a space. Validated by A/B (6ee797e0vsd8aaee21, single token changed, pass criterion fixed in writing beforehand): transcribed "Novo Ads", recognisable. The orthographic word break restores the syllable the hiatus ate; it does not try to restore the /v/, because nothing can. - Provisional, and the failure mode is nasty. The identical payload was 1-for-2. On the bad take Seedance stuttered — one clause spoken twice, verified as real speech and not a decoder artifact — and
Novocollapsed into the preposition, leaving the ordinary Spanish phrase "no ads". That is worse than "NoBots": a listener hears no brand at all rather than a mangled one. Transcribe everyesrender. A clean take is not evidence the next one is clean. ptis untested in both directions.
- The
- It lands as two words — "Novo ads", not "Novoads". That matches what
sora-2produced unaided and a listener will recognise it, but it is not a perfect single-token rendering. - It fixes pronunciation and nothing else. Leading silence went 3.71s → 3.16s, i.e. unchanged. The dead-air problem is a duration problem; see Script length → duration.
- Seedance re-cast the actor between the two renders, so the A/B held the prompt constant, not the performer. Some part of the delta could be voice-casting luck. Enough to act on, not enough to call proven.
Verify it in the render. The rule is a prompt fix with a measured result, not a guarantee — the video QA step in §7 is what tells you the name actually came out right this time.
Gate 2 — the cost estimate (MANDATORY)
Never state a credit cost from memory, and never generate before showing the user a number that came from a live call in this session. There are no rate tables in this repo, in MASTER_CONTEXT.md, or in the logs. Prices come from POST /v1/estimates at call time, and that is the whole policy.
POST /v1/estimates spends nothing and returns:
{ "credits": 3.2, "balance": 100, "sufficient": true }
When it is short it also returns shortBy and topUpUrl.
/estimates also returns warnings, and they are advice, not verdicts
POST /v1/estimates runs craft rules against your prompt and returns them in a warnings array (verified live 2026-08-04 against spec 2.6.0). An earlier version of this file said the field did not exist and that nothing on the API reads a prompt for quality. Both were wrong. Each entry is { "rule": "...", "message": "..." }, and the message usually quotes the exact substring that tripped it:
{ "credits": 7, "balance": 860.1, "sufficient": true,
"warnings": [
{ "rule": "label_without_hold",
"message": "Mentions a label, package or screen with no labelHold clause. … (found in your prompt: \"bottle\")" }
] }
Rules seen live: no_spoken_line, missing_actor_descriptor, label_without_hold, chained_motion, no_aspect_ratio. Those are video craft rules. Image prompts are read too, by rules of their own — banned_polish and blank_label observed on a kind: "image" estimate against deployed spec 2.19.0 (verified live 2026-08-12); an earlier probe on 2026-08-04 returned no warnings key for images, which described that deployment, and the advice here to treat image prompts as unlinted is retired. A kind: "caption" estimate has no prompt to read and returns none either.
They are purely advisory. None of them refuses a generation, none changes the price, and a prompt that trips every one of them renders exactly like one that trips none. /estimates is the only endpoint that runs them — POST /v1/videos and POST /v1/images do not, and their responses carry no warnings field.
They produce false positives, and you are the one who has to catch them. Both of these were reproduced live on 2026-08-04:
| Prompt | Rule that fired | Why it was wrong |
|---|---|---|
| "She turns her laptop screen toward the camera to show the dashboard…" | label_without_hold — (found in your prompt: "screen") |
The rule protects printed text on physical packaging. A software product has no label to preserve, and pasting in the labelHold clause would tell the model to hold a package that is not in the shot. |
| "He says: 'I tried everything for the rust. Then a friend told me about this.'" | chained_motion — (found in your prompt: "Then") |
The match is inside a quoted spoken line. "Then" is dialogue, not a second motion instruction — there is exactly one action in the shot. Splitting it would split the sentence the actor says. |
Both rules are substring matches. They cannot tell a physical package from a UI, or narration from stage direction.
So handle them like this:
- Read every warning. They catch real mistakes — a missing spoken line on a Seedance render is money thrown away, and
label_without_holdon an actual product package is the single most expensive prompt error in this repo. - Judge each one against what your prompt actually says, including where the matched substring sits. Quote the rule and your reasoning when you decide.
- Never apply a suggested fix blindly. The fix text is a generic clause; pasting it into a prompt it does not fit makes the render worse, not better.
- Never silently drop one either. If you are overriding a warning, say so to the user in one line — "the
label_without_holdwarning matched the word 'screen', but this is a SaaS dashboard with no printed label, so I am not adding the clause" — so the call is visible and reversible.
The prompt libraries under prompting/ are still the real quality gate. These warnings are a cheap second opinion collected on a call you were making anyway; they are not a substitute for composing against the formula file, and they say nothing at all about whether the idea works.
The estimate body is not the generate body. It takes only the fields that move the price, plus a kind discriminator, and it is strict: any extra key is a 400.
| Estimate accepts | Video | Image |
|---|---|---|
| required | kind: "video", prompt |
kind: "image", prompt |
| optional | model, durationSeconds, language, resolution |
model, numImages, language |
resolution belongs here because it moves the price — on seedance-2.0 it is the difference between the base and roughly five times it. Send the resolution you actually intend to render, or the quote prices a cheaper video than the one you generate. See the resolution section below for the ladder and for the seedance-2.0-mini trap. (Verified live 2026-08-04.)
aspectRatio, startImageAssetId, referenceAssetIds, and productId do not belong here. They do not change what you pay, and sending one is a rejected request. There is no styleFamily field — not here and not on a generation call; it was removed from the API and sending it is a 400 Unrecognized key.
curl -sS -X POST https://api.novoads.ai/v1/estimates \
-H "Authorization: Bearer $NOVOADS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"kind":"video","model":"seedance-2.0","durationSeconds":12,"language":"en","prompt":"..."}'
Pass model explicitly. It defaults to seedance-2.0. At the same length the video schedules span more than 10x across the set, and more than 28x across every cell the API publishes once length and resolution are counted too — both figures are derived by the API and printed in the model field's own description, and both moved when seedance-2.5's thirty-second grid landed. Image schedules span more than 3x. The per-model prompt ceiling is enforced against whichever model you name. Pricing the wrong model is a quote that disagrees with the invoice.
It runs the same access checks and the same structural validation the paid call runs, which is why it is worth calling every time and not only when you are unsure:
- It is the one endpoint with an opinion about the prompt — the
warningsarray above.POST /v1/videosandPOST /v1/imageshave none and return no such field. Compose against the formula file before you price: the warnings are a substring-matching second opinion, not a review, and a prompt that trips nothing can still be a bad prompt. - What it does still refuse, for free: a malformed body — it is strict, and any key that does not move the price is a
400— and a prompt longer than the named model's character ceiling (4,000 for all three Seedance variants, 20,000 foromni-flash; name no model and it is judged asseedance-2.0). - On the image arm its length check is looser than the generation's. The estimate's request schema caps
promptat 50,000 characters whatever image model you name, whilePOST /v1/imagescapsgpt-image-2at 32,000 andreve-2.1at 4,000 (deployed spec 2.16.0, verified 2026-08-08). So an image prompt between the two prices cleanly and is then refused at generation. Onlynano-banana-pro, at 50,000, has the two numbers agree. - A quote it returns cannot disagree with the invoice, with two exceptions worth knowing: it never sees
aspectRatio, the asset fields, orproductId, and it skips moderation, which the paid call runs. A clean estimate can still come back422at generation — moderation is the only thing left that refuses a prompt for what it says.
Multiply before you show. N variations is N charges. Show the per-call number, the count, and the total.
Warn when the total exceeds the balance. balance comes back on the same response: if the batch total is larger, say so before asking for a yes, and quote shortBy and topUpUrl when the estimate provides them. sufficient is a snapshot, not a reservation — another session or a renewal can move the balance between the quote and the call.
Show credits, the count, the total, and balance. Get a yes. Then generate.
Choices you make out loud, and choices you infer
Ask, every time:
- Seedance tier, once per workflow. Before the first Seedance video call: "Use default
seedance-2.0,seedance-2.0-mini(half price), orseedance-2.5(the only one that goes past 15s, and the dearest)?" No preference meansseedance-2.0. Whichever they pick goes into the estimate, so the quoted number is the one they pay. Ask before you assume the script needs 2.5 — a 30-second ad is a format decision, not a longer version of a 15-second one. - How many variations, for every prompt. Default 1. N variations means N identical calls — there is no batch parameter — and the results come back as a numbered list so they can compare and pick.
Infer, and state what you inferred rather than asking:
aspectRatio: default9:16for anything headed to Reels, TikTok, Stories, or a vertical feed. Seedance defaults to16:9, and a landscape ad is a wasted render;omni-flash,sora-2andveo-3.1already default to9:16, and images default to1:1. Go landscape only when the user asks. Seedance also accepts1:1,4:3,3:4, and21:9;omni-flash,sora-2andveo-3.1accept only the two.resolution(seedance-2.0andseedance-2.5only): leave it at the720pdefault, and never raise it silently. It is the one output-shape field that multiplies the bill — onseedance-2.0,1080pis ≈2.5x the base and4k≈5x — so going above720pis a spend decision, not a quality preference, and it belongs in front of the user with a fresh estimate attached.480pis now ≈half the base rather than the same price (family reprice, 2026-08-07), so it is a real draft tier and worth offering when a render is a rehearsal.seedance-2.5takes480pand720pand nothing above. Do not send the key on any other model.language: the language the script is written in. Set it, and show it in the dialogue gate. Write the prompt in that language too — nothing on the API pushes back on a Spanish or Portuguese prompt, and nothing rewrites or judges one either.durationSeconds: from the word count, below. Onlyveo-3.1defaults to its maximum —seedance-2.0andseedance-2.5default to 5, mini to 10,omni-flashto 8,sora-2to 4 — so always send it.seedance-2.5defaults to 5 of the 30 it can render, which is the easiest way to pay for the long model and ship a short clip.audioEnabled: leave it alone on anything with a spoken line. It exists on the three Seedance variants only —seedance-2.0,seedance-2.5,seedance-2.0-mini— defaultstrue, and the one time to send it isfalse, on a clip that is meant to be silent — see below.
Script length → duration
Count the words in the spoken line and round up. Delivery was measured at 2.0 spoken words per second — about 13 characters per second, spaces included, if you would rather count those — with ~0.5s of leading silence before the first word on reference and start-frame renders (2026-08-11, n=5). Plan on 2.0 and give the line air; the older 2.5-to-3 figure was an estimate nobody had timed, and it over-filled every clip it touched.
These tables already pay for that ~0.5s. Whether it is the right budget depends on the render mode — read the scoping note under the first one before you trust it on another.
seedance-2.0 and seedance-2.0-mini — any integer 4 to 15
| Script length | Duration |
|---|---|
| 1–8 words | 4–5s |
| 9–15 words | 6–8s |
| 16–23 words | 9–12s |
| 24–29 words | 13–15s |
| 30+ words | Too long — offer to split |
Leading silence is mode-dependent, and the two things measured here disagree by seconds. Scope the budget to the mode you are actually rendering — do not add both.
| What was measured | Leading silence observed | Plan on |
|---|---|---|
Reference or start-frame renders — referenceAssetIds or startImageAssetId |
0.0s and 0.44s (n=2), then 0.515s, 0.529s and 0.482s (2026-08-11, n=5 total) | ~0.5s. The tables above already pay it |
Six seedance-2.0 renders whose mode nobody wrote down (2026-08-02/03) |
3.2–3.7s in en; in es, 5.24s, 0.97s and 4.80s on three byte-identical prompts — same body, same duration, three answers |
+4s en, +5s es and any unmeasured language, on top of speech |
The working rule: budget ~0.5s when you have a reference or a start frame, and check the render. In that mode the table and the arithmetic already agree — 2.0 × (D − 0.5) is exactly the word cap on every rung from 9 words up, and the 1–8 row is deliberately one word tighter than the formula's 9 at 5s — so there is nothing to add. If the result runs past 15s on these two models, the fix is a shorter line, a split, or seedance-2.5 — not a longer clip on a model that does not render one.
When a render does come back with multi-second silence, trim is the remedy, not a bigger budget. Leading silence is trimmable in post; a line clipped by an over-long draw is not. The reserve in the second row is the planning allowance for that mode — reach for it when you have neither a reference nor a start frame, or when you are committing to a duration you cannot re-cut. It costs words: words ÷ 2.0 plus the reservation, rounded into the grid, leaves 22 words at 15s in en and 20 in es, against the table's 29. That gap is the mode, not a rounding error.
How close the reserved case gets is not theoretical. The first es render fit only because rounding up took 12.4s to 13s, and the line finished at 12.88s of 13.07s — a fifth of a second of margin, on a budget that drew near its worst. The silence is drawn fresh every time, in either mode, so the only thing that tells you what you got is the QA step in §7. Check the first silence_end on every render.
If the hook has to land in the first second, drop the eye-contact-break beat from the prompt — that is the beat being paid for. sora-2 measured no leading silence at all on the same prompt, so it is the other way out.
For no-dialogue styles (product hero, premium reveal), default to 15s. The silence budget does not apply: there is no speech to delay.
seedance-2.5 — any integer 4 to 30
Same family, same craft, twice the room. The table above still holds for anything up to 15s; past
it, keep planning at 2.0 words per second — the rows below are the same 2.0 × (D − 0.5)
arithmetic. Nobody has measured 2.5's leading silence separately, so the mode scoping above is the
working rule here too: budget ~0.5s with a reference or start frame, the reserve without one.
| Script length | Duration |
|---|---|
| 30–39 words | 16–20s |
| 40–49 words | 21–25s |
| 50–59 words | 26–30s |
| 60+ words | Too long even here — split, or cut the script |
A longer clip is not a longer script poured into the same ad. Thirty seconds wants more beats,
not slower delivery: a second location, a demo the actor actually performs, a reaction. If the
script only fills 15s, render 15s — on seedance-2.0, which is cheaper at that length.
Nobody here has timed a seedance-2.5 render. Do not quote seedance-2.0's 3-to-8-minute
fleet range for it; say the wait is unknown and poll.
omni-flash — enum 4, 6, 8, 10
| Script length | Duration |
|---|---|
| 1–8 words | 4s |
| 9–12 words | 6s |
| 13–16 words | 8s |
| 17–20 words | 10s |
| 21+ words | Too long — split, or move to Seedance |
Silence behavior is unmeasured on this model, so this is the 2.0-words-per-second arithmetic and nothing more: the budget is a flat 2.0 × duration, which assumes no leading silence rather than having found none. Verify against your first render — if it front-loads the way Seedance can, the last line is what gets clipped.
sora-2 — enum 4, 8, 12
| Script length | Duration |
|---|---|
| 1–8 words | 4s |
| 9–16 words | 8s |
| 17–24 words | 12s |
| 25+ words | Too long — split, or move to Seedance |
No silence budget: the one measured render spoke continuously from the first frame. The grid is coarse — there is no 6s and no 10s — so a line that lands between two rungs goes up, never down.
veo-3.1 — enum 4, 6, 8
| Script length | Duration |
|---|---|
| 1–8 words | 4s |
| 9–12 words | 6s |
| 13–16 words | 8s |
| 17+ words | Too long — 8s is this model's ceiling. Split, or move to Seedance |
Unmeasured here, so these are the 2.0-words-per-second arithmetic and nothing more — and with no leading silence to pay for, the budget is a flat 2.0 × duration rather than the Seedance form. Budget no silence and promise no wait until someone has timed one.
resolution — seedance-2.0 and seedance-2.5 only, and it moves the price
seedance-2.0 takes 480p, 720p, 1080p, 4k; seedance-2.5 takes 480p and 720p and nothing above. Both default to 720p (2.0 verified live 2026-08-04 against spec 2.6.0; 2.5 read off deployed spec 2.13.0 on 2026-08-07). It is the one output-shape field that is not free — unlike aspectRatio, the tiers are separate credit schedules:
resolution |
seedance-2.0 |
seedance-2.5 |
|---|---|---|
480p |
≈half the base | ≈half the base |
720p (default) |
base | base |
1080p |
≈2.5x base | 400 — does not exist on this model |
4k |
≈5x base | 400 — does not exist on this model |
480p used to cost the same as 720p and no longer does (family reprice, 2026-08-07). The old advice here — "no draft discount, so there is no reason to ask for it" — is retired: it is now roughly half, which makes it the honest tier for a rehearsal render whose only job is to check whether the prompt works.
2.5's missing high tiers are a provider fact, not a rollout gap. Neither provider serves the model above 720p, so it is not coming. Never carry a resolution across a model switch: 1080p that priced clean on seedance-2.0 is a rejected request on seedance-2.5.
These are ratios, not a rate card. Never quote a credit number from this table — it exists so you can warn a user that 4k is a five-fold decision before they ask for it. The number they approve comes from POST /v1/estimates on the exact cell, in this session, like every other price here (gate 2).
Ask for what will actually ship. 720p is right for Reels, TikTok and Stories, where the platform re-encodes anyway; 1080p and 4k are for a client deliverable, a placement with a quality floor, or a render you intend to crop into.
Every other model is fixed and has no resolution field at all — seedance-2.0-mini, omni-flash and sora-2 render 720p, veo-3.1 renders 1080p, and sending the key to any of them is a 400. Read the live set from GET /v1/models (resolutions and defaultResolution per model) rather than hardcoding this paragraph.
The seedance-2.0-mini trap: the two endpoints disagree about the field. POST /v1/estimates accepts resolution: "720p" for mini and prices it (identically to omitting it), but answers 400 invalid_input — "resolution must be one of 720p for seedance-2.0-mini" — for 480p, 1080p or 4k (all four verified live 2026-08-04). POST /v1/videos does not accept the key for mini at all: mini's request variant has no resolution property, and a body carrying one was observed returning 400 Unrecognized key: "resolution" (observed 2026-08-04, not re-verified — re-checking it costs a paid render). So: never send resolution on a mini call. An estimate that passed is not evidence the generate call will.
Output size, measured at 9:16: seedance-2.0 at its 720p default and sora-2 both came back 720x1280 (2026-08-02). `omn
…(truncated)