Ana içeriğe geç

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 and a platform-scoped call is refused with 400 malformedRequest (params.reason = platformScopeNotSupported).

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 an entitlement's subjectKey and a limit assignment's subject.key already use, so one client's owner.key matches its owner's rows on the sibling surfaces. An organization travels as its code.

No reveal endpoint

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

API Clients — /api-clients

MethodPathDescription
GET/api-clientsList the project's API clients, by name by default
GET/api-clients/{apiClientKey}One API client; never carries secret material
POST/api-clientsCreate a DRAFT client holding no material (201 + Location)
PUT/api-clients/{apiClientKey}Replace the editable half of a client; the version must match
DELETE/api-clients/{apiClientKey}Discard a draft that was never activated
POST/api-clients/{apiClientKey}/actions/activateMove a materially ready DRAFT client into service
POST/api-clients/{apiClientKey}/actions/suspendPause an ACTIVE client, reversibly
POST/api-clients/{apiClientKey}/actions/resumeReturn a SUSPENDED client to service
POST/api-clients/{apiClientKey}/actions/revokeEnd a client permanently and destroy its material — irreversible (200, or 202 + Location when handed to the durable sweeper)

Secret Generations — /api-clients/{apiClientKey}/secret-versions

MethodPathDescription
POST/api-clients/{apiClientKey}/secret-versionsIssue the first generation, or rotate (201 + Location, no-store); a generated value is returned once
POST/api-clients/{apiClientKey}/secret-versions/{versionId}/actions/revokeDestroy one generation now, including the active one

Authorization Restrictions — /api-clients/{apiClientKey}/authorization-restrictions

MethodPathDescription
GET/api-clients/{apiClientKey}/authorization-restrictionsEvery restriction row of the client, active or ended
POST/api-clients/{apiClientKey}/authorization-restrictionsAdd a DRAFT restriction row (201 + Location)
PUT/api-clients/{apiClientKey}/authorization-restrictions/{restrictionKey}Edit a DRAFT row; the version must match
POST/api-clients/{apiClientKey}/authorization-restrictions/{restrictionKey}/actions/activatePut a drafted row into force
POST/api-clients/{apiClientKey}/authorization-restrictions/{restrictionKey}/actions/endLift a restriction row; it is ended, never deleted
POST/api-clients/{apiClientKey}/authorization-restrictions/actions/change-modeChange the client's default reach (DENY_ALL / OWNER_DEFAULT / ALLOW_SUBSET)

Owner Views (read-only)

MethodPathDescription
GET/api-clients/{apiClientKey}/entitlementsEvery access entitlement of the client's owner — a client is never itself an entitlement subject
GET/api-clients/{apiClientKey}/limit-assignmentsEvery limit assignment whose subject is this client
Not paged

Both endpoints above, and the authorization-restrictions listing, return 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 name,asc by default; sort may also name operationalState or expiresAt. size is capped at 200, same as Pagination.

ParameterTypeMeaning
ownerTypeenumCONSUMER or APPLICATION — required together with ownerKey
ownerKeystringThe owner's own public key (a consumer's username or an application's name) — required together with ownerType
stateenumDRAFT | ACTIVE | SUSPENDED | REVOKED
searchstringCase-insensitive name / clientId contains-search

ownerKey without ownerType is 400 malformedRequest (params.reason = ownerKeyRequiresOwnerType); ownerType without ownerKey is the mirror (params.reason = ownerTypeRequiresOwnerKey).

Ownerless listing is intentional

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:

SituationResult
New keyThe client is created, or the secret generation is issued
Same key, same body, first call already finished409 idempotencyReplayExpiredparams.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 flight409 idempotencyInProgress
Same key, a different body409 idempotencyKeyReused
A replayed secret generation is never handed out twice

The dedup record for 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. 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.

FieldTypeRequiredMeaning
ownerobjectCreate: yes. Update: no (kept){type, key} — see Owner reference below
organizationCodestringNoThe owning organization's code; absent on update keeps the stored one, an explicit blank clears it
namestringNo
descriptionstringNo
clientIdstringNoBlank on create mints one; immutable afterwards
validFrom / expiresAttimestampNoValidity window
allowedGrantTypesstring[]No
tokenEndpointAuthMethodstringNo
tokenIssuanceProfilestringNo
tokenNeverExpiresbooleanNo
tokenExpiresInAmountintegerNoPaired with tokenExpiresInUnit
tokenExpiresInUnitenumNoMILLI_SECONDS | SECONDS | MINUTES | HOURS | DAYS | WEEKS | MONTHS | YEARS
refreshTokenAllowedbooleanNo
refreshTokenCountintegerNo
refreshTokenExpiresInAmountintegerNoPaired with refreshTokenExpiresInUnit
refreshTokenExpiresInUnitenumNoSame values as tokenExpiresInUnit
jwtSignatureAlgorithmstringNo
versionintegerPUT onlyThe 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).

