# Capabilities API (/api/capabilities)



Two reads before you submit: what you can run, and what its `config` must look like. For the full catalog in words, see [capabilities](/capabilities).

* Scope `read`. Every key has it.
* Rate bucket `agent_read`: **120 calls a minute per key**, shared with other reads.
* `capability_id` is a free string, never an enum. Presets have their own ids (`preset_fashion_tryon`). Read the list, don't hardcode it.

## List capabilities [#list-capabilities]

<Endpoint method="GET" path="/capabilities" />

| Param    | Type             | Default | Notes                      |
| -------- | ---------------- | ------- | -------------------------- |
| `limit`  | positive integer | 25      | Clamped to 1 to 50         |
| `cursor` | string           | none    | The previous `next_cursor` |

* Lenient: `?limit=abc`, `0`, `-3` or empty act as if omitted.
* No `search`, `category`, `page` or `offset`.

```bash title="Terminal"
curl -s https://app.riffads.com/api/v1/capabilities \
  -H "Authorization: Bearer $RIFFADS_API_KEY"
```

```json title="Response: 200 (trimmed)"
{
  "ok": true,
  "capabilities": [
    {
      "capability_id": "actor_ultra",
      "name": "Talking Actor",
      "description": "OmniHuman 1.5: an actor speaks your script.",
      "category": "avatar",
      "output_kind": "video",
      "status": "available"
    },
    {
      "capability_id": "veo_31",
      "name": "Veo 3.1",
      "description": "Google Veo 3.1 video generation with optional audio.",
      "category": "video",
      "output_kind": "video",
      "status": "available"
    },
    {
      "capability_id": "nb_pro",
      "name": "Nano Banana Pro",
      "description": "Gemini 3 Pro Image: the premium hero shot, up to 4K, best-in-class text rendering.",
      "category": "image",
      "output_kind": "image",
      "status": "available"
    }
  ],
  "next_cursor": "nb_pro"
}
```

The full body also has `total`: the rows this workspace sees, retired ones left out.

<TypeTable
  type="{
  capability_id: {
    type: 'string',
    required: true,
    description: 'Send as capability_id on estimate or submit. Stable across renames.',
  },
  name: {
    type: 'string',
    required: true,
    description: 'Display name. Can change. Never match on it.',
  },
  description: {
    type: 'string',
    description: 'One sentence. Omitted, not null, when empty.',
  },
  category: {
    type: '&#x22;avatar&#x22; | &#x22;video&#x22; | &#x22;image&#x22; | &#x22;tool&#x22; | &#x22;preset&#x22;',
    required: true,
    description: 'Coarse grouping.',
  },
  output_kind: {
    type: '&#x22;image&#x22; | &#x22;video&#x22; | &#x22;audio&#x22; | &#x22;text&#x22;',
    required: true,
    description: 'What comes out. Not derivable from category: Add Captions is a tool that returns video.',
  },
  status: {
    type: '&#x22;available&#x22; | &#x22;requires_plan&#x22; | &#x22;coming_soon&#x22;',
    required: true,
    description: 'Only available rows can be submitted.',
  },
  required_plan: {
    type: '&#x22;launch&#x22; | &#x22;growth&#x22; | &#x22;scale&#x22;',
    description: 'Only when status is requires_plan. The plan that includes it.',
  },
}"
/>

* **Order:** category (avatar, video, image, preset, tool), then `capability_id` ascending.
* **`next_cursor`:** the last row's `capability_id`. `null` on the last page. A stale cursor restarts at page one.
* **Hidden:** retired rows and rows not offered to agents. Ask for one by id and the detail endpoint says why.
* **It changes without notice:** plan gates, retirements, launches. Read it each session. Sent with `Cache-Control: no-store`.
* **Agent only:** AI Writer (`script_llm`) and Transcribe (`transcribe`) are listed here, not in the app. Both return text.

## Read one schema [#read-one-schema]

<Endpoint method="GET" path="/capabilities/{capability_id}" />

```bash title="Terminal"
curl -s https://app.riffads.com/api/v1/capabilities/actor_ultra \
  -H "Authorization: Bearer $RIFFADS_API_KEY"
```

```json title="Response: 200"
{
  "ok": true,
  "capability": {
    "capability_id": "actor_ultra",
    "name": "Talking Actor",
    "description": "OmniHuman 1.5: an actor speaks your script.",
    "category": "avatar",
    "output_kind": "video",
    "config_schema": {
      "type": "object",
      "properties": {
        "script": {
          "description": "Script.",
          "type": "string",
          "maxLength": 1500
        },
        "voiceId": {
          "description": "Voice. A voice id from list_voices.",
          "type": "string",
          "minLength": 1
        },
        "voice_audio": {
          "description": "Audio. Upload a voice recording to use instead of a script. Up to 60s, MP3, WAV, OGG or WEBM. Up to 1 audio asset id(s). Accepts an upload or anything this project already produced.",
          "maxItems": 1,
          "type": "array",
          "items": { "type": "string", "pattern": "^ast_[0-9a-f]{32}$" }
        },
        "resolution": {
          "default": "720p",
          "description": "Resolution.",
          "type": "string",
          "enum": ["720p"]
        }
      },
      "additionalProperties": false,
      "description": "Configuration for Talking Actor. Only the keys listed here are accepted: any other key is refused rather than ignored. A key with a default may be omitted. Call estimate_generation to price a config before submitting it."
    },
    "example_config": {
      "resolution": "720p"
    }
  }
}
```

