openapi: 3.2.0 info: title: KnoxCall Client Management API Keys API version: '1.0' description: 'The KnoxCall Client Management API provides programmatic access to manage your API gateway resources including routes, secrets, clients, environments, webhooks, and API keys. ## Authentication All requests must include an OAuth 2.1 access token **or** a legacy API key. The recommended path for new integrations is the first-party SDKs, which mint, cache, and refresh OAuth tokens for you (start with `knoxcall login` on a developer machine, or `KNOXCALL_CLIENT_ID` / `KNOXCALL_CLIENT_SECRET` in CI). You can authenticate using any of: - **Authorization header (OAuth 2.1 bearer token — recommended)**: `Authorization: Bearer ` - **Authorization header (legacy API key)**: `Authorization: Bearer tk_xxx_yyy` - **x-api-key header (legacy API key)**: `x-api-key: tk_xxx_yyy` OAuth 2.1 access tokens (`kc_` prefix) are minted at the root-host token endpoint `https://api.knoxcall.com/oauth/token` (not under `/v1`) using the `client_credentials` or `authorization_code` grant. See the `OAuth2` security scheme and the `/oauth/*` endpoints. Some resources that are managed exclusively through OAuth reject API-key auth with a `403 use_oauth` error. Long-lived API keys remain fully supported. They come in three types: - **Standard keys** (`tk_` prefix) are used on the production API (`api.knoxcall.com`). - **Test keys** (`tk_` prefix, type=test) are used on the sandbox API (`sandbox.knoxcall.com`). - **Enterprise Access Keys** (`AKE` prefix) follow the format `AKE[16chars]:[40chars]`. Using a test key on the production API (or vice versa) will return a `403 wrong_key_type` error. ## Request IDs Every response — success or error — includes an `X-Request-Id` response header (a UUID). Its value is identical to the `request_id` field in the response body (`meta.request_id` on success, `error.request_id` on error). Quote it when contacting support. ## API Versioning The API is versioned on two axes: - **Major version — the `/v1` path prefix.** Breaking changes ship under a new major (`/v2`). `/v1` never receives a breaking change. - **Minor version — the `KnoxCall-Version` header (dated, `YYYY-MM-DD`).** Send `KnoxCall-Version: 2026-08-05` to pin the behaviour of a specific dated release. Omit the header to get the newest minor version. Every response echoes the resolved version back in a `KnoxCall-Version` response header. Within a major version, **additive changes are not breaking** and may appear without a new dated version: new endpoints, new optional request fields, new response fields, new `error.type` values, and new enum members. Clients must tolerate unknown response fields and unknown `error.type` values (see the `ErrorResponse` schema). An **unknown or malformed** `KnoxCall-Version` value returns `400` with `error.type: invalid_api_version`. ## Rate Limiting API keys can have optional rate limits configured. When a limit is configured, the following headers are emitted on **every** response (not only `429`s): - `X-RateLimit-Limit` -- maximum requests allowed per window - `X-RateLimit-Remaining` -- remaining requests in the current window - `X-RateLimit-Reset` -- epoch seconds at which the window resets When the limit is exceeded the API returns `429` (`rate_limit_exceeded` / `rate_limited`) and additionally sets `Retry-After` (seconds until the window resets). ## DPoP sender-constrained tokens, and the server nonce A token minted with a `cnf.jkt` binding (DPoP, RFC 9449) requires a `DPoP` proof header on every request, signed over the request''s method and canonical public URL. **Every response to a request that carried a `DPoP` header also carries a `DPoP-Nonce` response header** -- mints, refusals and successful calls alike, on `POST /oauth/token`, `/v1`, the tenant data plane, the self-hosted proxy and the AI gateway. The value is stateless and changes every minute; the server accepts the current and the previous minute''s, so a nonce is good for 60-120 seconds.' contact: name: KnoxCall Support url: https://knoxcall.com email: support@knoxcall.com license: name: Proprietary url: https://knoxcall.com/terms servers: - url: https://api.knoxcall.com/v1 description: Production - url: https://sandbox.knoxcall.com/v1 description: Sandbox (test keys only) security: - OAuth2: [] - BearerAuth: [] - ApiKeyAuth: [] tags: - name: API Keys description: Create and revoke API keys for programmatic access. paths: /api-keys: get: operationId: listApiKeys summary: List API keys description: 'Returns a paginated list of API keys. Keys are masked -- the full key value is only returned at creation time and cannot be retrieved again. Every row carries `expires_at`, and it is worth reading rather than assuming: a key created without an explicit expiry choice is on a 90-day clock, and `null` is the only value that means "never expires".' tags: - API Keys parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' responses: '200': description: A paginated list of API keys. content: application/json: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/ApiKey' meta: $ref: '#/components/schemas/PaginationMeta' '401': $ref: '#/components/responses/Unauthorized' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' post: operationId: createApiKey summary: Create an API key description: 'Creates a new API key. The full key value is included in the response and will **never be shown again**. Store it securely immediately. The key type (standard or test) is determined by which API host you create it on -- production creates standard keys, sandbox creates test keys. **Every key expires.** Choose a lifetime with at most one of `expires_in_days`, `expires_at` or `never_expires`; send none of them and the key gets **90 days**. The resolved instant comes back as `expires_at` on the response and on every later read, and it is enforced on authentication — so a long-lived integration needs either a rotation schedule or an explicit `never_expires: true`.' tags: - API Keys requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateApiKeyRequest' examples: plain: summary: A key with no roles (default-denied on every policy-gated endpoint) value: name: CI/CD Pipeline rate_limit_requests: 1000 rate_limit_window_sec: 60 terraform: summary: A Terraform credential — assign the seeded `Key — Infrastructure` role value: name: terraform-prod role_ids: - b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d short_lived: summary: A 30-day key for a one-off migration value: name: data-migration expires_in_days: 30 never_expires: summary: A key with no expiry — valid until revoked by hand value: name: legacy-appliance never_expires: true responses: '200': description: The created API key with the full key value. content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/ApiKeyCreated' meta: $ref: '#/components/schemas/RequestMeta' '400': description: 'The request body failed validation (`validation_error`), or the expiry choice could not be resolved. `conflicting_expiry_fields` and `invalid_expiry` are specific to this endpoint: they are NOT flattened into `validation_error`, because "you sent two of them" and "the one you sent is out of range" need different fixes and a caller automating key rotation has to tell them apart. ' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: validation_error: summary: Request body failed validation value: error: type: validation_error message: name is required. request_id: 550e8400-e29b-41d4-a716-446655440000 conflicting_expiry_fields: summary: More than one expiry field sent value: error: type: conflicting_expiry_fields message: Send at most one of expires_in_days, expires_at or never_expires. request_id: 550e8400-e29b-41d4-a716-446655440000 invalid_expiry: summary: The expiry is out of range, or in the past value: error: type: invalid_expiry message: expires_in_days must be an integer between 1 and 365. request_id: 550e8400-e29b-41d4-a716-446655440000 '401': $ref: '#/components/responses/Unauthorized' '403': description: 'The privilege-escalation ceiling refused the request: a role in `role_ids` grants a permission the calling key does not itself hold. The message names the offending grant verbatim so it can be removed or matched. A key can never mint a key more privileged than itself. **A scope-narrowed OAuth token cannot mint a key at all**, with or without `role_ids`, and also gets `403 privilege_escalation`. An API key carries no scope narrowing (on the data plane it can invoke every route in its mode), so any key such a token minted would exceed the token''s own grant. Mint keys with an API key, or with an OAuth token whose scope is not narrowed (no scope requested, or `*`). Also returned as ordinary `forbidden` when the calling key lacks `api_key:create`. ' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: privilege_escalation: summary: The requested role exceeds the caller's own grants value: error: type: privilege_escalation message: 'This API key cannot grant a permission it does not itself hold. Refused grant from role "Key — Infrastructure": {"resource_type":"vault","actions":["create","read","update","delete","tokenize","detokenize"],"effect":"allow"} — no rule in your own policy set grants it.' request_id: 550e8400-e29b-41d4-a716-446655440000 forbidden: summary: The calling key lacks api_key:create value: error: type: forbidden message: 'Permission denied: create on api_key — no matching rule (default deny)' request_id: 550e8400-e29b-41d4-a716-446655440000 '404': description: 'One or more `role_ids` do not exist in this tenant. Another tenant''s role UUID is indistinguishable from a nonexistent one, by design. ' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: type: not_found message: 'Role not found in this tenant: b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d.' request_id: 550e8400-e29b-41d4-a716-446655440000 '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' /api-keys/{id}: parameters: - name: id in: path required: true schema: type: string description: The API key UUID or key_id (e.g. `tk_abc`). example: tk_a1b2c3 delete: operationId: revokeApiKey summary: Revoke an API key description: 'Revokes an API key, permanently disabling it. A revoked key cannot be reactivated. You cannot revoke the key currently being used to authenticate this request.' tags: - API Keys responses: '200': description: API key revoked. content: application/json: schema: type: object properties: data: type: object properties: revoked: type: boolean example: true meta: $ref: '#/components/schemas/RequestMeta' '400': description: Cannot revoke the currently-used API key. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: type: validation_error message: Cannot revoke the API key currently in use. request_id: 550e8400-e29b-41d4-a716-446655440000 '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /roles: get: operationId: listRoles summary: List permission roles description: 'Returns the tenant''s permission roles so a machine can discover the id to pass in `role_ids` when creating an API key. Read-only: there is no create, update or delete on `/v1`. Only the SEEDED key roles (`seeded: true`) are accepted in `role_ids`. Custom roles are being retired in favour of per-key permissions: none can be created, and this list still returns any a tenant already has (`seeded: false`), which `POST /v1/api-keys` refuses with `403 forbidden`. Filter on `seeded`. The seeded machine roles (`applies_to: [api_key]`) are `Key — Invoke`, `Key — Read-only`, `Key — Editor` and `Key — Infrastructure`. The last is the one to attach to a Terraform or CI credential — it covers the whole provisioning surface and deliberately excludes secret reveal, custody transitions, workload-identity bindings and the ephemeral proxy. Requires the `role:list` permission, which all four seeded machine roles carry.' tags: - API Keys parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - name: subject_kind in: query required: false schema: type: string enum: - api_key - user description: 'Return only roles whose `applies_to` includes this audience. `api_key` also returns a tenant''s existing custom key roles (`seeded: false`), which `role_ids` refuses; the roles assignable to a key are the `seeded: true` rows.' example: api_key responses: '200': description: A paginated list of roles. content: application/json: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/Role' meta: $ref: '#/components/schemas/PaginationMeta' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' components: schemas: Role: type: object description: 'A named bundle of permission policies. Read-only on `/v1`, and exposed so a machine can discover the id to pass in `role_ids`. The seeded roles (`seeded: true`) are platform-managed and cannot be renamed or recomposed; custom roles are being retired in favour of fixed roles and per-key permissions, so no new custom role can be created and only a seeded key role can be given to a new key. The rules a role grants are deliberately NOT exposed — enumerating them would map the tenant''s authorization surface to every machine credential in it. ' properties: id: type: string format: uuid example: b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d name: type: string description: Display name. The seeded machine roles are `Key — Invoke`, `Key — Read-only`, `Key — Editor` and `Key — Infrastructure`. example: Key — Infrastructure description: type: - string - 'null' example: For Terraform and other infrastructure-as-code runners. applies_to: type: array description: Which subject kinds this role may be assigned to. `role_ids` accepts a role only when this includes `api_key` AND `seeded` is true. items: type: string enum: - user - api_key example: - api_key is_default: type: boolean description: Whether the admin UI prefills this role when creating a new key. example: false seeded: type: boolean description: True for roles the platform creates and maintains (they cannot be deleted, renamed or recomposed). Only a seeded key role is accepted in `role_ids`; custom roles (`false`) are being retired. example: true ApiKey: type: object description: An API key (masked -- full key is only returned at creation time). properties: id: type: string format: uuid description: Internal UUID of the key record. example: f0a1b2c3-d4e5-6f7a-8b9c-0d1e2f3a4b5c key_id: type: string description: Public key identifier prefix (e.g. `tk_a1b2c3`). example: tk_a1b2c3 key_prefix: type: string description: First few characters of the key for identification. example: tk_a1b2c3_xY key_type: type: string enum: - standard - test - access_key description: 'Type of the API key. `standard` keys work on the production API, `test` keys work on the sandbox API, `access_key` keys work on both. ' example: standard name: type: string description: Human-readable name for the key. example: CI/CD Pipeline active: type: boolean description: Whether the key is active (false if revoked). example: true created_at: type: string format: date-time example: '2025-03-15T10:30:00.000Z' last_used_at: type: - string - 'null' format: date-time description: Timestamp of the last time this key was used. example: '2025-03-16T14:22:00.000Z' expires_at: type: - string - 'null' format: date-time description: 'When this key stops authenticating. `null` means it **never expires** — it stays valid until someone revokes it by hand. Expiry is enforced on every authentication path, so a key past this instant is `401` everywhere, with no way to extend it: an expired key is replaced, not renewed. **A key created without an explicit expiry choice gets 90 days.** That default is applied when the key is minted, so this field is the only place it is visible — read it on every key, including ones you did not create. ' example: '2025-06-13T10:30:00.000Z' rate_limit_requests: type: - integer - 'null' description: Maximum requests allowed per rate limit window. example: 1000 rate_limit_window_sec: type: - integer - 'null' description: Rate limit window duration in seconds. example: 60 RequestMeta: type: object description: Metadata included with every API response. properties: request_id: type: string format: uuid description: Unique identifier for this API request, useful for support and debugging. example: 550e8400-e29b-41d4-a716-446655440000 required: - request_id PaginationMeta: type: object description: Pagination metadata for list endpoints. properties: total: type: integer description: Total number of records matching the query. example: 42 page: type: integer description: Current page number. example: 1 per_page: type: integer description: Number of records per page. example: 20 total_pages: type: integer description: Total number of pages, capped at the deepest page the 100,000-row offset ceiling allows -- `min(ceil(total / per_page), floor(100000 / per_page) + 1)`. When it is below `ceil(total / per_page)` the result set is larger than pagination can reach; narrow it with a filter. example: 3 request_id: type: string format: uuid description: Unique identifier for this API request. example: 550e8400-e29b-41d4-a716-446655440000 required: - total - page - per_page - total_pages - request_id ErrorResponse: type: object description: Standard error response envelope. properties: error: type: object properties: type: type: string description: 'Machine-readable error type. The set is OPEN — endpoints define specific types beyond the common ones below, so clients should branch on the types they know and fall back to the HTTP status class for anything unrecognized. Common values: authentication_required, invalid_api_key, wrong_key_type, subscription_inactive, rate_limit_exceeded, validation_error, forbidden, not_found, method_not_allowed (405, carries Allow), conflict, in_use, plan_limit, plan_feature, internal_error, dependency_unavailable (503, carries Retry-After), invalid_request, invalid_definition, not_cancellable, invalid_idempotency_key, idempotency_key_reuse, request_in_progress, system_client_protected, use_oauth, use_dpop_nonce (401 on /v1 and the data plane, 400 on POST /oauth/token; see "DPoP sender-constrained tokens").' example: validation_error message: type: string description: Human-readable error message. example: name and target_base_url are required. request_id: type: string format: uuid description: Unique identifier for this request. example: 550e8400-e29b-41d4-a716-446655440000 required: - type - message - request_id required: - error CreateApiKeyRequest: type: object required: - name properties: name: type: string description: Human-readable name to identify this key. example: CI/CD Pipeline rate_limit_requests: type: integer minimum: 1 description: Maximum requests per rate limit window. Leave empty for no rate limit. example: 1000 rate_limit_window_sec: type: integer minimum: 1 description: Rate limit window in seconds. Required if `rate_limit_requests` is set. example: 60 expires_in_days: type: integer minimum: 1 maximum: 365 description: 'Lifetime in days, counted from the moment the key is minted. **At most one** of `expires_in_days`, `expires_at` and `never_expires` may be present; two or more is `400 conflicting_expiry_fields`. A value outside 1-365 is `400 invalid_expiry`. **Omitting all three is not a way to opt out of expiry** — the key gets the default 90 days. To create a key that never expires you must say so, with `never_expires: true`. ' example: 30 expires_at: type: string format: date-time description: 'Exact expiry instant, ISO 8601. Must be in the future and no more than 365 days from now, else `400 invalid_expiry`. Mutually exclusive with `expires_in_days` and `never_expires` (`400 conflicting_expiry_fields`). Use this when the key''s life is pinned to a date you already know — a contract end, an audit window — rather than to a duration. ' example: '2027-01-06T00:00:00.000Z' never_expires: type: boolean description: 'Send `true` for a key with **no expiry**: the response and every later read carry `expires_at: null`, and the key stays valid until it is revoked by hand. A key that never stops working also never stops working for whoever finds it in an old CI secret or a container image, so this is a deliberate choice and not a default. Mutually exclusive with `expires_in_days` and `expires_at` (`400 conflicting_expiry_fields`). `false` is refused with `400 invalid_expiry` rather than quietly meaning "use the default" — a body that mentions expiry at all is making a statement about it, and answering that statement with a silent 90-day clock is the guess this field exists to remove. ' example: true role_ids: type: array maxItems: 20 description: 'Permission roles to attach to the new key, assigned in the SAME transaction as the key itself. Discover ids with `GET /v1/roles?subject_kind=api_key`. Omit (or send `[]`) to create a key with no role — such a key is **default-denied on every policy-gated endpoint**, which is rarely what you want. For an infrastructure-as-code runner, attach the seeded `Key — Infrastructure` role. Every id must (a) belong to your tenant, else `404`; (b) declare the `api_key` audience in `applies_to`, else `400`; (c) grant nothing the calling key does not itself hold, else `403 privilege_escalation`; and (d) be one of the SEEDED key roles (`Key — Invoke`, `Key — Read-only`, `Key — Editor`, `Key — Infrastructure`), else `403 forbidden`. A key can never mint a key more privileged than itself. Custom roles are being retired in favour of per-key permissions: a custom role a key already holds keeps working, but no new key can be given one. ' items: type: string format: uuid example: - b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d ApiKeyCreated: type: object description: 'API key creation response. The `api_key` field contains the full key and is **only returned once** at creation time. Store it securely. ' properties: id: type: string format: uuid description: 'Internal UUID of the key record — the value `DELETE /v1/api-keys/{id}` and role assignments reference. Added 2026-08-09; previously the create response carried no UUID at all, so a caller had to list keys back to find it. ' example: f0a1b2c3-d4e5-6f7a-8b9c-0d1e2f3a4b5c role_ids: type: array description: 'The role UUIDs actually attached, in the same transaction as the key. Empty when none were requested — which means the key is default-denied on every policy-gated endpoint. ' items: type: string format: uuid example: - b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d key_id: type: string description: Public key identifier prefix. example: tk_a1b2c3 api_key: type: string description: 'The full API key. **This is the only time it will be shown.** Store it in a secure location immediately. ' example: tk_a1b2c3_xYz9AbCdEfGhIjKlMnOpQrStUvWx key_prefix: type: string description: First characters of the key for identification. example: tk_a1b2c3_xY key_type: type: string enum: - standard - test description: Key type determined by which API host the key was created on. example: standard name: type: string description: Name you assigned to the key. example: CI/CD Pipeline expires_at: type: - string - 'null' format: date-time description: 'When the new key stops authenticating, resolved from whichever of `expires_in_days` / `expires_at` / `never_expires` you sent. `null` means it never expires. **Sending none of the three puts this 90 days out**, so read this field back even when you did not ask for an expiry — it is the value that was actually applied, not an echo of your request. ' example: '2025-06-13T10:30:00.000Z' message: type: string description: Reminder to store the key securely. example: Store this API key securely. It will not be shown again. responses: Forbidden: description: 'The key is authenticated but not authorized for this action (missing scope), a plan limit was reached, or the resource must be managed via OAuth (`use_oauth`) / is a protected system client (`system_client_protected`). ' headers: X-Request-Id: $ref: '#/components/headers/X-Request-Id' KnoxCall-Version: $ref: '#/components/headers/KnoxCall-Version' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: forbidden: summary: Missing scope value: error: type: forbidden message: This key is not authorized to list AI gateways. request_id: 550e8400-e29b-41d4-a716-446655440000 use_oauth: summary: Resource is OAuth-only value: error: type: use_oauth message: This resource must be managed with an OAuth 2.1 access token. request_id: 550e8400-e29b-41d4-a716-446655440000 system_client_protected: summary: Protected system client value: error: type: system_client_protected message: This client is managed by the platform and cannot be modified. request_id: 550e8400-e29b-41d4-a716-446655440000 NotFound: description: The requested resource was not found. headers: X-Request-Id: $ref: '#/components/headers/X-Request-Id' KnoxCall-Version: $ref: '#/components/headers/KnoxCall-Version' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: type: not_found message: Route not found. request_id: 550e8400-e29b-41d4-a716-446655440000 InternalError: description: An unexpected server error occurred. headers: X-Request-Id: $ref: '#/components/headers/X-Request-Id' KnoxCall-Version: $ref: '#/components/headers/KnoxCall-Version' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: type: internal_error message: An unexpected error occurred. request_id: 550e8400-e29b-41d4-a716-446655440000 Unauthorized: description: Authentication is required or the provided API key is invalid. headers: X-Request-Id: $ref: '#/components/headers/X-Request-Id' KnoxCall-Version: $ref: '#/components/headers/KnoxCall-Version' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: missing_key: summary: No API key provided value: error: type: authentication_required message: 'API key is required. Use Authorization: Bearer or x-api-key header.' request_id: 550e8400-e29b-41d4-a716-446655440000 invalid_key: summary: Invalid API key value: error: type: invalid_api_key message: Invalid API key. request_id: 550e8400-e29b-41d4-a716-446655440000 ValidationError: description: The request failed validation (always `400 validation_error`). A malformed idempotency key returns `400 invalid_idempotency_key`. headers: X-Request-Id: $ref: '#/components/headers/X-Request-Id' KnoxCall-Version: $ref: '#/components/headers/KnoxCall-Version' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: validation_error: summary: Request body failed validation value: error: type: validation_error message: name and target_base_url are required. request_id: 550e8400-e29b-41d4-a716-446655440000 invalid_idempotency_key: summary: Malformed idempotency key value: error: type: invalid_idempotency_key message: X-Idempotency-Key must be a non-empty string of at most 255 characters. request_id: 550e8400-e29b-41d4-a716-446655440000 RateLimited: description: 'Too many requests. Retry after the indicated time. The `X-RateLimit-*` headers are also present on non-429 responses whenever a limit is configured. ' headers: X-Request-Id: $ref: '#/components/headers/X-Request-Id' KnoxCall-Version: $ref: '#/components/headers/KnoxCall-Version' X-RateLimit-Limit: $ref: '#/components/headers/X-RateLimit-Limit' X-RateLimit-Remaining: $ref: '#/components/headers/X-RateLimit-Remaining' X-RateLimit-Reset: $ref: '#/components/headers/X-RateLimit-Reset' Retry-After: $ref: '#/components/headers/Retry-After' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: type: rate_limit_exceeded message: API rate limit exceeded. Please retry later. request_id: 550e8400-e29b-41d4-a716-446655440000 headers: X-RateLimit-Remaining: description: 'Remaining requests in the current window. Emitted on every response whenever a rate limit is configured for the authenticated key. ' schema: type: integer example: 999 KnoxCall-Version: description: 'The dated API version (`YYYY-MM-DD`) that served this response. Equals the value of the `KnoxCall-Version` request header when supplied, otherwise the newest version. ' schema: type: string pattern: ^\d{4}-\d{2}-\d{2}$ example: '2026-08-05' X-Request-Id: description: 'Unique identifier for this request (UUID). Present on **every** response and equal to the `request_id` field in the response body. Quote it when contacting support. ' schema: type: string format: uuid example: 550e8400-e29b-41d4-a716-446655440000 Retry-After: description: Seconds to wait before retrying. Set on `429` responses. schema: type: integer example: 30 X-RateLimit-Reset: description: 'Epoch seconds at which the current rate-limit window resets. Emitted on every response whenever a rate limit is configured for the authenticated key. ' schema: type: integer example: 1735689600 X-RateLimit-Limit: description: 'Maximum requests allowed per window. Emitted on every response whenever a rate limit is configured for the authenticated key. ' schema: type: integer example: 1000 parameters: PageParam: name: page in: query schema: type: integer minimum: 1 default: 1 description: Page number for pagination. Clamped server-side so the derived offset never exceeds 100,000 rows -- the effective maximum is `floor(100000 / per_page) + 1`, i.e. 1,001 at `per_page=100`. A larger value returns that last page, and `meta.page` reports it. PerPageParam: name: per_page in: query schema: type: integer minimum: 1 maximum: 100 default: 20 description: Number of items per page (max 100). securitySchemes: OAuth2: type: oauth2 description: 'OAuth 2.1 authentication — recommended for new integrations. Access tokens (`kc_` prefix) are minted at the root-host token endpoint `https://api.knoxcall.com/oauth/token` and passed as `Authorization: Bearer `. Public clients must use PKCE with the `authorization_code` grant; confidential clients may use `client_credentials`. The first-party SDKs and the `knoxcall login` CLI handle token minting, caching, refresh, and DPoP for you. ' flows: clientCredentials: tokenUrl: https://api.knoxcall.com/oauth/token refreshUrl: https://api.knoxcall.com/oauth/token scopes: routes:read: Read routes. routes:write: Create, update, and delete routes. secrets:read: Read secret metadata. Does NOT include retrieving a live OAuth2 access token. secrets:oauth_token: Retrieve a live upstream OAuth2 access token (GET /v1/secrets/{id}/oauth2/token). Distinct from secrets:read because the call refreshes the credential at the provider. secrets:write: Create, update, and delete secrets. vaults:detokenize: Turn a vault token back into its original value. Required by GET /v1/vaults/{vault}/tokens/{token}, by a token template reference in a /v1/proxy body, and by creating a `detokenize` route action. Distinct from vaults:read, which lists vaults and token metadata and never returns a value. transit:decrypt: Open a portable `kc:` ciphertext with a tenant transit key. Required by creating a `decrypt` route action, which stores a standing instruction to open every matching field on every later request through that route. It is the same capability name the policy engine uses for POST /v1/decrypt. No routes scope satisfies it — managing a route is not permission to open the data flowing through it. proxy:invoke: Call the one-shot ephemeral proxy (any method on /v1/proxy). Required for EVERY verb — the proxy is one capability behind one handler, and a bodyless GET spends an escrowed credential exactly as a POST does. Neither proxy:read nor proxy:write satisfies it. clients:read: Read clients. clients:write: Manage clients. webhooks:write: Manage webhooks. workflows:write: Manage and execute workflows. '*:*': Full access (all scopes). authorizationCode: authorizationUrl: https://api.knoxcall.com/oauth/authorize tokenUrl: https://api.knoxcall.com/oauth/token refreshUrl: https://api.knoxcall.com/oauth/token scopes: routes:read: Read routes. routes:write: Create, update, and delete routes. secrets:read: Read secret metadata. Does NOT include retrieving a live OAuth2 access token. secrets:oauth_token: Retrieve a live upstream OAuth2 access token (GET /v1/secrets/{id}/oauth2/token). Distinct from secrets:read because the call refreshes the credential at the provider. secrets:write: Create, update, and delete secrets. vaults:detokenize: Turn a vault token back into its original value. Required by GET /v1/vaults/{vault}/tokens/{token}, by a token template reference in a /v1/proxy body, and by creating a `detokenize` route action. Distinct from vaults:read, which lists vaults and token metadata and never returns a value. transit:decrypt: Open a portable `kc:` ciphertext with a tenant transit key. Required by creating a `decrypt` route action, which stores a standing instruction to open every matching field on every later request through that route. It is the same capability name the policy engine uses for POST /v1/decrypt. No routes scope satisfies it — managing a route is not permission to open the data flowing through it. proxy:invoke: Call the one-shot ephemeral proxy (any method on /v1/proxy). Required for EVERY verb — the proxy is one capability behind one handler, and a bodyless GET spends an escrowed credential exactly as a POST does. Neither proxy:read nor proxy:write satisfies it. clients:read: Read clients. clients:write: Manage clients. webhooks:write: Manage webhooks. workflows:write: Manage and execute workflows. '*:*': Full access (all scopes). PhantomToken: type: http scheme: bearer description: "The AI **data plane** credential — a `kp_`-prefixed capability token, not a\nManagement API credential. It is minted per agent (or per MCP resource) at\n`POST /v1/ai-gateway/agents/{agentId}/tokens`, or exchanged from a CI workload's\nOIDC token at `POST /v1/oauth/token`, and it is the ONLY credential `/v1/ai/*`\naccepts: an OAuth `kc_` access token or a `tk_` API key is refused there, and a\n`kp_` token is refused on `api.knoxcall.com`.\n\nThree header spellings are accepted, so an existing provider SDK can be pointed at\nthe agent URL without rewriting its auth:\n\n Authorization: Bearer kp_live_a_...\n x-api-key: kp_live_a_...\n x-knox-ai-key: kp_live_a_...\n\n`kp_live_` is refused on the sandbox host and `kp_test_` on the Live host — the env\nsegment is enforced, not cosmetic. DPoP-bound tokens additionally require a valid\n`DPoP` proof header on every request, signed over the canonical public URL rather\nthan a rewritten `Host`.\n\nA rejected credential answers `401`/`403` in the data-plane error envelope with the\ncause in `code` and in the `X-Auth-Failure` response header.\n" BearerAuth: type: http scheme: bearer description: 'Legacy long-lived API key authentication via the Authorization header. Pass your API key as: `Authorization: Bearer tk_xxx_yyy`. Standard keys (`tk_` prefix) are accepted on `api.knoxcall.com`. Test keys (type=test) are accepted on `sandbox.knoxcall.com`. Enterprise Access Keys (`AKE` prefix) are accepted on both. Fully supported, but prefer the `OAuth2` scheme (or the SDKs, which handle OAuth for you) for new integrations. ' ApiKeyAuth: type: apiKey in: header name: x-api-key description: 'Legacy API key authentication via the `x-api-key` header. This is an alternative to Bearer token authentication for clients that cannot set the `Authorization` header. Prefer the `OAuth2` scheme for new integrations. ' CapabilityTokenAuth: type: http scheme: bearer description: 'Single-use client capability token (`kct_` prefix), passed as `Authorization: Bearer kct_...`. Minted server-side via `POST /client-tokens` and bound either to one exact payload + action (`decrypt`, `detokenize`) or to one vault + a closed set of page origins (`tokenize`). This is NOT an API key — it authenticates only the `POST /client/decrypt`, `POST /client/detokenize` and `POST /client/tokenize` consume endpoints and is invalidated after its first use. '