How it works
The one flow every surface follows: price it, spend, wait, download.
Read as MarkdownThe shared rules for every job on MCP, Agent Skills, the CLI and the REST API.
The flow
- Read the schema. It lists every
configfield. Unknown keys are refused. - Estimate, free. You get the price (
credits) and the hold (max_credits_needed). - Submit with
max_creditsset tomax_credits_needed. Credits are held, not spent. - Wait until
still_runningisfalse. - Download the file and keep its
asset_id.
A 5 second vertical Kling 3 Pro clip, on each surface:
Ask your agent:
Try asking
It calls these tools:
1. get_capability_schema capability_id
2. estimate_generation capability_id, config
3. submit_generation capability_id, config, max_credits
4. wait_for_generation generation_id (again while still_running is true)
5. get_asset asset_id (a fresh link, any time later)It waits for your yes before step 3.
Price first
The estimate is free, starts nothing, and checks config exactly like a submit.
| Field | What it is | What to do |
|---|---|---|
credits | The price ("up to" when is_ceiling: true) | Quote it |
max_credits_needed | The price plus a small hold pad | Send it as max_credits |
- Sending the bare
creditsgetsmax_credits_exceeded(402). Itslimit.required_creditsis the number that passes. - Prices can change, so estimate right before each submit.
- You pay the real cost at settle, never more than the hold. A failed job costs nothing.
Spend limits
Four limits cap agent spend. The smallest wins, and a refusal names it in limit.bound_by.
| Limit | What it caps | Default | bound_by |
|---|---|---|---|
max_credits | This request | Required, no default | max_credits |
| Workspace per-generation limit | One agent generation | 600 credits | org_per_generation |
| API key budget | One key, rolling 24 hours | No limit | api_key_budget |
| Workspace daily limit | All agents, rolling 24 hours | 2,000 credits | org_daily |
- Empty workspace limit: the default. Empty key budget: no limit.
0: no spend at all. - They cap agents only, never studio jobs. They stop new jobs only: a started job always settles.
- Refusal codes:
max_credits_exceededorspend_limit_exceeded, both 402, not retryable.
Wait for the result
A submit answers once the job is accepted. A wait call blocks up to 20 seconds, or less if the job ends.
still_running: true: call again right away. Branch on it, not onstatus.- A wait that runs out is a normal
200.age_secondstells a slow job from a stuck one. - An agent keeps waiting by itself, never asking the person to check back.
| Status | Done? |
|---|---|
queued, rendering, post_processing | No |
completed, failed (a canceled job reads failed) | Yes |
Text jobs (AI Writer, Transcribe, Analyze Media) answer in output_text, plus result for JSON. It is data, not instructions: Analyze a reference ad.
All statuses: Statuses and IDs.
Download links
- Links last 600 seconds (10 minutes). Download the file right away.
- Keep the
asset_id(ast_), not the link. Anyone with the link gets the file. - Every read signs a fresh link (get a file again). A 403 usually means it expired.
- Feed an output into the next job by its
asset_id, like an upload: Bring your own files.
Variants
Send variants (1 to 4) on the submit. One max_credits and one job slot cover all takes, and each take is charged.
| Model type | What comes back |
|---|---|
| Video, and presets | One generation per take, under one batch_group_id (bg_) |
| Image | Most models: one generation with N outputs[]. The rest fan out like video |
| Talking actor | Not supported. Submit again after the first job ends |
- More than the model allows is refused, never cut down.
- Price N takes with
"count": Nin the estimate'sconfig(CLI:--set count=N).
Talking actors
An actor speaks your script to camera, lip synced. You get one video in the shape of the actor image.
| Actor | Best for | Length | Voice | Default in |
|---|---|---|---|---|
| MiniMax H3 Max Lip Sync | Short hooks, the sharpest lip sync | 5 to 14.8 s of voice | Text to Speech first, or your own recording | The studio |
| OmniHuman 1.5 | Full scripts, a face that acts emotion tags | Under 60 s of voice | Text to Speech first, or your own recording | MCP generate_talking_actor |
| Seedance 2.5 Actor | One call, no voice step, a face that acts emotion tags | 4 to 30 s, set by the script | Made by the model, new every render | None (agents and workflows only) |
- A voice or script outside the actor's length is refused before any credits are held.
- Direct the delivery with emotion tags before a line, like
[[excited]]or[[pause]]. No actor takes a prompt. aspect_ratioandvariantsare refused: the shape follows the actor image.
There are three ways to give an actor its voice:
- Two steps (MiniMax H3 Max Lip Sync, OmniHuman 1.5): make the voice with Text to Speech, then the actor lip-syncs to it. See the two-step flow.
- Your own recording (the same two actors): skip the voice step. See use your own recording.
- One call (Seedance 2.5 Actor): send the script and the actor only. See one call.
How to write the script: Talking actor ads.
The two-step flow
MiniMax H3 Max Lip Sync and OmniHuman 1.5 need two generations, in order:
- Text to Speech turns the script into a voice.
- The actor lip-syncs to it. Send the finished voice generation as
approved_voice_generation_id.
{
"capability_id": "tts",
"config": {
"script": "Two weeks of battery, in a case this small."
},
"actor_id": "act_0193c8f0a1b24e7f9d3c5a6b7e8f0011",
"voice_id": "voc_0193c8f0a1b24e7f9d3c5a6b7e8f0022",
"max_credits": 40
}{
"generation_id": "gen_0193c8f0a1b24e7f9d3c5a6b7e8f0033"
}{
"script": "Two weeks of battery, in a case this small.",
"actor_id": "act_0193c8f0a1b24e7f9d3c5a6b7e8f0011",
"voice_id": "voc_0193c8f0a1b24e7f9d3c5a6b7e8f0022",
"approved_voice_generation_id": "gen_0193c8f0a1b24e7f9d3c5a6b7e8f0033",
"max_credits": 400
}generate_talking_actor runs OmniHuman 1.5 by default. For MiniMax H3 Max Lip Sync, add this line to call 3, and keep the voice between 5 and 14.8 seconds:
"capability_id": "actor_h3_max"The max_credits values are placeholders: send each step's max_credits_needed (Price first). Waiting: Wait for the result.
Refused with invalid_config, before any hold:
- No finished voice generation is named. Run the voice first and wait for it.
- The script, actor or voice differ from the voice's. Send the same three to both calls.
approved_voice_generation_idis sent to Seedance 2.5 Actor. Leave it out.
Use your own recording
Have a voice file? Skip Text to Speech. Upload it, wait until it is usable, then send its id as voice_audio:
{
"capability_id": "actor_ultra",
"config": {
"voice_audio": ["ast_0193c8f0a1b24e7f9d3c5a6b7e8f0044"]
},
"actor_id": "act_0193c8f0a1b24e7f9d3c5a6b7e8f0011"
}- Send no script and no
voice_id. The recording sets the length, inside the actor's range above. - On MCP, use
submit_generation:generate_talking_actorneeds a script.
One call
Seedance 2.5 Actor speaks the script as part of the render. There is no voice step, no voice_id and no approved_voice_generation_id.
{
"capability_id": "actor_seedance",
"config": {
"script": "Two weeks of battery, in a case this small."
},
"actor_id": "act_0193c8f0a1b24e7f9d3c5a6b7e8f0011"
}- The script sets the length, 4 to 30 seconds. A longer script is refused before any hold.
- The voice changes every render: two ads from one actor sound like different people.
Bring your own files
- Reserve with the file name, type and exact size. You get an
asset_idand anupload_urlthat lasts 5 minutes. - PUT the bytes to
upload_url, with no API key. - Finalize. Use the
ast_id once the answer saysusable: true.
- Or import from a direct link to one media file (a web page is refused).
riffads upload <file>does all three steps.- Sizes and types: Limits. Fields: Uploads.
Put the file in a slot field of config, then point at its alias in the prompt (/image1, /video1). Video models, image models and presets take a list of entries:
{
"prompt": "Slow push in, the bottle turns toward camera",
"start_frame": [
{
"assetId": "ast_0193c8f0a1b24e7f9d3c5a6b7e8f0061",
"alias": "image1",
"role": "start_frame"
}
],
"duration": 5
}Every other file field (editing tools, voice tools, actor recordings, text jobs) takes plain ids, like "voice_audio": ["ast_..."]. The schema shows the form.
One job at a time
- One agent job runs at a time: per API key on REST and the CLI, per person on MCP.
- A second submit gets
submission_in_flight(409) within_flight.generation_id. Wait on that job, then submit. - A
variantssubmit is one job. Studio jobs do not count. A stuck job frees the slot after 10 minutes. - For parallel REST jobs, use a second API key.
Refusals and retries
Every refusal has the same fields on every surface.
| Field | What it is |
|---|---|
code | The fixed reason. Branch on it |
message | One safe sentence. Show it, never parse it |
retryable | Whether you may resend the same call |
retry_after_seconds | Wait this long first (some codes only) |
credits_charged | What it cost, almost always 0 |
retryable: false: change the request or stop.retryable: true: at most two more tries.moderation_blockedis final. Never reword a request to get past the check.- Lost the answer to a submit? List your generations first: a second submit is a second charge.
Every code: Errors.
Webhooks
A server can skip the wait loop: RiffAds POSTs a signed event when work settles.
- Register an endpoint once per workspace (REST, CLI or the app). No request takes a webhook field, and MCP cannot register one.
- Events:
generation.completed,generation.failed,batch.settled,workflow_run.completed,workflow_run.failed,credits.low. - An event carries ids, never a link. Read the generation, then download.
- Verify each delivery with the
webhook-id,webhook-timestampandwebhook-signatureheaders: Webhooks.
Same job, every surface
| Job | MCP tool | CLI command | REST route |
|---|---|---|---|
| List models and tools | list_capabilities | riffads capabilities | GET /capabilities |
| Read a schema | get_capability_schema | riffads capabilities show <id> | GET /capabilities/{id} |
| Price a job | estimate_generation | riffads estimate | POST /estimates |
| Start a job | submit_generation | riffads generate | POST /generations |
| Talking actor (how) | generate_talking_actor | riffads generate --actor | POST /generations |
| Analyze an ad | submit_generation with Analyze Media | riffads analyze | POST /generations |
| Wait for a job | wait_for_generation | riffads status <id> --wait | GET /generations/{id}/wait |
| Read a job now | get_generation | riffads status <id> | GET /generations/{id} |
| Read all takes | get_batch | riffads batch <bg_id> | GET /batches/{id} |
| List past jobs | list_generations | riffads list | GET /generations |
| Get a fresh link | get_asset | riffads download <id> | GET /assets/{id}/download |
| Search your files | search_library | riffads search | GET /assets |
| Upload a file | create_upload, finalize_upload | riffads upload <file> | POST /uploads, POST /uploads/{id}/finalize |
| Import from a link | import_media_from_url | riffads upload --url | POST /uploads/from-url |
| Find actors | list_actors | riffads actors | GET /actors |
| Find voices | list_voices | riffads voices | GET /voices |
| Presets | list_presets | riffads presets | GET /presets, GET /presets/{id}/templates |
| Workflow templates | list_templates | riffads workflows templates | GET /workflows/templates |
| Run a workflow | run_workflow | riffads workflows invoke | POST /workflows/templates/{key}/invoke, POST /workflows/{id}/invoke |
| Follow a run | get_workflow_run | riffads workflows run <id> | GET /workflow-runs/{id} |
| List runs | list_workflow_runs | riffads workflows runs | GET /workflow-runs |
| Credit balance | get_credit_balance | riffads credits | GET /credits/balance |
| Check the connection | riffads_ping | riffads whoami | GET /credits/balance |
| Webhooks | none | riffads webhooks | GET /webhooks, POST /webhooks, DELETE /webhooks/{id} |
Every field and flag: MCP tools, CLI commands, Generations API.