No `status` on the detail body. A `200` means you can run it.

### Schema rules [#schema-rules]

* **Strict.** `additionalProperties: false`. An unknown key in `config` (like `maxCredits`) is `400 invalid_config`.
* **Locked preset fields are absent.** They are not yours to set.
* **Keys with a default may be left out.** `required` lists only required keys with no default. `actor_ultra` has none, so it has no `required` array. It still needs a script or an uploaded recording: that either/or is checked at submit.
* **`example_config` is the smallest valid config.** It includes required keys that have defaults, so it can differ from `required`. File slots hold the placeholder `ast_00000000000000000000000000000000`: swap in real ids from [uploads](/api/uploads).
* **`count` max is 4** over the API.
* **Asset ids match `^ast_[0-9a-f]{32}$`.**
* **Read the descriptions.** File limits, per-file durations, step sizes, slot dependencies and group rules live there.

### Two file slot shapes [#two-file-slot-shapes]

Plain tools take an array of bare ids:

```json title="config_schema.properties.source_video, from auto_caption"
{
  "description": "Video. Up to 1 video asset id(s). Each file may run up to 600 seconds. Accepts an upload or anything this project already produced.",
  "minItems": 1,
  "maxItems": 1,
  "type": "array",
  "items": { "type": "string", "pattern": "^ast_[0-9a-f]{32}$" }
}
```

Image and video models take one strict object per file, with the role pinned and an alias you pick:

```json title="config_schema.properties.start_frame, from sora_2"
{
  "description": "Start Frame. Up to 1 image file(s), each as an asset id plus its own alias, with role \"start_frame\".",
  "maxItems": 1,
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "assetId": { "type": "string", "pattern": "^ast_[0-9a-f]{32}$" },
      "alias": { "type": "string", "pattern": "^(image|video|audio)[1-9]\\d*$" },
      "role": { "type": "string", "const": "start_frame" }
    },
    "required": ["assetId", "alias", "role"],
    "additionalProperties": false
  }
}
```

Aliases count from 1 per kind (`image1`, `image2`, `video1`). Don't reuse an alias across slots in one config. The config level `description` says this too.

### What the schema can't tell you [#what-the-schema-cant-tell-you]

* **Settings that depend on a file.** A start frame or reference image can change the valid `aspect_ratio`, `resolution` and `duration`. Enums show every value any case allows. Check a real config with `POST /estimates`. [Generations API](/api/generations).
* **Price.** None here. `POST /estimates` returns the number to base `max_credits` on.
* **REST names.** Descriptions name MCP tools (`list_voices`, `estimate_generation`). Over REST those are `GET /voices` and `POST /estimates`.

### Fields that are not config [#fields-that-are-not-config]

`actor_id`, `actor_image_asset_id`, `voice_id` and `approved_voice_generation_id` go on the **submit body**, not in `config`. Inside `config` they are unknown keys. [Generations API](/api/generations), [actors and voices](/api/library).

<Callout type="warn" title="actor_ultra has no aspect_ratio">
  Sending `aspect_ratio` is an unknown key. The shape comes from the actor. Script road: run `tts` first and pass its `generation_id` as `approved_voice_generation_id`. Audio road: fill `voice_audio` with your recording, no `tts` needed. [Talking actor guide](/guides/talking-actor).
</Callout>

## Errors [#errors]

An id you can't use adds `capability_status` to the error envelope.

| `capability_status` | `code`                   | HTTP | Meaning                                               |
| ------------------- | ------------------------ | ---- | ----------------------------------------------------- |
| `coming_soon`       | `capability_unavailable` | 404  | Listed, not live yet                                  |
| `retired`           | `capability_unavailable` | 404  | Withdrawn. Old generations still read fine            |
| `requires_plan`     | `required_plan`          | 403  | Your plan doesn't include it. Carries `required_plan` |
| `unknown`           | `capability_unavailable` | 404  | Never existed, misspelled, or not offered to agents   |

```json title="Response: 404"
{
  "error": {
    "ok": false,
    "code": "capability_unavailable",
    "message": "No such capability. Call list_capabilities for the current list.",
    "retryable": false,
    "credits_charged": 0,
    "capability_status": "unknown"
  }
}
```

None are retryable. Read the list again and pick an `available` row. All codes: [error codes](/reference/errors).
