REST API

Generations

Estimate, submit, read, wait on and batch-read a generation.

Read as Markdown

Every endpoint that makes a video, image or audio file, and every field they take and return.

EndpointScopeRate limitWhat it does
POST /estimatesRead60/minReturns the number to put in max_credits. Starts nothing
POST /generationsGenerate20/minStarts a job. 201 with Location
GET /generations/{id}Read120/minReads one generation. Answers now
GET /generations/{id}/waitRead120/minBlocks up to 20 seconds, then says if done
GET /batches/{id}Read120/minReads every sibling of a variants submit

There is no list endpoint and no cancel endpoint. Keep every generation_id you get.

Estimate

POST/api/v1/estimatesAPI key

Returns the number you need for max_credits. Starts nothing, charges nothing, runs no content check.

Terminal
curl -s https://app.riffads.com/api/v1/estimates \
  -H "Authorization: Bearer $RIFFADS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "capability_id": "tts",
    "config": {
      "script": "Three reasons this bottle keeps water cold all day."
    }
  }'
Response: 200
{
  "ok": true,
  "capability_id": "tts",
  "credits": 4,
  "is_ceiling": false,
  "breakdown": [
    { "label": "Tool", "unit": "second", "quantity": 4, "credits": 4 }
  ]
}

Request body

Strict. Exactly two keys. A third is a 400.

Prop

Type

  • No max_credits, variants, actor_id or approved_voice_generation_id here. Those are submit fields.
  • Pricing several variants? Put count inside config.
  • Capabilities that work on a file (auto_caption, change_voice, remove_background, resize, speech_to_speech, trim_video, upscale) need the upload id in config. Missing file: invalid_config. Uploads.
Request: file-based capability
{
  "capability_id": "auto_caption",
  "config": {
    "source_video": ["ast_9f2c41ab7d0e4c8fb35a6e1d2c904f77"]
  }
}

A good estimate does not mean a good submit

The submit refuses any config key the schema does not list. The estimate is not always that strict. Send only keys from config_schema.

Response

Prop

Type

breakdown[] fieldTypeNotes
labelstringDisplay text only
unitstringsecond, image, thousand_chars, million_tokens, request
quantitynumber
creditsinteger

Numbers change over time. Call the estimate before each submit. Don't cache it.

Estimate errors

CodeHTTPRetryCause
rate_limited429yesOver 60 a minute
capability_unavailable404noUnknown, retired or not live. Has capability_status
required_plan403noNot on this plan. Has required_plan
invalid_config400noConfig fails the schema
not_priced409noCapability can't be used yet. Try another
pricing_unavailable503yesOur side. Retry shortly
internal_error500yesOur side

Submit

POST/api/v1/generationsAPI key

Starts a job. Answers when the job is accepted, not done.

Terminal
curl -s https://app.riffads.com/api/v1/generations \
  -H "Authorization: Bearer $RIFFADS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "capability_id": "veo_31",
    "config": { "prompt": "A steel bottle on a kitchen counter, morning light, slow push in." },
    "max_credits": 440
  }'

Request body

Prop

Type

Sending the bare estimate fails

max_credits must be at least ceil(estimate * 1.1). Estimate 400 means send 440 or more. Less gets 402 max_credits_exceeded, nothing started. This is the most common first-integration failure.

  • Unknown key, in the body or in config: 400 invalid_config naming the key. maxCredits is refused.
  • These fields don't exist and answer 400: project_id, organization_id, quote, source, purpose, idempotency_key, workflow_run_id, pending_actor, webhook_url, callback_url.
  • Naming trap: body field is voice_id, config field is voiceId.
  • variants fills the capability's own count field and wins over count in config. A capability with no count refuses variants.

One submission at a time per key

A second POST /generations on the same key while the first is running gets 409 submission_in_flight. Wait on in_flight.generation_id, then submit.

Response: 409
{
  "error": {
    "ok": false,
    "code": "submission_in_flight",
    "message": "This API key already has a generation running (gen_0193c8f0a1b24e7f9d3c5a6b7e8f0033, rendering). Wait for it to finish, or check on it with get_generation, before starting another one.",
    "retryable": true,
    "credits_charged": 0,
    "in_flight": {
      "generation_id": "gen_0193c8f0a1b24e7f9d3c5a6b7e8f0033",
      "status": "rendering"
    }
  }
}
  • variants: 4 is one submit, one slot.
  • A stuck job frees the slot after 10 minutes.
  • Jobs started in the app don't count.
  • Need parallel jobs? Use a second key.

Response

