# Contract stability (/reference/stability)



An agent does not read a form. It reads the JSON Schema a capability publishes, the tool list and the REST paths. This page says which parts of that are a promise and which are free to change, so you know what to hard-code and what to read at run time.

## Contract: build on it [#contract-build-on-it]

A change to any of these can break a working client, so none of them changes without the process below.

| Contract                                                                       | Where you read it                                 |
| ------------------------------------------------------------------------------ | ------------------------------------------------- |
| Capability ids (`actor_ultra`, `analyze_media`)                                | `list_capabilities`, `GET /capabilities`          |
| Config property names, JSON types, `required`, `default`                       | `get_capability_schema`, `GET /capabilities/{id}` |
| The set of `enum` values, `const`, `pattern`                                   | the same schema                                   |
| Bounds: `minimum`, `maximum`, `minLength`, `maxLength`, `minItems`, `maxItems` | the same schema                                   |
| Strictness (`additionalProperties: false`): an unknown key is refused          | every write body, and the estimate                |
| The `deprecated` keyword on an old field name                                  | the same schema                                   |
| `output_kind`, `category` and `status` values                                  | capability list rows                              |
| Error `code` values, their `retryable` verdicts, `limit.bound_by` values       | [Error codes](/reference/errors)                  |
| MCP tool names and their arguments                                             | `tools/list` on `https://mcp.riffads.com`         |
| REST paths, methods, request and response field names                          | `https://app.riffads.com/api/v1/openapi.json`     |
| CLI command names, flags, `--json` fields and exit codes                       | [CLI commands](/cli/commands)                     |

## Detail: may change at any time [#detail-may-change-at-any-time]

Never parse or match on these.

* Every `description`, label, help text and unit note in a schema.
* Display names of capabilities, presets, actors and voices. An operator edits them.
* The order of properties, of `required` and of `enum` values.
* `example_config` values. The example always validates, but the values can change.
* The wording of `message`, `next_action`, `charge_summary` and `note`. Show them to a person; branch on `code`, `retryable` and the typed fields.

## Additive changes ship without warning [#additive-changes-ship-without-warning]

These can land in any release. A client written to the rules below never notices.

| Change                                        | What your client should do                                   |
| --------------------------------------------- | ------------------------------------------------------------ |
| A new capability                              | Read the list at run time. Never hard-code the full catalog. |
| A new optional config field                   | Nothing. Leave it out and the default applies.               |
| A new `enum` value or a wider bound           | Nothing. Your old values still validate.                     |
| A new MCP tool, REST route or query parameter | Nothing. Old ones keep their names.                          |
| A new field in a response                     | Ignore fields you do not know. Never fail on an extra key.   |

The request side is the opposite: RiffAds refuses an unknown key rather than ignoring it, so a typo fails loudly instead of being dropped. An estimate is checked exactly like a submit, so estimating a config is a free way to test it against the live contract.

## Breaking changes [#breaking-changes]

| Change                                                 | What happens                                                                                                                                                                              |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A config field is renamed                              | The old name keeps working for one minor version, marked `deprecated`. See below.                                                                                                         |
| A field or `enum` value is removed, or a bound narrows | Only when a provider forces it, with a minor version bump. The refusal is `invalid_config` naming the field.                                                                              |
| A capability is removed                                | It is retired: it leaves the capability list, and a request that names it is refused with `capability_unavailable` and `capability_status: "retired"`, so the reason is machine readable. |
| A tool, REST field or error code would be renamed      | It is not. A new one is added beside the old one.                                                                                                                                         |

## The deprecation window [#the-deprecation-window]

An old field name is accepted for **one minor version** of the MCP server version (`1.1.x` accepts a name renamed in `1.1.0`, and `1.2.0` stops). While it is accepted:

* The schema lists the old name as an optional property with `"deprecated": true`, no default, and a description that names the new field and the version that stops accepting it:

  ```json title="A deprecated property in config_schema"
  "size": {
    "type": "string",
    "deprecated": true,
    "description": "Deprecated: renamed to image_size. Send image_size instead. size is accepted until version 1.2.0."
  }
  ```

* A config that uses the old name is priced, submitted and deduplicated exactly as if it used the new one.

* A config that sends both names is refused with `invalid_config` naming both. Send only the new one.

When you see `deprecated: true`, switch to the new name in the same release of your client. There is no reason to wait for the window to close.

<Callout type="info" title="The field names above are an illustration">
  No capability has a deprecated field today. The shape is what one will look like.
</Callout>

## Versions [#versions]

This page describes version **1.1.0**. One number covers the MCP server and the REST document. The REST path also carries a major version of its own (`/api/v1`).

| Where       | How to read it                                                                 |
| ----------- | ------------------------------------------------------------------------------ |
| MCP         | `serverInfo.version` in the `initialize` answer from `https://mcp.riffads.com` |
| REST        | `info.version` in `https://app.riffads.com/api/v1/openapi.json`                |
| Server card | `version` in `https://riffads.com/.well-known/mcp/server-card.json`            |
| CLI         | `riffads --version`. The CLI has its own version number.                       |

A minor bump means something was added, or a rename started its window.

## How to notice a change [#how-to-notice-a-change]

* **Watch the version.** Compare the number above between runs. A new minor version is the moment to diff.
* **Diff the OpenAPI file.** It is built from the same schemas that check requests, so a changed field shows up there first.
* **Scan for `deprecated`.** Walk `get_capability_schema` for the capabilities you use and look for `"deprecated": true`.
* **Watch `status`.** A capability you depend on that leaves the list, or answers `capability_unavailable` with a `capability_status`, has been retired or paused. `requires_plan` means the workspace plan changed, not the contract.
* **Treat `invalid_config` as a signal.** A config that worked yesterday and is refused today names the field that moved. Read the schema again rather than retrying.