Owner reference

FieldTypeMeaning
typeenumCONSUMER or APPLICATION
keystringThe owner's own public key: a consumer's username, or an application's name

Lifecycle action body

Shared by activate / suspend / resume / revoke on the client, and by actions/revoke on a secret generation and actions/activate / actions/end on a restriction row:

FieldTypeRequiredMeaning
versionintegerYesThe stored version (client actions) or restriction version the caller expects; a missing value is 400 malformedRequest (params.field = version, params.reason = versionRequired) rather than an unguarded write
reasonstringNoOperator free text; stored verbatim, read back only by the actions that record one (suspend, revoke, restriction end)

Discarding a draft (DELETE /api-clients/{apiClientKey}?version=) takes the same version as a query parameter instead — a DELETE carries no body, so what is deleted, and on which revision, is readable from the URL alone.

Issue or rotate a secret — POST /api-clients/{apiClientKey}/secret-versions?version=

The stored client version travels as a required query parameter, the same reasoning as the DELETE above. Idempotency-Key is also required (see Idempotency).

FieldTypeMeaning
materialTypeenumLOCAL_SECRET (default when absent) or SECRET_REFERENCE
generatebooleanAsk Apinizer to produce the value; ignored for a referenced secret
valuestringThe operator's own secret, for a manual (non-generated) LOCAL_SECRET; never echoed back
referencestringWhere the secret lives, for SECRET_REFERENCE; mutually exclusive with value
labelstringOperator-facing name of this generation
validFrom / expiresAttimestampValidity window
graceWindowSecondsintegerHow long the superseded generation keeps working; absent takes the platform's suggested default
storageModeenumENCRYPTED (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).

Authorization restriction body — POST / PUT .../authorization-restrictions[/{restrictionKey}]

The owning client always comes from the path — the body never names it. status is not writable here: effectiveness moves only through the activate / end actions.

FieldTypeMeaning
modeenumDENY_ALL | OWNER_DEFAULT | ALLOW_SUBSET — see Restriction modes
targetTypeenumRequired (with targetStableRef) on an ALLOW_SUBSET row; absent on a client-general row. API_PROXY | API_PROXY_GROUP | API_PRODUCT | API_PROXY_ENV
targetStableRefstringThe target's portable reference, paired with targetType
environmentRefstringOnly for API_PROXY_ENV; must be absent for every other target kind
versionintegerRequired on update — the stored restriction row's counter

Restriction modes

  • DENY_ALL — the client reaches nothing its owner holds.
  • OWNER_DEFAULT — the client inherits its owner's effective grants unchanged.
  • ALLOW_SUBSET — the client reaches only the targets its own ALLOW_SUBSET rows name; a target the owner does not itself hold is refused at write time rather than granted.

At most one client-general row (DENY_ALL or OWNER_DEFAULT, naming no target) may be ACTIVE at a time. With no ACTIVE client-general row, the effective mode defaults from the owner's kind: DENY_ALL for an application-owned client, OWNER_DEFAULT for a consumer-owned one.

Change the default restriction mode — POST .../authorization-restrictions/actions/change-mode

Not an edit in place: the active client-general row is ended and a new one is written, so the change leaves a history rather than overwriting one.

FieldTypeRequiredMeaning
modeenumYesDENY_ALL | OWNER_DEFAULT | ALLOW_SUBSET — a missing value is 400 malformedRequest (params.field = mode, params.reason = modeRequired)
versionintegerNoThe active client-general row's version; legitimately absent when the client has none yet
reasonstringNoOperator free text

Response

API client — 200 / 201

GET /api-clients/{apiClientKey} and every row of GET /api-clients:

FieldTypeMeaning
apiClientKeystringPortable, installation-wide public identifier
clientIdstringThe gateway-facing identifier a token request presents
name / descriptionstring
ownerobject{type, key} — see Owner reference
organizationCodestringThe owning organization's code, or absent
operationalStateenumDRAFT | ACTIVE | SUSPENDED | REVOKED — whether the operator currently allows this client to be used
readinessenumREADY | PENDING_SECRET | UNRESOLVED_DEPENDENCY — whether the client actually has what it needs, independent of operationalState
validFrom / expiresAttimestamp
allowedGrantTypesstring[]
tokenEndpointAuthMethod / tokenIssuanceProfilestring
tokenNeverExpiresboolean
tokenExpiresInAmount / tokenExpiresInUnitinteger / enum
refreshTokenAllowedboolean
refreshTokenCountinteger
refreshTokenExpiresInAmount / refreshTokenExpiresInUnitinteger / enum
jwtSignatureAlgorithmstring
revokedAt / revokedBy / revokedReasontimestamp / string / stringPresent once operationalState is REVOKED
lifecycleRevisionintegerThe separate, monotonic counter the runtime reconciles against; informational here
versionintegerThe optimistic-concurrency fence — send back on the next write or lifecycle action
secretVersionsarrayEvery generation of this client, described without any secret material — see below
No material, ever

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 Secret issued response.

Secret generation (nested)

One entry of secretVersions, on the client shape above:

FieldTypeMeaning
versionIdstringThis generation's own key
labelstring
statusenumPENDING_ACTIVATION | ACTIVE | GRACE | REVOKED
materialTypestringLOCAL_SECRET or SECRET_REFERENCE, as stored
storageModeenumENCRYPTED | HASHED
recoveryStateenumAVAILABLE | 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 / createdAttimestamp
revokedAt / revokedReasontimestamp / string
versionintegerThis generation's own optimistic-concurrency counter

Mutation — 200 / 201 / 202