Response: 201 Created
{
  "ok": true,
  "generation_id": "gen_0193c8f0a1b24e7f9d3c5a6b7e8f0033",
  "generation_ids": ["gen_0193c8f0a1b24e7f9d3c5a6b7e8f0033"],
  "run_id": "run_9f2c1b",
  "replay": false,
  "credits": {
    "credits_held": 440,
    "credits_charged": null,
    "settlement": "open",
    "terminal": false,
    "wallet_balance": 4104
  },
  "capability_id": "veo_31",
  "capability_name": "Veo 3.1",
  "output_kind": "video",
  "outputs_expected": 1,
  "charge_summary": "440 credits are on hold. Nothing has been charged so far, and the final amount is not known until this finishes.",
  "next_action": "GET /api/v1/generations/gen_0193c8f0a1b24e7f9d3c5a6b7e8f0033/wait, and keep calling it until still_running is false. Do not hand the checking back to the person and do not ask them to prompt you again."
}
Response headers
HTTP/1.1 201 Created
Location: /api/v1/generations/gen_0193c8f0a1b24e7f9d3c5a6b7e8f0033
Cache-Control: no-store
FieldTypeAlwaysNotes
generation_idstringyesThe one to wait on. With variants: the first sibling
generation_idsstring[]yesLength 1 without variants
run_idstring or nullyesnull is normal: queued, starting shortly
batch_group_idstringvariants onlyA bg_ id for GET /batches/{id}
replaybooleanyestrue: you got an already running job back. See conventions
creditsobjectyesSee the credits object
capability_idstringyesEchoed
capability_namestringyesDisplay name
output_kindimage video audio textyes
outputs_expectedintegeryesHow many files to expect
charge_summarystringyesOne readable sentence about credits
next_actionstringyesAdvice for agents. Don't branch on it

Submit errors

The first check that fails wins. Every refusal here has credits_charged: 0.

SituationCodeHTTPRetry
Over 20 submits a minute on this keyrate_limited429yes
Key lacks Generateinsufficient_scope403no
max_credits too lowmax_credits_exceeded402no
A workspace or key spend cap blocked it (ask an owner)spend_limit_exceeded402no
Not enough credits (top up in the app)insufficient_credits402no
Unknown key, or config fails the schemainvalid_config400no
Capability retired or not livecapability_unavailable404no
Capability needs a higher planrequired_plan403no
Script, prompt or upload refusedmoderation_blocked422no
Content checks downmoderation_unavailable503yes
Upload still scanninginput_not_ready409yes
Upload not in this workspacenot_found404no
Another job already running on this keysubmission_in_flight409yes
Same request failed too oftenrequest_blocked429no
Estimate moved since you checkedestimate_changed409yes
Capability can't be used yetnot_priced409no
Our sidepricing_unavailable503yes
  • max_credits_exceeded and spend_limit_exceeded carry limit.bound_by, naming the cap that refused.
  • Every code: errors.

Read one generation

GET/api/v1/generations/{id}API key

Answers right away. Another workspace's id answers not_found.

Response: 200
{
  "ok": true,
  "generation": {
    "generation_id": "gen_0193c8f0a1b24e7f9d3c5a6b7e8f0033",
    "capability_id": "veo_31",
    "status": "completed",
    "batch_group_id": null,
    "batch_index": null,
    "outputs": [
      {
        "index": 0,
        "kind": "video",
        "url": "https://<signed-url>",
        "file_name": "veo-31-0.mp4",
        "width": 1080,
        "height": 1920
      }
    ],
    "outputs_expected": 1,
    "outputs_delivered": 1,
    "credits": {
      "credits_held": 440,
      "credits_charged": 400,
      "settlement": "captured",
      "terminal": true,
      "wallet_balance": 4104
    },
    "charge_summary": "Charged 400 credits.",
    "credits_estimated": 400,
    "error": null,
    "created_at": "2026-09-17T10:04:11.482Z",
    "completed_at": "2026-09-17T10:07:39.120Z"
  },
  "output_urls_expire_in_seconds": 600
}
FieldTypeNotes
generation_idstringgen_ prefix
capability_idstring or nullnull only on very old rows
statusqueued rendering post_processing completed failedThe full set. A canceled job reports failed
batch_group_idstring or nullVariants only
batch_indexinteger or nullThis sibling's position
outputs[].indexinteger
outputs[].kindimage video audio
outputs[].urlstring or nullnull until the job is completed and settled
outputs[].file_namestring or null
outputs[].widthinteger or null
outputs[].heightinteger or null
outputs_expectedinteger or nullWait for this many, not outputs.length
outputs_deliveredinteger or null
creditsobjectSee below
charge_summarystringOne readable sentence
credits_estimatedintegerThe estimate at submit
errorobject or null{ code, message }. Set only when status is failed
created_atISO 8601
completed_atISO 8601 or null
output_urls_expire_in_seconds600Top level. Only when some output has a link

Links die in 600 seconds

Download the file right away. Don't store the URL. Read the generation again for a fresh link. Results.

The credits object

credits_charged is null until credits.terminal is true. null means not known yet, never zero.

Wait

GET/api/v1/generations/{id}/waitAPI key

