CLI

CLI commands

Every riffads command and flag, JSON output, exit codes and CI use.

Read as Markdown

CLI: not distributed yet

Everything on this page is built and works against the live API. The package itself is not published yet, so the install command below will not resolve until it is. The REST API and the MCP server are open today.

14 commands. Each wraps REST API calls. Setup first: CLI setup.

CommandStarts paid workWhat it does
loginnoChecks a key, then saves it
logoutnoDeletes the saved key file
whoaminoWhich key and host it will use, and whether they work
creditsnoAvailable credits and the agent spend ceiling
capabilitiesnoWhat you can generate. show <id> adds schema and example
actorsnoActors (act_)
voicesnoVoices (voc_)
estimatenoThe number for --max-credits. Reserves nothing
generateyesStarts a job, waits, saves the file
statusnoReads one generation (gen_)
batchnoReads every variant of one submit (bg_)
downloadnoSaves outputs to disk
uploadnoUploads a file, prints its ast_ id
workflowson invokeLists templates, starts and follows runs

Global options

Put them before or after the command.

FlagShortDefaultWhat it does
--jsonoffFull API response as JSON on stdout, signed links included
--quiet-qoffHide progress and notes. Errors and warnings still print
--api-key <key>noneUse this key over the env var and saved login
--base-url <url>https://riffads.comHost. Set https://app.riffads.com
--help-hoffHelp for the command
  • Aliases: ls runs capabilities, balance runs credits, get runs status.
  • riffads --version, -v or riffads version prints the version.
  • Write --json and --quiet bare. --json=true does not switch output.
  • Accepted forms: --name value, --name=value, -s value, --no-flag, -- to stop parsing. No bundled short flags (-abc).
  • Unknown options are refused, with a hint: Unknown option "--max-credit". Did you mean --max-credits?

Account

login, logout, whoami

Terminal
riffads login [--api-key <key>] [--base-url <url>]
riffads logout
riffads whoami
  • login exits 0 saved, 2 no key given, 3 key refused. stdout stays empty.
  • logout makes no network call. Always exits 0.
  • whoami --json adds base_url_source, api_key_source (flag, env, file, or default for the host), credits_available and agent_limits.

Details: CLI setup.

credits

Alias balance. Output goes to stdout, so riffads credits | jq works with --json.

Terminal
riffads credits --json | jq '{available: .credits_available, ceiling: .agent_limits.most_one_generation_may_cost, bound_by: .agent_limits.bound_by}'

most_one_generation_may_cost is the highest --max-credits a job can pass right now. bound_by names the cap that sets it.

Browse

Read commands share a limit of 120 requests a minute.

capabilities

Alias ls.

Terminal
riffads capabilities [show <capability-id>]
FlagDefaultWhat it does
--limit <n>server defaultRows per page
--cursor <cursor>noneContinue from next_cursor
--alloffFollow every page
--schemaoffWith show: only the JSON config schema
--exampleoffWith show: only a valid example config
Terminal
riffads capabilities show actor_ultra --example > brief.json
riffads capabilities show actor_ultra --schema | jq '.properties | keys'
  • List lines: id, output kind, status, name, and needs <plan> when gated.
  • show is required: riffads capabilities actor_ultra exits 2.
  • The CLI never validates config. The server names the bad field.

Every id: capabilities.

actors and voices

Terminal
riffads actors --search casual --limit 20
riffads actors --all --json | jq -r '.actors[].actor_id'
riffads voices --language en --search warm --json | jq -r '.voices[] | "\(.voice_id)\t\(.name)"'
FlagShortOnWhat it does
--search <text>-sbothMatch name, description, tags
--language <code>voicesFilter by language, for example en
--limit <n>bothRows per page
--cursor <cursor>bothContinue from next_cursor
--allbothFollow every page

actors --json adds default_voice_id, default_voice_name, is_platform.

Generate

estimate

Terminal
riffads estimate --capability <id> [--config <json|@file|->] [--set k=v]

Spends nothing. Limit: 60 a minute. Takes the same --capability, --config, --set, --set-string flags as generate.

stdout is a bare integer. Details go to stderr.

