Setup
Get an API key and make your first video with four HTTP calls.
Read as MarkdownMake your first video from a terminal: make a key, estimate, submit, then wait and download.
Base URL for every call:
Auth header on every call:
Authorization: Bearer sk_live_...- Server side only. Never call the API from a browser.
- You need a workspace on a paid plan or an active trial, plus
curlandjq.
Make a key
Owners and admins make keys on the API keys page:
Tick Generate, or every submit fails with 403 insufficient_scope. The key starts with sk_live_ and is shown once, so copy it now.
export RIFFADS_API_KEY="sk_live_..."Estimate
The estimate is free and starts nothing. It returns two numbers:
credits: the expected price.max_credits_needed: the number to send asmax_creditson the submit.
/api/v1/estimatesAPI keyREQ='{
"capability_id": "kling_3_pro",
"config": {
"prompt": "Handheld close-up of a matte black water bottle on a gym bench, morning light",
"aspect_ratio": "9:16",
"duration": 5,
"generate_audio": true
}
}'
EST=$(curl -s -X POST https://app.riffads.com/api/v1/estimates \
-H "Authorization: Bearer $RIFFADS_API_KEY" \
-H "Content-Type: application/json" \
-d "$REQ")
echo "$EST" | jq '{credits, max_credits_needed}'
MAX=$(echo "$EST" | jq '.max_credits_needed')This asks Kling 3 Pro for a 5 second vertical clip with sound.
Submit
Send the same request plus max_credits, taken from the estimate. The answer is 201 with a generation_id (gen_...). Save it.
/api/v1/generationsAPI keyGEN=$(echo "$REQ" | jq --argjson max "$MAX" '. + {max_credits: $max}' \
| curl -s -X POST https://app.riffads.com/api/v1/generations \
-H "Authorization: Bearer $RIFFADS_API_KEY" \
-H "Content-Type: application/json" \
-d @- \
| jq -r '.generation_id')
echo "$GEN"The body is strict: a typo like maxCredits is a 400.
Wait and download
Each wait call blocks up to 20 seconds. Call it again while still_running is true (Wait for the result).
/api/v1/generations/{id}/waitAPI keywhile :; 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')" = "true" ] || break
done
echo "$BODY" | jq -r '.generation.status'
URL=$(echo "$BODY" | jq -r '.generation.outputs[0].url // empty')
[ -n "$URL" ] && curl -sL -o ad.mp4 "$URL"| You see | It means |
|---|---|
completed | The file is in ad.mp4 |
failed | There is no file. Read generation.error |
An error object | The call was refused. See Troubleshooting |
- Why
max_credits_neededand notcredits: Price first. - Links expire in 10 minutes, so keep the
asset_id: Download links.
Troubleshooting
Every error has code, message and retryable. All codes: Errors.
| Code | What usually went wrong | Fix |
|---|---|---|
not_authorized | The key has quotes or a trailing newline, the header is not Bearer, or the key was revoked | Send Authorization: Bearer sk_live_... exactly, with a live key |
insufficient_scope | The key was made without Generate | Make a new key with Generate ticked |
max_credits_exceeded | You sent the estimate's credits, or a number you picked | Send the estimate's max_credits_needed, or limit.required_credits from the refusal |
invalid_config | A field name is misspelled, or a value is not allowed | The message names the field. Check it against GET /capabilities/{id} |
insufficient_credits | The wallet is too low for this job | Top up in the app |
spend_limit_exceeded | A limit a person set blocked it | limit.bound_by names which one. See Spend limits |
submission_in_flight | You sent a second job while the first is still running on this key | Wait for the first one, then submit |
moderation_blocked | The script, prompt or file broke the content policy | Change the wording or the file. The same one fails again |
required_plan | The plan does not include the API, or this model needs a higher plan | Upgrade the plan in the app. When the refusal has required_plan, it names the plan you need |
Next
Generations
Every field on estimate, submit, read, wait and list.
Uploads
Send your own product shot, face or clip.
Webhooks
Get a signed event when work ends, instead of a wait loop.
The machine-readable OpenAPI spec for every route: