# Setup (/api/setup)



Make your first video from a terminal: make a key, estimate, submit, then wait and download.

**Base URL** for every call:

<https://app.riffads.com/api/v1>

**Auth header** on every call:

```text title="Header"
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 `curl` and `jq`.

### Make a key [#make-a-key]

Owners and admins make keys on the API keys page:

<https://app.riffads.com/api-keys>

Tick **Generate**, or every submit fails with `403 insufficient_scope`. The key starts with `sk_live_` and is shown once, so copy it now.

```bash title="Terminal"
export RIFFADS_API_KEY="sk_live_..."
```

### Estimate [#estimate]

The estimate is free and starts nothing. It returns two numbers:

* `credits`: the expected price.
* `max_credits_needed`: the number to send as `max_credits` on the submit.

`POST /api/v1/estimates`

```bash title="Terminal"
REQ='{
  "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 (`kling_3_pro`) for a 5 second vertical clip with sound.

### Submit [#submit]

Send the same request plus `max_credits`, taken from the estimate. The answer is `201` with a `generation_id` (`gen_...`). Save it.

`POST /api/v1/generations`

```bash title="Terminal"
GEN=$(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 [#wait-and-download]

Each wait call blocks up to 20 seconds. Call it again while `still_running` is `true` ([Wait for the result](/how-it-works#wait-for-the-result)).

`GET /api/v1/generations/{id}/wait`

```bash title="Terminal"
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')" = "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](#troubleshooting) |

* Why `max_credits_needed` and not `credits`: [Price first](/how-it-works#price-first).
* Links expire in 10 minutes, so keep the `asset_id`: [Download links](/how-it-works#download-links).

## Troubleshooting [#troubleshooting]

Every error has `code`, `message` and `retryable`. All codes: [Errors](/reference/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](/how-it-works#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 [#next]

- [Generations](/api/generations): Every field on estimate, submit, read, wait and list.

- [Uploads](/api/uploads): Send your own product shot, face or clip.

- [Webhooks](/api/webhooks): Get a signed event when work ends, instead of a wait loop.

The machine-readable OpenAPI spec for every route:

<https://app.riffads.com/api/v1/openapi.json>