Terminal
EST=$(riffads estimate --capability veo_31 --config @look.json)
MAX=$(( (EST * 11 + 9) / 10 ))   # ceil(estimate * 1.1)

at most on stderr means is_ceiling is true. --json gives credits, is_ceiling, breakdown, and note and seconds when present.

generate

Terminal
riffads generate --capability <id> --config <json> --max-credits <n>
FlagShortRequiredDefaultWhat it does
--capability <id>-cyesCapability to run
--config <json|@file|->no{}Capability settings
--set <key=value>no, repeatableOne field, coerced
--set-string <key=value>no, repeatableOne field, kept as a string
--max-credits <n>-myesnoneMost this job may cost, all variants
--variants <n>noserver defaultSeveral takes of one config, up to 4, each charged
--actor <actor-id>noLibrary actor
--actor-image-asset <asset-id>noUploaded image as the face
--voice <voice-id>noVoice
--approved-voice-generation <generation-id>noFinished tts generation for a talking actor
--no-waitnooffPrint the id and exit 0
--timeout <seconds>no1800Stop waiting after this
--output <dir>-onoSave outputs here

Send ceil(estimate * 1.1) as --max-credits

The bare estimate is refused with max_credits_exceeded, exit 4. --max-credits has no default: leaving it out exits 2.

What it does:

  1. Submits. The id prints to stdout first, so a broken wait still leaves you the id.
  2. Waits on the server's wait endpoint. Retryable refusals during the wait are slept off, up to 5 in a row.
  3. Saves outputs with -o as <generation_id>-<index><ext>. Absolute paths go to stdout, one per line.
stderr (sample)
Veo 3.1: 484 credits held for 1 output
gen_8c31fa: completed, 1 of 1 delivered
held 484 credits, charged 462 credits (captured)
  [0] video https://files.riffads.com/org/gen_8c31fa-0.mp4 (signed link hidden: use --json or riffads download)
  • Wait timeout: exit 5. Nothing is canceled.
  • Ctrl+C stops the wait, not the job. Exit 130.
  • generate -o has no --force. An existing file warns and exits 2 even if the job worked. Use riffads download <id> -o ./out --force.
  • One job at a time per key. A second gets submission_in_flight, exit 5.
  • run_id null prints Queued, starting shortly. Not a failure.

Talking actor: two generations

Voice first. Its id drives the video.

Terminal
SCRIPT='Three reasons these sneakers sold out twice.'

VOICE=$(riffads generate -c tts \
  --config "{\"script\":\"$SCRIPT\"}" \
  -m "$VOICE_MAX" --voice voc_ada_en --no-wait)

riffads status "$VOICE" --wait

riffads generate -c actor_ultra \
  --config "{\"script\":\"$SCRIPT\"}" \
  -m "$ACTOR_MAX" \
  --actor act_7f2a91 --voice voc_ada_en \
  --approved-voice-generation "$VOICE" \
  --output ./out
  • tts needs --voice.
  • actor_ultra has no aspect_ratio. Output is 720p, start frame shape, up to 60 seconds of audio.

More: talking actor.

Passing a config

estimate and generate parse config the same way.

Terminal
--config '{"script":"..."}'    # inline
--config @brief.json           # a file
--config -                     # stdin
  • Must be a JSON object. Omitted: {}.
  • --set runs after --config. Dotted paths nest: --set voice.speed=1.2 gives {"voice":{"speed":1.2}}.
  • --set coerces true, false, null and numbers. --set-string never coerces and wins on the same path.
Terminal
jq -n --arg s "$SCRIPT_TEXT" '{script:$s}' \
  | riffads generate -c tts --voice voc_ada_en --config - -m "$MAX"

Follow and download

status

Alias get.

Terminal
riffads status <generation-id> [--wait] [--timeout <seconds>]
FlagShortDefaultWhat it does
--wait-woffBlock until the job ends
--timeout <seconds>1800Stop waiting after this
  • stdout: queued, rendering, post_processing, completed or failed. A canceled job reads failed.
  • An unsettled charge prints not settled yet, never 0.

batch

Terminal
riffads batch <batch-group-id> [--wait] [--timeout <seconds>]