The server watches the job for you. It answers when the job ends, or after about 18 seconds with still_running: true. Call it again in a loop.

Terminal
GEN=gen_0193c8f0a1b24e7f9d3c5a6b7e8f0033
while :; do
  BODY=$(curl -s "https://app.riffads.com/api/v1/generations/$GEN/wait" \
    -H "Authorization: Bearer $RIFFADS_API_KEY")
  [ "$(echo "$BODY" | jq -r '.still_running')" = "false" ] && break
done
echo "$BODY" | jq -r '.generation.outputs[0].url'
Response: 200, still running (generation trimmed)
{
  "ok": true,
  "generation": { "generation_id": "gen_0193c8f0...", "status": "rendering", "outputs": [] },
  "still_running": true,
  "waited_seconds": 18,
  "age_seconds": 41,
  "retry_after_seconds": 0,
  "next_action": "Still working. Call wait_for_generation again with the same generation_id right away, and keep calling it until still_running is false. Do not ask the person to prompt you again."
}
FieldTypeAlwaysNotes
generationobjectyesSame shape as GET /generations/{id}
still_runningbooleanyesBranch on this, not on status
waited_secondsintegeryesHow long this call blocked
age_secondsintegeryesSeconds since the job was created. Log it and cap it
retry_after_seconds0while runningCall straight back
next_actionstringyesNames MCP tools even over REST. Advice only
output_urls_expire_in_seconds600when a link exists
  • Timeout answers 200, never 408 or 504.
  • Each call costs 1 read from the 120 a minute bucket, however long it blocks.
  • A finished job answers at once.
  • No timeout parameter.
  • There is no published render time. Don't build a deadline from the wait.
  • A wait loop uses far fewer requests than polling GET /generations/{id}.

Read a batch

GET/api/v1/batches/{id}API key

A submit with variants makes several siblings. Watch only the first id and you will stop while the others still run. Pass the bg_ batch_group_id from the submit.

Response: 200 (siblings trimmed)
{
  "ok": true,
  "batch_group_id": "bg_0193c8f0a1b24e7f9d3c5a6b7e8f0099",
  "status": "partial",
  "variants": 4,
  "variants_finished": 4,
  "outputs_delivered": 3,
  "credits": {
    "credits_held": 168,
    "credits_charged": 126,
    "settlement": "captured",
    "terminal": true
  },
  "charge_summary": "Charged 126 credits for the 3 of 4 outputs delivered.",
  "generations": [
    { "generation_id": "gen_0193...a1", "batch_index": 0, "status": "completed" },
    { "generation_id": "gen_0193...a2", "batch_index": 1, "status": "completed" },
    { "generation_id": "gen_0193...a3", "batch_index": 2, "status": "completed" },
    { "generation_id": "gen_0193...a4", "batch_index": 3, "status": "failed" }
  ],
  "next_action": "Part of this batch delivered and part failed. 3 files are ready, and you are charged only for what was delivered. Hand over what there is and say plainly which variants did not make it.",
  "output_urls_expire_in_seconds": 600
}
FieldTypeNotes
batch_group_idstringEchoed
statusrunning completed failed partial
variantsintegerSiblings in the batch
variants_finishedintegerSiblings completed or failed
outputs_deliveredintegerSummed
creditsobjectRolled up. credits_charged stays null until every sibling is done
charge_summarystringOne sentence for the batch
generationsarrayEach sibling, same shape as GET /generations/{id}
next_actionstring
output_urls_expire_in_seconds600When some sibling has a link

While status is running: wait on one sibling with /wait, then read the batch again. Looping on GET /batches/{id} burns reads.

Talking actor is two submits

  1. Submit tts with the script and a voice_id. Wait for it.
  2. Submit actor_ultra with the same script, same voice_id, an actor_id, and the finished tts generation_id as approved_voice_generation_id.
Request: step 2
{
  "capability_id": "actor_ultra",
  "config": { "script": "Three reasons this bottle keeps water cold all day." },
  "actor_id": "act_0193c8f0a1b24e7f9d3c5a6b7e8f0011",
  "voice_id": "voc_0193c8f0a1b24e7f9d3c5a6b7e8f0022",
  "approved_voice_generation_id": "gen_0193c8f0a1b24e7f9d3c5a6b7e8f0044",
  "max_credits": 160
}
  • Skip step 1: 400 invalid_config, Generate and approve the voice before creating the video.
  • Script, actor and voice must match across both calls.
  • Voice is picked in order: body voice_id, then config.voiceId, then the actor's default. None: 400 invalid_config.
  • actor_ultra has no aspect_ratio. Sending one is an unknown key. The video takes the actor image's shape.
  • The id is actor_ultra. talking_actor is not a capability id.
  • The actor_ultra estimate from a script is a ceiling (is_ceiling: true). Put an uploaded audio id in config.voice_audio (an array) for an exact one.

Full walkthrough: talking actor.

On this page