Every write on the client itself (POST, PUT, and every actions/*) answers with:

FieldTypeMeaning
apiClientobjectThe client as it now stands — the API client shape above
runtimeDeploymentobjectWhat the push told the gateways, when the write triggered one — the shared Write responses shape (state: IN_SYNC | DEGRADED | PENDING, environments[]); absent when nothing was pushed

POST .../actions/revoke is the one action on this surface that may answer 202 instead of 200: when the withdrawal has been handed to the durable sweeper rather than completed synchronously, the response carries Location pointing at the same client and runtimeDeployment.state = PENDING.

Secret issued — 201

The answer to POST .../secret-versions, sent with Cache-Control: no-store and Pragma: no-cache:

FieldTypeMeaning
apiClientobjectThe client as it now stands, including the new generation in secretVersions
versionIdstringThe new generation's key
secretstringThe generated value, shown once — absent when the caller supplied its own value

Authorization restriction — 200 / 201

FieldTypeMeaning
restrictionKeystringPortable key of this row
apiClientKeystringThe owning client
modeenumDENY_ALL | OWNER_DEFAULT | ALLOW_SUBSET
targetTypeenumPresent only on an ALLOW_SUBSET row
targetStableRefstringPaired with targetType
environmentRefstringOnly for API_PROXY_ENV
statusenumDRAFT | ACTIVE | ENDED
versioninteger
assignedAt / assignedBytimestamp / string
endedAt / endedBy / endedReasontimestamp / string / stringPresent once status is ENDED

Ended rows are listed alongside active ones — they are the history of how the client's reach changed, and are never deleted.

Owner's entitlements — 200

Every row of GET .../entitlements — the client owner's own access entitlements, not the client's:

FieldTypeMeaning
entitlementKeystring
subjectTypeenumCONSUMER or APPLICATION — the owner's kind
subjectKeystringThe owner's own public key
targetTypeenumAPI_PROXY | API_PROXY_GROUP | API_PRODUCT | API_PROXY_ENV
targetRefstringThe target's portable reference
environmentRefstringOnly for API_PROXY_ENV
statusenumDRAFT | ACTIVE | ENDED
validFrom / validUntiltimestamp
sourceenumHow the entitlement came to exist
versioninteger
assignedAt / assignedBytimestamp / string
endedAt / endedBy / endedReasontimestamp / string / string

What this client itself may actually reach is this list narrowed by its own authorization restrictions — the two endpoints answer the two halves of that question.

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:

FieldTypeMeaning
bindingKeystring
subjectobjectEchoes this client back as the subject (type: API_CLIENT, portable key)
familyenumREQUEST_RATE or AI_TOKEN_BUDGET
actorScopeenumANY | AUTHENTICATED | ANONYMOUS
targetobjectWhat the ceiling narrows to — proxy, group, environment, endpoint, model or provider, portably referenced
modeenumPLAN | CUSTOM | INHERIT | OFF
planKeysstring[]Published plan keys, when mode is PLAN
customRuleSpecobjectThe plan's own rule body, inline, when mode is CUSTOM
audienceRuleobjectPresent only for a rule-based assignment; never the case for a client subject
cacheErrorHandlingType / cacheConnectionTimeoutInSeconds / showRateLimitStatisticsInResponseHeaderenum / integer / booleanRequest Rate only
customMessageTemplate / customMessageContentType / customMessageHttpStatusCodestring / string / integerRequest Rate only
sourceenumHow the assignment came to exist (manual, bulk run, portal subscription, …)
statusenumDRAFT | ACTIVE | ENDED
validFrom / validUntiltimestamp
assignedAt / assignedBytimestamp / string
endedAt / endedBy / endedReasontimestamp / string / string
operatorOverrideboolean
versioninteger
bulkRunIdstringThe Bulk Assignment run that created this row, when there is one
replacedBindingKeystringThe 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 other list endpoints on this page return a plain array instead.

Errors

Every error follows the Common Contract's error envelope — branch on errorKey, never on message.

Error keyWhen
malformedRequestA body was missing (params.reason = bodyRequired), a required field was missing (ownerRequired, versionRequired, modeRequiredparams.field names it), ownerKey/ownerType was given without its pair (ownerKeyRequiresOwnerType / ownerTypeRequiresOwnerKey), the admin (platform) project was addressed (platformScopeNotSupported), or an underlying validation rule was refused — params.reason then carries the service's own key
resourceNotFoundAn unknown or out-of-project apiClientKey, restrictionKey, or ownerKey
versionConflictThe stored version no longer matches what the caller sent — params.reason = apiClientRevisionConflict (client) or restrictionRevisionConflict (restriction row)
lifecycleConflictparams.reason names the cause: apiClientIllegalTransition (an activate/suspend/resume/revoke move the current state refuses), apiClientNotReady (activate attempted before the client is materially ready), apiClientSecretRotationRefused, secretVersionAlreadyRevoked, apiClientNotDraft (discard attempted on a non-draft client), clientIdImmutable / apiClientKeyImmutable (an update tried to change an immutable identifier), restrictionKeyImmutable, or entitlementDuplicateActive (an equivalent restriction row is already active)
idempotencyKeyRequiredPOST /api-clients or POST .../secret-versions was missing its Idempotency-Key header
idempotencyKeyReusedThe same key was sent with a different request body
idempotencyReplayExpiredThe same key and body already completed — params.resource names what it created
idempotencyInProgressThe same key and body's first call is still running
permissionDeniedAuthenticated, 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/DELETE) 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 and every endpoint refuses a platform-scoped call with 400 malformedRequest (params.reason = platformScopeNotSupported).
  • A project outside the caller's authorized scope is always 404 resourceNotFound, never 403 — see Common Contract → Telling 402, 403 and 404 apart. The same rule applies to a client or restriction whose stored project does not match the one in the path: it is answered with the identical 404 a nonexistent key would get, never a distinguishing status.

Example

Create a client owned by a consumer, issue its first secret, narrow it to one API proxy, then revoke it:

# 1. Create — DRAFT, no material yet
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" },
"name": "acme-nightly-batch",
"description": "Nightly settlement batch job",
"allowedGrantTypes": ["client_credentials"],
"tokenExpiresInAmount": 1,
"tokenExpiresInUnit": "HOURS"
}'
# 201 Created, Location: /apiops/projects/finance/api-clients/acme-nightly-batch-4f9a2c8d

# 2. Issue the first secret — version 0 (freshly created)
curl -X POST "https://apinizer.example.com/apiops/projects/finance/api-clients/acme-nightly-batch-4f9a2c8d/secret-versions?version=0" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9a0c3e7f-1b2d-4c5e-9f6a-8d7c1e0b4a92" \
-d '{
"generate": true,
"label": "initial",
"storageMode": "ENCRYPTED"
}'
# 201 Created — response.secret holds the plaintext ONCE; store it now

# 3. Narrow the client to one API proxy
curl -X POST "https://apinizer.example.com/apiops/projects/finance/api-clients/acme-nightly-batch-4f9a2c8d/authorization-restrictions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mode": "ALLOW_SUBSET",
"targetType": "API_PROXY",
"targetStableRef": "settlement-batch-api"
}'
# 201 Created

# 4. Activate the client — version has advanced with the secret write; read it back first
curl -X POST "https://apinizer.example.com/apiops/projects/finance/api-clients/acme-nightly-batch-4f9a2c8d/actions/activate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "version": 1 }'

# 5. Revoke it later — irreversible
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, or 202 + Location when handed to the durable sweeper
  • 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