Same flags as status. stdout: running, completed, failed or partial. Exit code is the worst variant's.

download

Terminal
riffads download <generation-id> [--output <dir>] [--index <n>] [--force]
FlagShortDefaultWhat it does
--output <dir>-o.Target folder, created if missing
--index <n>allOnly this output
--force-foffOverwrite existing files
Terminal
for f in $(riffads download gen_8c31fa -o ./out); do ffprobe "$f"; done
  • Takes an id, never a URL. It fetches fresh signed links itself (links last 600 seconds).
  • Absolute paths on stdout, one per line.
  • Writes <file>.part, renames when complete. A file is complete or absent.
  • Up to 3 attempts per call.
  • Existing file without --force: warning, exit 2. Nothing delivered yet: exit 7.

upload

Terminal
riffads upload <file> [--content-type <type>]

Uploads and finalizes. Only the ast_ id goes to stdout.

ExtensionsType
.png, .jpg, .jpeg, .webp, .gifimage
.mp4, .mov, .webmvideo
.mp3, .wav, .oggaudio

Other extensions are refused. --content-type overrides the guess. Size limits: uploads.

Terminal
riffads generate -c actor_ultra \
  --actor-image-asset "$(riffads upload face.png)" \
  --voice voc_ada_en --approved-voice-generation "$VOICE" \
  --config @brief.json -m "$MAX"

Exit 0 only when the file finalizes as usable: true. Otherwise 2.

Workflows

Terminal
riffads workflows <templates|invoke|run> [...]
FlagShortDefaultWhat it does
--template <key>noneWith invoke: run a template, not a saved workflow
--max-credits <n>-mnone, required to invokeMost the whole run may cost
--input <node.field=value>-inoneOne input on one node, repeatable
--wait-woffFollow until done
--timeout <seconds>3600Stop following after this

workflows templates

Lists templates: key, step count, name on stdout. Each input as the -i flag to pass on stderr. No subcommand runs this one.

stdout
product_demo                 4 steps  Product demo

There is no list of saved workflows. Copy the workflow id from the app.

workflows invoke

Terminal
riffads workflows invoke <workflow-id> -m <n> [-i node.field=value] [--wait]
riffads workflows invoke --template <key> -m <n> [-i node.field=value] [--wait]
Terminal
riffads workflows invoke --template product_demo \
  --max-credits 900 \
  -i script.text='new sneakers, upbeat' \
  --wait --timeout 5400
  • Exactly one of workflow id or --template.
  • --input splits on the first = and the last . before it. Values coerce true, false and numbers. Everything else is a string.
  • stdout: the run id (wfr_...).

workflows run

Terminal
riffads workflows run <workflow-run-id> [--wait] [--timeout <seconds>]

Reads a run once, or with --wait checks every 30 seconds. stdout: the run status. stderr: one line per node.

Run stateExit
Still running at timeout5
Partial7
Failed or canceled, nothing charged6
Failed after some steps charged7
Otherwise0

More: workflows.

Output streams

StreamCarries
stdoutData: ids, file paths, asset ids, status words, --json bodies
stderrPeople: spinner, costs, hints, warnings, errors
Terminal
GEN=$(riffads generate -c tts --config '{"script":"hello there"}' --voice voc_ada_en -m 40 --no-wait)
ASSET=$(riffads upload face.png)
STATE=$(riffads status "$GEN")

Signed links never print to stdout unless you pass --json.

JSON output

--json prints the response body on stdout. Failures print too:

stdout under --json
{
  "error": {
    "ok": false,
    "code": "max_credits_exceeded",
    "message": "This generation costs more than the max_credits you set (400 credits). This one needs 462 credits. Raise max_credits, or ask for something cheaper, such as a shorter or lower resolution render.",
    "retryable": false,
    "credits_charged": 0,
    "limit": { "bound_by": "max_credits", "limit_credits": 400, "required_credits": 462 }
  }
}

generate --json does not pipe straight into jq

The id line prints before the JSON. Split submit from read instead:

