Credentials API
Endpoints
CRUD Operations
- List Credentials - Get all credentials for a project
- Create Credential - Create a new credential
- Update Credential - Update an existing credential
- Change Credential Password - Change credential password
- Delete Credential - Delete a credential
Credential Organizations
- Credential Organizations - Manage organizational groupings of credentials
- Create Credential Organization - Create a new organization
- Get Credential Organization - Retrieve an organization by code
- Update Credential Organization - Update an organization
- Delete Credential Organization - Delete an organization
Access Management
- Get Granted Access List - Get list of API Proxies/Groups granted to credential
- Grant Access - Grant access to API Proxy or API Proxy Group
- Revoke Access - Revoke access from API Proxy or API Proxy Group
Secrets Management
- Secrets Management - Manage secret keys, certificates, keys, keystores, and truststores for credentials
JWK Settings
- JWK Settings - Manage JWK settings for validation, signing, decryption, and encryption
Token Settings
- Token Settings - Configure token expiration, refresh tokens, grant types, and JWT signature algorithms
Credential Identity Fields
A credential is identified by two values, and only one of them takes part in the payload of these endpoints.
| Field | In the payload | Description |
|---|---|---|
username | Yes, writable | Login and display identity. Unique across ALL projects of the installation, not only the target project. It may be changed after creation. |
consumerKey | No | Permanent, URL-safe public identity assigned by the server when the credential is created. Unique across the installation, never editable, and never carried over to another record: a duplicated credential, or one brought in with Import as New, is assigned a new key. It does not change when username changes. |
consumerKey is stored on the credential, but it is not part of the request or response body of the endpoints on this page in this version: it can neither be set nor read through them, and a value sent under that name is ignored. Keep using username to address a credential here.
A typed consumer surface that exposes consumerKey as an addressable, read-only identity is planned for a later version. Until then, do not build an integration that expects the field in these responses.
API Client Fields
A credential (and an application) can now own API clients — machine identities with their own client id and versioned client secrets, issued from /credential/token and /credential/jwt. As of this version the API client entity carries an issuance-configuration field group, mirroring the shape of Credential Token Settings:
| Field | Type | Description |
|---|---|---|
allowedGrantTypes | array of strings | Reserved for a future per-client grant restriction. |
tokenEndpointAuthMethod | string | Reserved for a future client-authentication-method selector. |
tokenIssuanceProfile | string | Reserved for a future named issuance profile. |
tokenNeverExpires | boolean | Same meaning as the credential field of the same name. Unset reads as false. |
tokenExpiresInAmount / tokenExpiresInUnit | long / string | Same meaning as the credential fields of the same name. Unit values: MILLI_SECONDS, SECONDS, MINUTES, HOURS, DAYS, WEEKS, MONTHS, YEARS. |
refreshTokenAllowed | boolean | Same meaning as the credential field of the same name. Unset reads as false — a newly created API client is born without refresh capability. |
refreshTokenCount / refreshTokenExpiresInAmount / refreshTokenExpiresInUnit | integer / long / string | Same meaning as the credential fields of the same name. |
jwtSignatureAlgorithm | string | Same meaning as the credential field of the same name. Unset reads as RS256. |
None of these fields are exposed through APIops in this version: an API client has no APIops CRUD surface at all yet, so none of them can be set or read through a request to this API. Every API client is issued and activated with these effective defaults, and the token endpoint independently accepts only the client_credentials and refresh_token grant types for an API client regardless of allowedGrantTypes's content — password is never accepted for one.
A typed APIops surface for API clients is planned for a later version. Until then, do not build an integration that expects to create, update or read an API client through /apiops/**.
The same applies to the API client lifecycle operations introduced in this version — suspend, resume, revoke and single secret-version revocation (see Suspending, Resuming and Revoking an API Client): they are performed from the manager interface and are not exposed through /apiops/**. One consequence does reach this API: deleting a credential that still owns API clients not yet revoked is refused with an HTTP 409 response — revoke the clients from the manager interface first.
API Client Runtime Deployment (Manager REST API)
The API client lifecycle endpoints referenced above live on the manager's own REST surface (/api/api-clients/**), not on /apiops/** — see the note above. They are documented here, separately from the rest of this page, because their response shape is the same one an APIops caller of a different resource already sees, and an integration reading both should recognize it as one contract.
The Mutation Envelope
Every write on an API client — create, update, activate, suspend, resume, revoke, and revoking a single secret generation — answers with the same envelope shape:
{
"apiClient": { "...": "the client as it now stands" },
"runtimeDeployment": {
"state": "IN_SYNC",
"revision": 42,
"environments": [
{ "name": "production", "acknowledged": true },
{ "name": "staging", "acknowledged": false, "detail": "connection timed out" }
]
}
}
runtimeDeployment.state is one of three values, and only one of them means the change is fully live everywhere:
| State | Meaning |
|---|---|
IN_SYNC | Every targeted environment has acknowledged the change. |
DEGRADED | The change is stored — the record already reflects it, and for a revoke the stored tokens are already deleted — but at least one environment has not yet acknowledged it. |
PENDING | The change is stored and delivery has been handed to an asynchronous retry process; no environment answer is being waited for on this response. Produced only by the terminal revoke endpoint, and only when the synchronous push did not reach full acknowledgement. |
revision is present only on the durable read described below; a mutation's own envelope reports what that one push achieved and omits it. Neither DEGRADED nor PENDING should be read as "finished" by an automated caller — a client reported this way may still be reachable on an environment that has not yet caught up.
Terminal Revoke: 202 Accepted
POST /api/api-clients/{apiClientKey}/actions/revoke answers 202 Accepted with a Location header pointing at the runtime-deployment endpoint below, and a PENDING envelope, when the synchronous push did not achieve full acknowledgement across every targeted environment. Every other mutation on this resource — including suspend and resume — always answers 200 OK with IN_SYNC or DEGRADED; 202/PENDING is specific to a revoke that could not be confirmed everywhere immediately.
A 202 here does not mean the revoke failed or is reversible — the record change and the token deletion already happened. It means the outcome on every environment is not yet known, and a caller that needs to confirm full propagation should poll the endpoint the Location header names.
Durable Read: GET .../runtime-deployment
GET /api/api-clients/{apiClientKey}/runtime-deployment
Returns the same envelope shape as above, but describing the client's current standing rather than one past push: which environments have confirmed the client's latest revision, which have not, and why (when known). Unlike a mutation's own envelope, this endpoint writes nothing and can be polled freely.
| Status | Meaning |
|---|---|
200 OK | A runtime record exists for this client; the body is the envelope above. |
204 No Content | No runtime record exists for this client (for example, one created before this mechanism existed). There is no partial or inferred answer to give in that case. |
API Client Secret Migration (Manager REST API)
Like the runtime-deployment endpoints above, this one lives on the manager's own REST surface (/api/api-clients/**), not on /apiops/**, and has no APIops CRUD surface. It gives an API client created by the legacy-record migration the exact secret its legacy credential already uses, so the same client id and password keep authenticating without a forced reset. The startup migration runs this same carry-over itself, installation-wide, the first time the Manager starts on this version (see Automatic Migration at Upgrade); this endpoint is its scripted form, for re-running the step against one project after the fact. This section covers only the request and response shape.
POST /api/api-clients/migration/secrets/carry-over
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
projectId | string | (none) | Scopes the run to one project. Omitted or blank runs over the whole installation and is refused unless the caller has system administrator permission. |
apply | boolean | false | false classifies every candidate and writes nothing. true creates a secret for each eligible client. |
activate | boolean | false | Only takes effect together with apply=true. Additionally moves a Draft client whose secret was just carried — or that already held one — to Active. Ignored on a dry run. |
Response Shape
{
"apply": true,
"activate": false,
"recoverySupported": true,
"scanned": 42,
"counts": { "CARRIED": 38, "ALREADY_HAS_MATERIAL": 2, "SKIPPED_BLANK_SECRET": 2 },
"activatedCount": 0,
"rows": [
{
"apiClientKey": "...",
"clientId": "legacy-service-1",
"projectId": "...",
"migratedFromCredentialId": "...",
"operationalState": "DRAFT",
"outcome": "CARRIED",
"detail": "A secret generation was derived from the legacy password; the legacy row is unchanged.",
"secretVersionId": "...",
"recoveryState": "AVAILABLE",
"activated": false
}
]
}
| Field | Description |
|---|---|
apply / activate | Echo the request's own switches. activate reads back false whenever apply was false, regardless of what was sent, since nothing is activated on a dry run. |
recoverySupported | Whether the installation holds a recovery key (KEK) for sealing a recovery envelope. It does not determine whether a carried-over secret can be revealed — see the note below. |
scanned | How many candidate clients — those created by the legacy migration — were evaluated. |
counts | Number of rows for each outcome value present in this run (see the table below); an outcome with zero rows is omitted. |
activatedCount | How many clients this run itself moved from Draft to Active. |
rows | One entry per candidate client. operationalState reflects the client's lifecycle state as observed at the start of this row's evaluation, not after; activated is the field that tells you whether this run additionally activated it. secretVersionId and recoveryState are present when outcome is CARRIED, CARRIED_RAW, or CARRIED_REFERENCE. |
Outcome Values
outcome | Meaning |
|---|---|
WOULD_CARRY | Dry run only: a secret would be created from the legacy password. |
CARRIED | A secret was created from the legacy password and deployed; the legacy credential is unchanged. |
CARRIED_RAW | The stored password was not a ciphertext of this installation's key — plain text, or sealed under a different installation's key — so it was carried over as-is, byte for byte, matching what the legacy runtime itself expected. Rotating the client's secret afterward is recommended. |
CARRIED_REFERENCE | The legacy password was a variable or expression reference (a value starting with ${ or #{); the reference itself is carried as a secret-reference generation and is resolved by the gateway on every request, exactly as it was resolved on the legacy password field. |
ALREADY_HAS_MATERIAL | The client already has a secret of its own; running again changes nothing. |
SOURCE_MISSING | The legacy credential this client was created from no longer exists. |
SKIPPED_EXTERNAL_IDENTITY_SOURCE | The legacy credential authenticates through LDAP, a database, an API, or an external identity provider rather than a locally stored password — there is no password to carry. |
SKIPPED_BLANK_SECRET | The legacy credential has no password stored, or it is empty once read. |
SKIPPED_REVOKED | The client has been revoked and takes no new material. |
FAILED_DECRYPT | The stored password looked like a ciphertext of this installation's key but decrypted to nothing — a corrupt value; nothing is carried for this row. |
FAILED_ISSUE | The secret could not be created — for example a concurrent change to the client — and nothing was written for this row. |
SKIPPED_REFERENCE_NOT_SUPPORTED is no longer produced: a variable or expression password is now carried over as CARRIED_REFERENCE instead of being left on the legacy path.
A carried-over secret is always stored in Encrypted mode, so recoveryState reads AVAILABLE for every CARRIED row regardless of recoverySupported — a platform-encrypted copy is kept even without a recovery key. recoverySupported only reports whether a recovery key is additionally available to seal the envelope Reveal prefers. A CARRIED_REFERENCE row is different: there is no local secret material to recover — the value is resolved by the gateway at request time — so its recoveryState reads UNAVAILABLE, since the material belongs to whatever resolves the reference, not to this installation.
Permissions for This Endpoint
isSystemAdmin() or identity management manage permission in the target project, for both the dry run and the real carry-over — a dry run is authorized as the write it could become — and an installation-wide run (blank projectId) always requires system administrator permission. The sibling GET /api/api-clients/migration/inventory (what each legacy record would become) needs identity management view permission; POST /api/api-clients/migration/backfill?projectId=&apply= (derive the API clients; apply defaults to false = dry run) follows the same manage rule as this endpoint.
Portal API Clients (Portal Management API)
Most of this page documents /apiops/** (and the two Manager REST sections just above, under /api/api-clients/**). The five endpoints below are a third surface, /apiportal/management/** — the base path the API Portal's own backend calls on behalf of a signed-in developer to run the API Clients tab of an application — see Application Promote → API Clients for the portal-facing behavior this documents the wire shape of. There is no equivalent for it under /apiops/**.
An application can own one or more typed API clients — see API Client Fields above for the entity these endpoints operate on, scoped here to one portal application (ownerType=APPLICATION). Every endpoint below is scoped to one appId, and a apiClientKey that exists but is not owned by that application answers the same "not found" error as a key that does not exist at all — this surface never confirms or denies whether a key belongs to a different application.
Endpoints
GET /apiportal/management/portal/api-clients/get-by-app-id/{appId}— list the application's api clients (revoked ones included, as history), without secretsPOST /apiportal/management/portal/api-clients?appId={appId}— create a new api client, body{"name": "..."}POST /apiportal/management/portal/api-clients/{apiClientKey}/rotate?appId={appId}— issue a new secret for an existing clientPOST /apiportal/management/portal/api-clients/{apiClientKey}/suspend?appId={appId}&reason={reason}— suspend a client (reasonoptional)POST /apiportal/management/portal/api-clients/{apiClientKey}/resume?appId={appId}— resume a suspended client
List — Response Shape
GET .../get-by-app-id/{appId} returns an array of:
[
{
"apiClientKey": "...",
"clientId": "...",
"name": "Billing integration",
"operationalState": "ACTIVE",
"readiness": "READY",
"migratedFromLegacy": false,
"version": 3,
"secretVersions": [
{
"versionId": "...",
"label": "Portal",
"status": "ACTIVE",
"createdAt": "2026-08-01T10:00:00Z",
"validFrom": "2026-08-01T10:00:00Z",
"expiresAt": null
}
]
}
]
| Field | Description |
|---|---|
apiClientKey | The client's management-reference key. Used to address it in the rotate/suspend/resume paths above. |
clientId | The OAuth client_id presented at authentication. For a client created by the legacy-record migration, this is the migrated credential's old username. |
operationalState | DRAFT, ACTIVE, SUSPENDED, or REVOKED — whether the client is currently allowed to authenticate. A client this surface creates is always born ACTIVE; DRAFT is only seen on a client the legacy-record migration created but has not yet been activated. |
readiness | READY, PENDING_SECRET, or UNRESOLVED_DEPENDENCY — whether the client actually has usable secret material, independent of operationalState. |
migratedFromLegacy | true when this client was derived from the application's older, single-key credential by the legacy-record migration. |
secretVersions | Every secret generation's calendar (version id, label, status, created/valid-from/expires timestamps) — never the secret value itself. What a generation's status reads while a rotation's previous secret is still inside its grace window is covered in Grace Period and Scheduled Rotation; it is not repeated here. |
Never present in this DTO: a secret value, a verifier, a recovery envelope, or a trailing hint of any kind — this is the deliberate difference from the manager's own ApiClientDTO. The only response that ever carries a secret value is the create/rotate response below, and only once.
Create and Rotate — Response Shape
Both POST .../api-clients?appId=... (create) and POST .../api-clients/{apiClientKey}/rotate?appId=... (rotate) answer with:
{
"client": { "...": "the PortalApiClientDTO shape above, after the write" },
"secretVersionId": "...",
"clientSecretOnce": "the plaintext secret — present in this response only"
}
clientSecretOnce is the only place this API ever returns a secret value; neither the list endpoint above nor any other call echoes it again. It is never logged and never appears in an audit trail. Both responses are served with Cache-Control: no-store and Pragma: no-cache.
On rotate, the previous secret generation keeps authenticating through its grace window rather than stopping immediately — see Suspending, Resuming and Revoking an API Client for the underlying mechanism this reuses.
Suspend and Resume — Response Shape
Both answer with the client's PortalApiClientDTO (the list-response shape above) as it now stands, after the transition. reason on suspend is optional; when omitted, a default reason is stored. Suspend and resume follow the same operationalState transition rules as every other api client — see API Client Fields above — so, for example, resuming a client that is not currently SUSPENDED is refused.
Errors
| Condition | Response |
|---|---|
appId does not resolve to an application | 400, error key portal.apiClient.applicationNotFound |
apiClientKey does not resolve to a client owned by that application (including a key that belongs to a different application) | 400, error key portal.apiClient.notFoundInApplication |
Create with a blank or missing name | 400, error key portal.apiClient.nameRequired |
Authentication and Permissions for This Section
These endpoints authenticate the same way as the rest of this page: a Personal API Access Token, sent as the Authorization header value — see Authentication below. What is different here is whose token is accepted and what else is sent alongside it: the token's owner must hold a portal role (or be a system administrator), and the caller — the Portal backend, never the developer's own portal session — also sends the acting portal's id in the X-Portal-Id header; see API Portal Settings → Security for the Management API Key Users list a Manager installation can use to further restrict which keys are accepted for a given portal. Authorization for which application and client a given call may touch is enforced one layer up, by the Portal backend itself, before it ever reaches this API — see Application Promote → API Clients.
That outer check is not the same for every one of the five endpoints: listing an application's clients (GET .../get-by-app-id/{appId}) only requires the caller's signed-in developer to be a member of the application's organization, any role included. Creating, rotating, suspending or resuming a client additionally requires a write role in that organization — Owner or Developer — so a Viewer's session is refused by the Portal backend before it ever issues the corresponding request to this API.
Authentication
All endpoints require authentication using a Personal API Access Token.
Permissions
GET Operations (List Credentials, Get Granted Access List)
- User must have
IDENTITY+VIEWpermission in the project - If the asset category does not exist, user must have at least one permission in the project
POST/PUT/DELETE Operations (Create/Update/Delete Credential, Grant/Revoke Access)
- User must have
IDENTITY+MANAGEpermission in the project - For deployment operations (when granting/revoking access with deploy), user must also have
IDENTITY+DEPLOY_UNDEPLOYpermission
Permission Requirements
| Operation | Required Permission |
|---|---|
| List Credentials, Get Granted Access List | IDENTITY + VIEW (or any permission if category doesn't exist) |
| Create/Update/Delete Credential | IDENTITY + MANAGE |
| Grant/Revoke Access (with deploy) | IDENTITY + DEPLOY_UNDEPLOY |
Related Documentation
- Authentication Guide - How to obtain and use API tokens
- Error Handling - Error response formats