API Clients Management API
Overview
An API client is a machine credential owned by a consumer or an application — never a subject in its own right. API Clients covers the concept, the Manager screen, and the client-secret storage modes; this page documents the Management API surface for the same object.
Every address on this page is under /apiops/projects/{projectName} and follows the shared Common Contract: the stable error envelope, correlation IDs, bounded pagination, and Idempotency-Key on every create. A caller outside the project's scope always gets 404 resourceNotFound, never 403. API clients are project-owned, so the admin (platform) project has no clients of its own. The one exception is the client of a PORTAL application, which is filed under the administration project: see Administration path.
References on this surface are portable. A client is addressed by its apiClientKey — a public, installation-wide identifier, distinct from the Mongo id and from the gateway-facing clientId a token request presents. Its owner travels as {type, key}: type is CONSUMER or APPLICATION, and key is that owner's own public name — a consumer's username or an application's name — the same reference a limit assignment's subject.key already uses, so one client's owner.key matches its owner's rows on the sibling surfaces. An organization travels as its code.
A generated secret is returned exactly once, by POST .../secret-versions, under no-store. There is no endpoint on this surface to show a stored secret again — that is a Manager-screen operation only: it demands a written reason, writes an audit row, and is rate-limited, none of which has a meaning for an unattended pipeline. A caller that lost the value rotates again; see Idempotency for why a replay cannot hand it out a second time either.
Endpoints
An API client has no access setting of its own: it is authorized by the ACL rows of its ACL owner, which are managed on the Access Control screen and with the credential access endpoints. The authorization-restrictions and entitlements endpoints of earlier versions are removed. Per-client narrowing no longer exists: an API client that must not have its owner's reach is suspended or revoked.
API Clients — /api-clients
| Method | Path | Description |
|---|---|---|
GET | /api-clients | List the project's API clients, by clientId by default |
GET | /api-clients/{apiClientKey} | One API client; never carries secret material |
POST | /api-clients | Create an ACTIVE client with its first secret and deploy it to the gateways (201 + Location, no-store) |
PUT | /api-clients/{apiClientKey} | Replace the editable half of a client; the version must match |
POST | /api-clients/{apiClientKey}/actions/suspend | Pause an ACTIVE client, reversibly |
POST | /api-clients/{apiClientKey}/actions/resume | Return a SUSPENDED client to service |
POST | /api-clients/{apiClientKey}/actions/revoke | End a client permanently and destroy its material — irreversible (200) |
A client is no longer created as a DRAFT. The POST /api-clients/{apiClientKey}/actions/activate endpoint (answers 404) and the draft discard DELETE /api-clients/{apiClientKey} (answers 405) of earlier versions are removed. To take a client out of service, suspend or revoke it.
Secret Generations — /api-clients/{apiClientKey}/secret-versions
| Method | Path | Description |
|---|---|---|
POST | /api-clients/{apiClientKey}/secret-versions | Rotate: add a new generation (201 + Location, no-store); a generated value is returned once |
PUT | /api-clients/{apiClientKey}/secret-versions/{versionId} | Configure one generation: replace its label and expiry (200) |
POST | /api-clients/{apiClientKey}/secret-versions/{versionId}/actions/revoke | Destroy one generation now, including the active one |
Administration path
A portal application belongs to no project, so its API clients answer on /apiops/projects/admin/api-clients/... only. On a project path such a client answers 404 resourceNotFound and is left out of the list, also when an older record still carries that project. GET, POST and PUT, the three lifecycle actions and the secret generation endpoints all work there for the clients of PORTAL applications.
- Who passes. System Administrator, Portal Business and Portal Manager. The two portal roles reach clients of
PORTALapplications only; a client of anINTERNALapplication answers them404 resourceNotFound. A System Administrator reaches all of them. Portal Developer and every other caller get404 resourceNotFound. - Owner. On this path
owner.key(and theownerKeyfilter) takes the application'sapplicationKeyor its name. The key is unique and wins. A name that two applications share is400 malformedRequest(params.field = owner.key,params.reason = applicationNameAmbiguous): send theapplicationKey.
Owner Views (read-only)
| Method | Path | Description |
|---|---|---|
GET | /api-clients/{apiClientKey}/limit-assignments | Every limit assignment whose subject is this client |
The endpoint above returns a plain array — not the paged envelope GET /api-clients uses. Each answers a bounded set that belongs to one client.
List API clients
GET /api-clients?ownerType&ownerKey&state&search&page&size&sort sorts by clientId,asc by default; sort may also name operationalState or expiresAt. name is still accepted as a sort field and is read as clientId. size is capped at 200, same as Pagination.
| Parameter | Type | Meaning |
|---|---|---|
ownerType | enum | CONSUMER or APPLICATION — required together with ownerKey |
ownerKey | string | The owner's own public key (a consumer's username or an application's name) — required together with ownerType |
state | enum | ACTIVE | SUSPENDED | REVOKED (DRAFT only matches clients left from earlier versions) |
search | string | Case-insensitive clientId / description contains-search |
ownerKey without ownerType is 400 malformedRequest (params.reason = ownerKeyRequiresOwnerType); ownerType without ownerKey is the mirror (params.reason = ownerTypeRequiresOwnerKey).
Unlike the equivalent Manager screen, ownerType/ownerKey are optional here: the project is already authorized from the path, and the answer is bounded by the standard page size — a caller reconciling a whole project's clients has no single owner to name. The owner filter remains available and, when given, is resolved to an id first, so an unknown owner is 404 resourceNotFound rather than an empty page.
Idempotency
POST /api-clients and POST /api-clients/{apiClientKey}/secret-versions each require an Idempotency-Key header (see Common Contract → Idempotency); a missing or blank header is 400 idempotencyKeyRequired. The key is claimed per project and per operation, together with a canonical hash of the request body:
| Situation | Result |
|---|---|
| New key | The client is created with its first secret, or the secret generation is issued |
| Same key, same body, first call already finished | 409 idempotencyReplayExpired — params.resource names the apiClientKey (create) or the versionId (secret generation) the first call produced; read it back with GET instead |
| Same key, same body, first call still in flight | 409 idempotencyInProgress |
| Same key, a different body | 409 idempotencyKeyReused |
The dedup record for POST /api-clients and POST .../secret-versions stores only the outcome's identity — the generation's versionId and the client's new version — never the generated value. A replayed key answers 409, not the original response; store the secret from the first successful call, because it cannot be retrieved again.
Request Body
Create / update an API client — POST / PUT /api-clients/{apiClientKey}
Both share one body. POST creates the client active, with its first secret, and deploys it to the gateways at once — there is no draft to equip and activate later. On PUT, the management key (apiClientKey) and the clientId are immutable — a body that omits either keeps the stored value, a body that changes either is 409 lifecycleConflict. The owner is not editable through this body: an owner sent on update is accepted syntactically and ignored, the stored owner is always restored. An absent organizationCode on update keeps the stored organization rather than detaching the client.
| Field | Type | Required | Meaning |
|---|---|---|---|
owner | object | Create: yes. Update: no (kept) | {type, key} — see Owner reference below |
organizationCode | string | No | The owning organization's code; absent on update keeps the stored one, an explicit blank clears it |
description | string | No | |
clientId | string | No | Blank on create mints one; immutable afterwards |
validFrom / expiresAt | timestamp | No | Validity window |
allowedGrantTypes | string[] | No | |
tokenEndpointAuthMethod | string | No | none makes a public client (browser login): it is created active without a secret |
redirectUris | string[] | No | Browser-login redirect URIs of a public client; absent on update keeps the stored list |
tokenIssuanceProfile | string | No | |
tokenNeverExpires | boolean | No | |
tokenExpiresInAmount | integer | No | Paired with tokenExpiresInUnit |
tokenExpiresInUnit | enum | No | MILLI_SECONDS | SECONDS | MINUTES | HOURS | DAYS | WEEKS | MONTHS | YEARS |
refreshTokenAllowed | boolean | No | |
refreshTokenCount | integer | No | |
refreshTokenExpiresInAmount | integer | No | Paired with refreshTokenExpiresInUnit |
refreshTokenExpiresInUnit | enum | No | Same values as tokenExpiresInUnit |
jwtSignatureAlgorithm | string | No | |
secret | object | POST only, optional | The first secret, in the shape of POST .../secret-versions and under the same rules. Absent, a value is generated and returned once. Refused for a public client, and on PUT (400 malformedRequest, params.field = secret, params.reason = secretOnCreateOnly) |
version | integer | PUT only | The stored version the caller expects to be updating |
A body missing entirely on POST/PUT is 400 malformedRequest (params.reason = bodyRequired). A create whose body carries no owner at all is 400 malformedRequest (params.field = owner, params.reason = ownerRequired). A first secret that cannot be used now (already expired, or starting later) is refused before anything is written.
A client has no display name on this surface; it is identified by its clientId. A name sent in the body is accepted and ignored, never a 400.
Owner reference
| Field | Type | Meaning |
|---|---|---|
type | enum | CONSUMER or APPLICATION |
key | string | The owner's own public key: a consumer's username, or an application's name |
Lifecycle action body
Shared by suspend / resume / revoke on the client, and by actions/revoke on a secret generation:
| Field | Type | Required | Meaning |
|---|---|---|---|
version | integer | Yes | The stored version the caller expects; a missing value is 400 malformedRequest (params.field = version, params.reason = versionRequired) rather than an unguarded write |
reason | string | No | Operator free text; stored verbatim, read back only by the actions that record one (suspend, revoke) |
Issue or rotate a secret — POST /api-clients/{apiClientKey}/secret-versions?version=
The stored client version travels as a required query parameter. Idempotency-Key is also required (see Idempotency).
| Field | Type | Meaning |
|---|---|---|
materialType | enum | LOCAL_SECRET (default when absent) or SECRET_REFERENCE |
generate | boolean | Ask Apinizer to produce the value; ignored for a referenced secret |
value | string | The operator's own secret, for a manual (non-generated) LOCAL_SECRET; never echoed back |
reference | string | Where the secret lives, for SECRET_REFERENCE; mutually exclusive with value |
label | string | Operator-facing name of this generation |
validFrom / expiresAt | timestamp | Validity window |
graceWindowSeconds | integer | How long the superseded generation keeps working; absent takes the platform's suggested default |
storageMode | enum | ENCRYPTED (default, recoverable) or HASHED (verifier only, shown once); ignored for SECRET_REFERENCE |
An absent version query parameter is 400 malformedRequest (params.field = version, params.reason = versionRequired); a missing body entirely is 400 malformedRequest (params.reason = bodyRequired).
Configure a secret generation — PUT /api-clients/{apiClientKey}/secret-versions/{versionId}
Replaces the label and the expiry of one generation and pushes the client to the gateways. The body is not a patch: a field you leave out is cleared, not kept.
| Field | Type | Meaning |
|---|---|---|
version | integer | Required. The stored client version you expect; a missing value is 400 malformedRequest (params.field = version, params.reason = versionRequired) |
label | string | Operator-facing name. Omit it and a label is generated |
expiresAt | timestamp | Omit it and the generation has no expiry. A changed value that does not lie in the future, or that lies before the start of the generation, is 400 malformedRequest; sending the stored value again is accepted |
The answer is the Mutation shape. A stale version is 409 versionConflict (params.reason = apiClientRevisionConflict). A revoked client or a revoked generation is refused. The request needs no Idempotency-Key.
Response
API client — 200 / 201
GET /api-clients/{apiClientKey} and every row of GET /api-clients:
| Field | Type | Meaning |
|---|---|---|
apiClientKey | string | Portable, installation-wide public identifier |
clientId | string | The gateway-facing identifier a token request presents |
description | string | |
owner | object | {type, key} — see Owner reference |
organizationCode | string | The owning organization's code, or absent |
operationalState | enum | ACTIVE | SUSPENDED | REVOKED — whether the operator currently allows this client to be used. DRAFT appears only on clients left from earlier versions |
readiness | enum | READY | PENDING_SECRET | UNRESOLVED_DEPENDENCY — whether the client actually has what it needs, independent of operationalState |
validFrom / expiresAt | timestamp | |
allowedGrantTypes | string[] | |
tokenEndpointAuthMethod / tokenIssuanceProfile | string | |
redirectUris | string[] | Browser-login redirect URIs of a public client |
tokenNeverExpires | boolean | |
tokenExpiresInAmount / tokenExpiresInUnit | integer / enum | |
refreshTokenAllowed | boolean | |
refreshTokenCount | integer | |
refreshTokenExpiresInAmount / refreshTokenExpiresInUnit | integer / enum | |
jwtSignatureAlgorithm | string | |
revokedAt / revokedBy / revokedReason | timestamp / string / string | Present once operationalState is REVOKED |
lifecycleRevision | integer | The separate, monotonic counter the runtime reconciles against; informational here |
version | integer | The optimistic-concurrency fence — send back on the next write or lifecycle action |
secretVersions | array | Every generation of this client, described without any secret material — see below |
Neither this shape nor any other list on this page carries a stored verifier, a platform ciphertext, a KEK recovery envelope, a secret reference, or a hint of one. secretVersions describes each generation's identity and lifecycle only. A generated value exists on the wire exactly once, in the Client created or Secret issued response.
Secret generation (nested)
One entry of secretVersions, on the client shape above:
| Field | Type | Meaning |
|---|---|---|
versionId | string | This generation's own key |
label | string | |
status | enum | PENDING_ACTIVATION | ACTIVE | GRACE | REVOKED |
materialType | string | LOCAL_SECRET or SECRET_REFERENCE, as stored |
storageMode | enum | ENCRYPTED | HASHED |
recoveryState | enum | AVAILABLE | UNAVAILABLE | SHREDDED — whether the original material can still be produced for an operator through the Manager screen; unrelated to whether the version still authenticates |
validFrom / expiresAt / graceUntil / createdAt | timestamp | |
revokedAt / revokedReason | timestamp / string | |
version | integer | This generation's own optimistic-concurrency counter |
Mutation — 200 / 201
Every write on the client itself (POST, PUT, and every actions/*), and POST .../secret-versions/{versionId}/actions/revoke, answers with the fields below; POST adds the first secret's fields — see Client created:
| Field | Type | Meaning |
|---|---|---|
apiClient | object | The client as it now stands — the API client shape above |
runtimeDeployment | object | What the push told the gateways, when the write triggered one — the shared Write responses shape (state: IN_SYNC | DEGRADED, environments[]); absent when nothing was pushed |
runtimeDeployment.state is IN_SYNC when every environment acknowledged the change, or DEGRADED when the change is stored but at least one environment did not acknowledge it. Each environments[] entry carries name, acknowledged and, when the environment did not acknowledge, a one-line detail of at most 300 characters. Do not retry a DEGRADED write; redeploy instead. After a secret rotation or a generation revoke, a DEGRADED write means a gateway may still verify against the previous copy of the secret, so a pipeline must test runtimeDeployment.state and not only the HTTP status before it retires the old secret.
Client created — 201
The answer to POST /api-clients, sent with Cache-Control: no-store and Pragma: no-cache: the Mutation fields plus
| Field | Type | Meaning |
|---|---|---|
versionId | string | The first secret generation's key; absent for a public client |
secret | string | The generated first secret, shown once — absent when the caller supplied its own secret.value, and for a public client |
Secret issued — 201
The answer to POST .../secret-versions, sent with Cache-Control: no-store and Pragma: no-cache:
| Field | Type | Meaning |
|---|---|---|
apiClient | object | The client as it now stands, including the new generation in secretVersions |
versionId | string | The new generation's key |
secret | string | The generated value, shown once — absent when the caller supplied its own value |
runtimeDeployment | object | The gateway push result for the rotation — same shape and meaning as in Mutation; absent when nothing was pushed |
Limit assignments — 200
Every row of GET .../limit-assignments — every limit assignment whose subject is this client, in the same portable-reference vocabulary as Bulk Limit Assignment and the Limit Plans surface:
| Field | Type | Meaning |
|---|---|---|
bindingKey | string | |
subject | object | Echoes this client back as the subject (type: API_CLIENT, portable key) |
family | enum | REQUEST_RATE or AI_TOKEN_BUDGET |
actorScope | enum | ANY | AUTHENTICATED | ANONYMOUS |
target | object | What the ceiling narrows to — proxy, group, environment, endpoint, model or provider, portably referenced |
mode | enum | PLAN | CUSTOM | OFF |
planKeys | string[] | Plan keys, when mode is PLAN |
customRuleSpec | object | The plan's own rule body, inline, when mode is CUSTOM |
audienceRule | object | Present only for a rule-based assignment; never the case for a client subject |
cacheErrorHandlingType / cacheConnectionTimeoutInSeconds | enum / integer | Request Rate only |
showRateLimitStatisticsInResponseHeader | boolean | Applies to Request Rate and AI Token Budget assignments; an AI plan that leaves its own setting unset uses this value |
customMessageTemplate / customMessageContentType / customMessageHttpStatusCode | string / string / integer | Request Rate only |
source | enum | How the assignment came to exist (manual, bulk run, portal subscription, …) |
status | enum | DRAFT | ACTIVE | ENDED |
validFrom / validUntil | timestamp | |
assignedAt / assignedBy | timestamp / string | |
endedAt / endedBy / endedReason | timestamp / string / string | |
operatorOverride | boolean | |
version | integer | |
assignmentMode / inheritTo | enum / enum | The Counted As choice. An API client is always counted on its own, so these are SELF / absent here |
bulkRunId | string | The Bulk Assignment run that created this row, when there is one |
replacedBindingKey | string | The row this one replaced under a REPLACE conflict policy, when there is one |
List envelope — 200
GET /api-clients returns the shared paged envelope; the limit-assignments list on this page returns a plain array instead.
Errors
Every error follows the Common Contract's error envelope — branch on errorKey, never on message.
| Error key | When |
|---|---|
malformedRequest | A body was missing (params.reason = bodyRequired), a required field was missing (ownerRequired, versionRequired, modeRequired — params.field names it), a secret was sent on PUT (secretOnCreateOnly), ownerKey/ownerType was given without its pair (ownerKeyRequiresOwnerType / ownerTypeRequiresOwnerKey), an application name on the administration path is shared by two applications (applicationNameAmbiguous, with params.field = owner.key), or an underlying validation rule was refused — params.reason then carries the service's own key |
resourceNotFound | An unknown or out-of-project apiClientKey or ownerKey |
versionConflict | The stored version no longer matches what the caller sent — params.reason = apiClientRevisionConflict (client) |
lifecycleConflict | params.reason names the cause: apiClientIllegalTransition (a suspend/resume/revoke move the current state refuses), apiClientNotReady (the client does not have what it needs to be in service), apiClientRevokedReadOnly (an update of a revoked client), apiClientSecretRotationRefused, secretVersionAlreadyRevoked, clientIdImmutable / apiClientKeyImmutable (an update tried to change an immutable identifier) |
idempotencyKeyRequired | POST /api-clients or POST .../secret-versions was missing its Idempotency-Key header |
idempotencyKeyReused | The same key was sent with a different request body |
idempotencyReplayExpired | The same key and body already completed — params.resource names what it created |
idempotencyInProgress | The same key and body's first call is still running |
permissionDenied | Authenticated, but the caller lacks Identity Manage (writes) or View (reads) |
See Common Contract → Error keys for the complete closed set and the status-code table.
Permissions & Licensing
- Reads (
GET) require the Identity permission at View; writes (POST/PUT) require it at Manage. No AI Gateway license module gates any endpoint on this surface. - API clients are project-owned. The admin (platform) project has none of its own, with one exception: the clients of
PORTALapplications answer on theadminpath to the System Administrator, Portal Business and Portal Manager roles (see Administration path). The earlier400 platformScopeNotSupportedanswer no longer applies to them. - A project outside the caller's authorized scope is always
404 resourceNotFound, never403— see Common Contract → Telling 402, 403 and 404 apart. The same rule applies to a client whose stored project does not match the one in the path: it is answered with the identical404a nonexistent key would get, never a distinguishing status.
Example
Create a client owned by a consumer — it is active at once, with its first secret — rotate the secret, then revoke the client:
# 1. Create — ACTIVE, deployed, with a generated first secret
curl -X POST "https://apinizer.example.com/apiops/projects/finance/api-clients" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5e2a1c7b-9d4f-4a3e-8b1c-2f6d9a0e3c81" \
-d '{
"owner": { "type": "CONSUMER", "key": "acme-integration-user" },
"description": "Nightly settlement batch job",
"allowedGrantTypes": ["client_credentials"],
"tokenExpiresInAmount": 1,
"tokenExpiresInUnit": "HOURS",
"secret": { "generate": true, "label": "initial", "storageMode": "ENCRYPTED" }
}'
# 201 Created, Location: /apiops/projects/finance/api-clients/acme-nightly-batch-4f9a2c8d
# response.secret holds the plaintext ONCE; store it now. response.apiClient.version is the version to send next
# 2. Rotate the secret — send the client's current version
curl -X POST "https://apinizer.example.com/apiops/projects/finance/api-clients/acme-nightly-batch-4f9a2c8d/secret-versions?version=1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9a0c3e7f-1b2d-4c5e-9f6a-8d7c1e0b4a92" \
-d '{
"generate": true,
"label": "rotation-1",
"graceWindowSeconds": 3600
}'
# 201 Created — response.secret holds the new plaintext ONCE
# 3. Revoke it later — irreversible; read the client back first for its current version
curl -X POST "https://apinizer.example.com/apiops/projects/finance/api-clients/acme-nightly-batch-4f9a2c8d/actions/revoke" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "version": 2, "reason": "Batch job decommissioned" }'
# 200 OK
Related Documentation
- API Clients - The concept, the Manager screen, and client-secret storage modes
- Bulk Limit Assignment API - Attach a Request Rate or AI Token Budget assignment to a filtered population of clients (among other subject types) in one run
- Limit Plans, Assignments & Defaults API - The manager's own REST surface behind limit plans and per-subject assignments
- Common Contract - The shared error envelope, idempotency, pagination, and status-code rules every endpoint on this page follows
- Error Handling - The envelope used by the manager's older endpoints