GEN=$(riffads generate -c tts --config @brief.json --voice voc_ada_en -m 60 --no-wait)
riffads status "$GEN" --wait --json | jq
CallJSON bodyHas
generate --no-wait --jsonSubmit bodygeneration_ids
generate --json, one output, finishedWait body plus settled generationno generation_ids
generate --json with --variantsBatch bodygenerations
status --jsonGeneration body
batch --jsonBatch bodygenerations

login, logout and download print no JSON on success.

credits_charged is null until the job settles. Never read null as 0.

Exit codes

ExitMeaningError codes
0Worked
1Something broke. Tell a humaninternal_error
2Change the requestinvalid_config, capability_unavailable, not_found, moderation_blocked, request_blocked
3Fix key, scope, plan or workspacenot_authorized, no_workspace, connection_not_configured, workspace_unavailable, required_plan, read_only_connection, insufficient_scope
4Not enough credits, or a spend capinsufficient_credits, max_credits_exceeded, spend_limit_exceeded, not_priced, quota_exceeded
5Refused, safe to send againinput_not_ready, estimate_changed, pricing_unavailable, moderation_unavailable, rate_limited, submission_in_flight, provider_unavailable
6Job failed, nothing charged
7Job delivered only part of what was asked
130Ctrl+C. The job was not canceled
  • 6 and 7 are job outcomes. A refused submit never exits 6 or 7, and prints no id.
  • internal_error is retryable: true on the wire but exits 1, so CI does not retry an outage.
  • Batches exit with the worst variant. Order, least to worst: 0 < 6 < 7 < 5 < 2 < 4 < 3 < 1.

Meaning of each code: errors.

Retries

  • Inside a wait, the CLI retries for you: up to 5 in a row, honouring retry_after_seconds, else 1, 2, 4, 8, 16 seconds.
  • A submit is never retried. rate_limited, submission_in_flight and provider_unavailable on submit exit 5 at once.
  • One job in flight per key. Use one key per parallel lane.
  • Resending after the first job finished starts a second paid job.
  • Timeouts cancel nothing. No render time is promised: budget generously, or --no-wait and follow later.

CI script

ci-generate.sh
#!/usr/bin/env bash
set -uo pipefail

export RIFFADS_API_KEY="${RIFFADS_API_KEY:?set the key}"
export RIFFADS_BASE_URL=https://app.riffads.com

# 1. Check key and host. Exits 3 if wrong.
riffads whoami || exit $?

# 2. Start from a valid example.
riffads capabilities show tts --example > brief.json
jq --arg s "Three reasons these sneakers sold out twice." \
   '.script = $s' brief.json > brief.tmp && mv brief.tmp brief.json
VOICE=$(riffads voices --language en --json | jq -r '.voices[0].voice_id') || exit $?

# 3. Estimate. Free.
EST=$(riffads estimate --capability tts --config @brief.json) || exit $?

# 4. max_credits: ceil(estimate * 1.1).
MAX=$(( (EST * 11 + 9) / 10 ))

# 5. Submit and save. stdout: id on line 1, then one path per line.
riffads generate \
        --capability tts \
        --config @brief.json \
        --voice "$VOICE" \
        --max-credits "$MAX" \
        --output ./out > gen.out
CODE=$?
GEN=$(head -n 1 gen.out)

case "$CODE" in
  0) echo "delivered: $GEN" ;;
  2) echo "fix the request or the content"; exit 2 ;;
  3) echo "fix the key, its scope, the plan or the workspace"; exit 3 ;;
  4) echo "top up in the app, or ask for less"; exit 4 ;;
  5) echo "retryable: sleep and send the same call again"; exit 5 ;;
  6) echo "the job failed and nothing was charged"; exit 6 ;;
  7) echo "only part of it arrived"; riffads status "$GEN"; exit 7 ;;
  *) echo "broken, tell a human"; exit 1 ;;
esac
  • No set -e. It would exit before the case runs.
  • Do not capture generate -o in a variable. stdout has the id, then paths. Redirect and take line 1.
  • Derive MAX every run. Never hardcode it.

Not in the CLI

  • No cancel, logs or publish command.
  • No list of past generations or saved workflows. Keep your ids.
  • No outbound webhooks.
  • No workflow authoring.
  • Nothing posts to Meta, TikTok, YouTube or X.

On this page