
# Update an API

`PATCH /v1/apis/{api_id}` · permission `apis:write`

Updates an API with a JSON merge patch (RFC 7396, `application/merge-patch+json`): fields you send change, fields you omit stay. `If-Match` is required. `key` and `identifier` cannot change after creation, and the system API cannot be modified. A patch that changes nothing writes nothing and keeps the version.

## Request

| Parameter | In | Description |
|---|---|---|
| `api_id` | path | The API id. |
| `If-Match` | header | The `ETag` from the last read (required). |

Body: any of the writable fields below (all optional). A field cannot be removed with `null`.

| Field | Type | Required | Description |
|---|---|---|---|
| `key` | string | no | Stable configuration key, `^[a-z0-9_-]{1,64}$`. Immutable. |
| `identifier` | string | no | Token audience: an https URL (no query or fragment) or `urn:<nid>:<nss>`; stored in canonical form. Immutable. The `urn:idp:` namespace is reserved. |
| `name` | string | no | 1-128 characters. |
| `access_token_lifetime_s` | integer | no | 60-86400. Default 3600 (600 with `mcp_profile`). |
| `token_format` | string | no | `jwt` (default) or `opaque`. |
| `rbac_mode` | string | no | How roles and permissions reach the token: `claims` (default) adds `permissions` and `org_permissions`; `off` adds neither (`org_roles` is still added); `enforce` adds them and also limits the granted scopes to the user's effective permissions. |
| `allow_offline_access` | boolean | no | Default `true`. |
| `require_resource_indicator` | boolean | no | Default `false` (`true` with `mcp_profile`). |
| `dpop_required` | boolean | no | Default `false`. |
| `mcp_profile` | boolean | no | Requires an https `identifier` and `require_resource_indicator: true`. |
| `first_party_access` | string | no | `all` (default) or `granted_only`. |
| `introspection_applications` | array | no | Applications (`app_…`) that may introspect this API's tokens, at most 50, no duplicates. Only `web` and `m2m` applications of the same environment. The list is replaced as a whole; `[]` empties it, `null` is refused. |

### Introspection applications

The introspection endpoint (`POST <issuer>/oauth2/introspect`, RFC 7662) answers `active: true` for an access token only to two callers: the application the token was issued to, or an application in the `introspection_applications` list of the API named in the token's `aud`. Every other caller gets `active: false`, so APIs cannot read each other's tokens. Only confidential applications (`web`, `m2m`) of the same environment can be listed; the list is stored as a set and returned in id order. A suspended application stays on the list but cannot introspect. The system API's list cannot be written.

## Response

`200 OK` with the new `ETag`:

```json
{
  "id": "api_01h455vb4pex5vsknk084sn02q",
  "key": "orders",
  "identifier": "https://api.example.com/orders",
  "name": "Orders API",
  "is_system": false,
  "access_token_lifetime_s": 3600,
  "token_format": "jwt",
  "rbac_mode": "off",
  "allow_offline_access": true,
  "require_resource_indicator": false,
  "dpop_required": false,
  "mcp_profile": false,
  "first_party_access": "all",
  "introspection_applications": ["app_01h455vb4pex5vsknk084sn02r"],
  "created_at": "2026-10-05T12:00:00Z",
  "updated_at": "2026-10-05T12:00:00Z"
}
```

## Errors

| HTTP | code | When |
|---|---|---|
| 400 | `unknown_field` | The patch contains a read-only or unknown field. |
| 404 | `not_found` | No such API in this environment. |
| 409 | `conflict` | The API is the system API. |
| 412 | `precondition_failed` | `If-Match` does not match the current version. |
| 415 | `unsupported_media_type` | The content type is not `application/merge-patch+json`. |
| 422 | `validation_failed` | A value is invalid, a field was set to `null`, or `key` / `identifier` was changed. |
| 422 | `validation_failed` | An `introspection_applications` item does not exist in this environment (`unknown_application`), is not confidential (`not_confidential`), is a duplicate (`duplicate`) or is not an `app_…` id (`invalid_value`); the pointer names the index in the request. |
| 428 | `precondition_required` | `If-Match` is missing. |

## Example

```bash
curl -X PATCH https://api.eu.keenauth.com/v1/apis/api_01h455vb4pex5vsknk084sn02q \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/merge-patch+json" \
  -H 'If-Match: "1"' \
  -d '{"access_token_lifetime_s":900}'
```
