kid in its protected header (or under sourceKidOverride if the header has no kid) and re-encrypts the plaintext under newKid. The result is a fresh JWE General JSON Serialization (RFC 7516 §7.2) payload — single-recipient for SIMPLE target keys, multi-recipient for COMPOSITE hybrid targets.
- [Encrypt data (JWE).](https://apidocs.ankatech.co/reference/encryptdata.md): Encrypts Base64‑encoded data using the public key associated with kid. Supported algorithms: ML‑KEM (KEM), RSA, ECC (ECIES). Output: a typed JWE General JSON Serialization (RFC 7516 §7.2) payload — uniform shape across SIMPLE single-recipient keys and COMPOSITE hybrid multi-recipient keys.
- [Decrypt a JWE General JSON Serialization payload](https://apidocs.ankatech.co/reference/decryptdata.md): Parses and decrypts the given JWE using the private key identified by the kid in its protected header. Returns the plaintext (Base64-encoded) plus metadata about key selection, algorithm, and any warnings. Uses JWE General JSON Serialization (RFC 7516 §7.2) uniformly — single-recipient for SIMPLE keys, multi-recipient for COMPOSITE hybrid keys.
- [Decrypt then verify nested JWE(JWS)](https://apidocs.ankatech.co/reference/decryptthenverify.md): Performs decrypt-then-verify operation: decrypts nested JWE to reveal inner JWS, then verifies the signature and extracts original plaintext. Decryption and verification key identifiers are automatically extracted from token headers per JOSE standards (RFC 7516 §4.1.4, RFC 7515 §4.1.4). Both keys must be homogeneous (both symmetric or both asymmetric). COMPOSITE keys are supported via JWE/JWS General JSON Serialization format. Key type combinations: - Symmetric + Symmetric (e.g., AES-GCM + HMAC-SHA256) - Asymmetric + Asymmetric (e.g., ML-KEM + ML-DSA, RSA + RSA) - COMPOSITE + COMPOSITE (e.g., COMPOSITE_KEM_COMBINE + COMPOSITE_SIGNATURE) - COMPOSITE + SIMPLE (any homogeneous combination) Returns 422 if signature verification fails or token format is invalid.
- [Get supported algorithms (crypto-agility)](https://apidocs.ankatech.co/reference/getsupportedalgorithms.md): Returns the algorithm catalogue with full metadata for crypto-agility decisions. Supports optional server-side filtering via query parameters. ## Query Parameters (all optional) | Parameter | Type | Logic | Description | |-----------|------|-------|-------------| | `category` | string | = | CLASSICAL, POST_QUANTUM, or HYBRID | | `status` | string | = | RECOMMENDED, EXPERIMENTAL, or LEGACY | | `minSecurityLevel` | int | >= | Minimum NIST level (1, 3, 5) | | `maxSecurityLevel` | int | <= | Maximum NIST level (1, 3, 5) | | `keyOps` | array | AND | Required operations (must support ALL) | | `standards` | array | AND | Required standards (must have ALL) | | `kty` | array | OR | Published key types (matches ANY, including COMPOSITE). An unpublished token is refused with 400 | | `alg` | array | OR | Algorithms (matches ANY) | ## Response fields | Field | Type | Description | |-------|------|-------------| | `kty` | string | Key type: ML-KEM, ML-DSA, RSA, EC, oct (simple) or COMPOSITE (every hybrid pairing) | | `compositeMode` | string | How a hybrid entry's components are combined. Present only on composite entries | | `alg` | string | Algorithm identifier: ML-KEM-768, RSA-4096 (simple) or X25519+ML-KEM-768 (composite) | | `keyOps` | array | Permitted operations: encrypt, decrypt, sign, verify | | `status` | string | RECOMMENDED, EXPERIMENTAL, or LEGACY | | `sunsetDate` | date | Planned deprecation date (ISO 8601, optional) | | `advisory` | string | Migration or security guidance (optional) | | `securityLevel` | integer | NIST security level (1-5) | | `standards` | array | Endorsing standards, from the single platform vocabulary: ANSSI, BSI, CRYPTREC, ENISA, ETSI, GMT, GOST, IETF, ISO, KISA, MYSEAL, NCA_NCS, NIST, NSA | | `category` | string | CLASSICAL, POST_QUANTUM, or HYBRID | ## Composite algorithm entries Composite algorithms appear as first-class entries with: - `category: "HYBRID"` - `kty`: the single coarse token `COMPOSITE`, for every hybrid pairing - `compositeMode`: how the components are combined — the construction, on its own axis - `alg`: Combined identifier (e.g., "X25519+ML-KEM-768", "Ed25519+ML-DSA-65") Use `category=HYBRID` to filter composite entries only. ## Example queries ``` # Post-quantum algorithms only GET /api/key-management/supported-algorithms?category=POST_QUANTUM # Hybrid/composite algorithms only GET /api/key-management/supported-algorithms?category=HYBRID # High security, recommended algorithms GET /api/key-management/supported-algorithms?minSecurityLevel=3&status=RECOMMENDED # Signature algorithms with NIST compliance GET /api/key-management/supported-algorithms?keyOps=sign,verify&standards=NIST # Find specific composite algorithm GET /api/key-management/supported-algorithms?alg=X25519+ML-KEM-768 ```
- [Get policy cache statistics](https://apidocs.ankatech.co/reference/getcachestats.md): Returns detailed statistics for Caffeine caches and decision cache
- [Policy cache health check](https://apidocs.ankatech.co/reference/gethealth.md): Checks if policy cache system is healthy and Redis Pub/Sub listener is active
- [Verify a detached General-JSON JWS with a supplied public key (stream).](https://apidocs.ankatech.co/reference/verifyinteroperable.md): Accepts two multipart parts: • metadata – JSON (SignatureUtilityApi) containing the caller's detached-JWS object (signatureBase64) and the corresponding publicKey. • file – the raw payload that was signed. The controller ignores any kid inside the JWS header and uses the public key supplied by the caller for streaming verification. On success the endpoint returns a VerifySignatureResponse with isValid=true.
- [Encrypt data with a provided public key (stream).](https://apidocs.ankatech.co/reference/encryptinteroperable.md): Accepts two multipart parts: • metadata – JSON (CryptoUtilityApi) with kty, alg, publicKey • file – binary plaintext Returns a streaming application/octet-stream response without persisting the key.
- [Convert PKCS#7 to JOSE (streaming)](https://apidocs.ankatech.co/reference/convertpkcs7tojosestream.md): Converts PKCS#7/CMS files of any size to modern JOSE (JWE/JWS) detached format. This is a **format conversion** (PKCS#7 container → JOSE container), not an algorithm conversion. Classical algorithms (RSA, ECDSA, AES) are preserved or upgraded (CBC→GCM). **Supported conversions:** - SignedData (1 signer) → JWS detached JSON - EnvelopedData (1 recipient) → JWE detached multipart - SignedAndEnvelopedData (1 signer + 1 recipient) → JWE(JWS) detached multipart **Response format varies by PKCS#7 type:** **SignedData** returns `application/octet-stream`: ```json {"protected":"eyJhbGc...","signature":"dGVz..."} ``` **EnvelopedData** returns `multipart/mixed`: ``` ------ankatech-multipart/form-data parts: • metadata – JSON (VerifyDetachedJwsStreamRequest) with a detached-JWS (General-JSON). • file – the binary payload originally signed. Extracts kid and alg from the header, verifies and returns a VerifySignatureResponse.multipart/form-data fields:metadata â€" JSON body SignStreamRequestfile â€" binary data to signapplication/octet-stream. Output structure depends on key type:multipart/form-data request with a JSON SignEncryptStreamRequest metadata part ({signKid, encryptKid, validityDays?}) and a binary file part. The payload is signed with signKid and the resulting compact JWS is encrypted with encryptKid, so the signature is carried encrypted. signKid and encryptKid MUST be different and homogeneous (both symmetric or both asymmetric). The response is the nested ciphertext streamed as multipart/mixed (JWE header + ciphertext).
- [Re-sign data (streaming).](https://apidocs.ankatech.co/reference/resigndatastream.md): Verifies a detached-JWS (oldJws) under oldKid— either the header's kid or sourceKidOverride if the header lacks a kid—and immediately signs the same payload with newKid. The response is multipart/mixed (PRD §64): PART 1 (application/octet-stream) is the new detached-JWS JSON and the trailing PART 2 (application/json) is a StreamVerdict emitted AFTER the source (old) signature is verified — an INVALID verdict signals a source-signature failure discovered at end-of-stream.oldKid (from the JWE header) and re-encrypts it on the fly with newKid (provided via query parameter). Returns a multipart/mixed response (PRD §64): the new JWE header part, the re-encrypted ciphertext part, and a trailing application/json StreamVerdict part emitted AFTER the source AES-GCM tag is verified at end-of-stream (an INVALID verdict signals a tampered source; no committed ciphertext is promotable). All key-selection metadata is returned in the single `Crypto-Policy-Info` response header.multipart/form-data fields:metadata – JSON body EncryptStreamRequestfile – binary plaintextapplication/jose+json) is a JWE General-JSON header with detached ciphertext.application/octet-stream) is the proprietary envelope [wkLen]‖wrappedKey‖[ivLen]‖iv‖ciphertext‖tag streamed on-the-fly.application/jose+json — JWE General-JSON header (detached ciphertext).application/octet-stream — proprietary binary envelope followed by the ciphertext and GCM tag.application/octet-stream) is the decrypted plaintext and the trailing PART 2 (application/json) is a StreamVerdict emitted AFTER the end-of-stream AES-GCM tag is verified ({verdict,operation,reason,correlationId,timestamp}). Because the GCM tag is only known at end-of-stream, a tamper is reported through an INVALID verdict in PART 2 — never a silently truncated success.multipart/mixed (outer JWE header + ciphertext), decrypts it and verifies the recovered inner JWS. The response is multipart/mixed: PART 1 is the decrypted plaintext, PART 2 a JSON verdict carrying gcmTagVerdict and signatureVerdict — both MUST be VALID for the overall verdict to be VALID.
- [OAuth 2.0 token endpoint (RFC 6749/8693)](https://apidocs.ankatech.co/reference/createtoken.md): OAuth 2.0 token endpoint supporting four grant types. **Request Format:** application/x-www-form-urlencoded (OAuth 2.0 standard) **Supported Grant Types:** - password: User authentication (RFC 6749 Section 4.3) - client_credentials: Application authentication (RFC 6749 Section 4.4) - refresh_token: Token refresh (RFC 6749 Section 6) - urn:ietf:params:oauth:grant-type:token-exchange: Impersonation (RFC 8693) **Security:** - Rate limited: 10 attempts/min for password grant, 30/min for refresh - Account lockout: 5 failed attempts → 15 min lockout - Tokens are JWTs signed with RS256 (RSA-3072) **Token Claims:** - HUMAN tokens: audience=ankasecure-admin, includes loginId (email) - APPLICATION tokens: audience=ankasecure-core, includes kp/kpv (key permissions) All tokens include: sub, iss, aud, exp, iat, jti, tenantId, scopes, userType
- [Token revocation (RFC 7009)](https://apidocs.ankatech.co/reference/revoketoken.md): Revokes an access token or a refresh token, per OAuth 2.0 RFC 7009. **Both token planes are supported.** RFC 7009 §2 makes refresh-token revocation the mandatory half. A JWT access token is blacklisted by its `jti` until it expires; an opaque refresh token is deleted outright. The optional `token_type_hint` only **reorders** the search and never ends it (§2.1), so a wrong hint still revokes; an unrecognized or empty hint is treated as absent and is never an error. **Authorization is OWNERSHIP, not a scope.** Any authenticated caller may reach this endpoint, and may revoke only a token issued to itself (§2.1). A token belonging to another principal is silently left alone — including for a platform administrator, who gains no privilege here. `DELETE /api/v3/auth/token/s/{tokenId}` remains the only administrative revocation path. **Request format** — `application/x-www-form-urlencoded`, as RFC 7009 §2.1 specifies. A JSON body is answered `400` carrying the RFC 6749 §5.2 error object, not `415` — see the 400 response below. **Outcomes** — per §2.2 the response is `200` with an empty body when the token was revoked, was already gone, never existed, was not the caller's, or was not parseable. These are byte-identical on purpose: a distinguishable refusal would let any caller use this endpoint to discover whether a token value exists and whom it belongs to. **The one exception is `503`**, returned when the revocation store is unavailable. RFC 7009 §2.2: on a 503 the client must assume the token still exists. It is decided before any per-token outcome is known, so it discloses nothing — and answering 200 there would report a revocation that did not happen.
- [Logout and clear refresh token cookie](https://apidocs.ankatech.co/reference/logoutsession.md): Revokes the current access token and clears the refresh token cookie. Call this endpoint before client-side cleanup to ensure server-side token revocation and cookie deletion. **Security:** - Clears httpOnly refresh token cookie - Revokes access token (adds to blacklist) - Works even with expired access tokens
- [Token introspection (RFC 7662)](https://apidocs.ankatech.co/reference/introspecttoken.md): Reports whether a token is currently active, per OAuth 2.0 RFC 7662. **What `active` means** (RFC 7662 §2.2) — the token was issued by *this* authorization server, has not been revoked, and is within its validity window. That is the whole determination. The token's `aud` is **reported** in the response and takes part in no decision: this service issues several audiences (`ankasecure-admin` for human users, `ankasecure-core` for application actors), and all of them introspect identically. **Both token planes are searched.** The value may be a JWT access token or an opaque refresh token. Per RFC 7662 §2.1 the optional `token_type_hint` only **reorders** the search — it never ends it, so a wrong hint still finds the token. An unrecognized or empty hint is treated as absent and is never an error. **Request format** — `application/x-www-form-urlencoded`, as RFC 7662 §2.1 specifies. A JSON body is answered `400` carrying the RFC 6749 §5.2 error object, not `415` — see the 400 response below. **Authorization** — requires `platform.token.read` (all tenants; restricted to PRIVATE_CLOUD and ON_PREMISE deployments) or `auth.tenant.token.read` (the caller's own tenant, in every deployment mode). **Disclosure** — an inactive token yields `{"active": false}` and nothing else: no claims, and no distinction between expired, revoked, belonging to another tenant, and never having existed. A token belonging to another tenant is reported inactive rather than refused, because a `403` would confirm the token exists while declining to describe it.
- [Admin: View revoked tokens](https://apidocs.ankatech.co/reference/getblacklist.md): View currently blacklisted (revoked) tokens. **Authorization model:** - `platform.token.read`: returns global blacklist (optionally filtered by `?tenantId=`). Requires PRIVATE_CLOUD or ON_PREMISE — rejected in SaaS. - `auth.tenant.token.read`: returns blacklist scoped to caller's tenant only. Any client-supplied `?tenantId=` parameter is silently ignored. Results are limited to 5000 entries per call. Truncation is indicated by `"truncated": true` in the response.
- [Admin: Force revoke any token](https://apidocs.ankatech.co/reference/forcerevoketoken.md): Forcibly revoke a token by JTI. **Authorization model:** - `platform.token.revoke`: revoke any tenant's token. Requires PRIVATE_CLOUD or ON_PREMISE deployment — rejected in SaaS mode. - `auth.tenant.token.revoke`: revoke tokens belonging to users in the caller's own tenant. Works in all deployment modes. **Anti-enumeration:** Returns 404 when the JTI is not found OR belongs to a different tenant (indistinguishable from non-existent).
- [Complete password reset](https://apidocs.ankatech.co/reference/resetpassword.md): Validates reset token and sets new password. Invalidates all active sessions. **Security:** - Validates single-use reset token (JWT with 1h expiration) - Revokes all existing tokens for the user (force re-authentication) - Password must meet strength requirements - Token blacklisted after successful reset to prevent reuse **Process:** 1. Validates reset token (signature, expiration, purpose claim) 2. Checks token hasn't been used (single-use via Redis blacklist) 3. Updates user password with secure PBKDF2 hashing 4. Revokes all active user tokens 5. Adds reset token to blacklist 6. Returns success confirmation
- [Request password reset link](https://apidocs.ankatech.co/reference/forgotpassword.md): Sends a password-reset link to the address if an account exists for it. **The request carries the e-mail and nothing else.** The tenant is resolved from the address, server-side, exactly as sign-in already resolves it. A payload naming a tenant — or carrying any member other than email — is refused with a 400: this operation is pre-authentication, so a tenant field would either demand an identifier the person does not have, or become the tenant-enumeration oracle the post-authentication tenant picker is filtered to prevent. **One link per matching account.** When the address matches accounts in more than one tenant, one reset link is sent for each. The mailbox owner learns which tenants they belong to; the HTTP caller learns nothing, because the response does not change. **Security:** - The 202 is unconditional and its body is a fixed constant, identical for a match in several tenants, in exactly one, in none, and for an internal failure (OWASP ASVS 2.1.11). The body never echoes the submitted address - Covered by the service-wide request rate limiter; no per-endpoint override - Response timing is deliberately NOT equalized, and the defence does not rest on it: one mail per matching account costs measurably different work per match, and equalizing that needs real mechanism rather than an assertion - The reset token is single-use and expires in 1 hour (configurable) **Process:** 1. Validates the e-mail's shape (the only rejection this endpoint makes) 2. Resolves every active account for the address, across tenants (internal, never disclosed) 3. Generates a distinct single-use reset token per matching account 4. Publishes one reset mail per matching account 5. Returns the same generic acknowledgement whatever step 2 found
- [Change your own password](https://apidocs.ankatech.co/reference/changepassword.md): Replaces the password of the account named by the access token presented with this request. It changes no other account: the request body carries no user, e-mail or tenant identifier, so there is nothing in it to aim elsewhere. **The current password is required even though the session is already valid.** Reauthentication on a credential change is the settled industry pattern (OWASP ASVS 2.1.x; Keycloak, Auth0 and Okta all require it) and it is what stops a stolen access token from taking the account over permanently. **Password policy** — at least 12 characters, with at least one lower-case letter, one upper-case letter, one digit and one special character; and different from the last 5 passwords of this account. **One refusal for three causes.** A wrong current password, a new password that violates the policy and a new password that repeats a recent one all answer with a byte-identical 400, so a caller cannot tell which was true. A subject whose credential is held by an external identity provider is refused differently, with 409 and its own problem type: telling such a caller their password is wrong for an account that has none would be untrue and unactionable. **On success every live token of the account is refused from that moment on** and the caller is expected to sign in again. A refusal writes nothing.
- [Activate user account](https://apidocs.ankatech.co/reference/activateaccount.md): Activates a user account that was created with requirePasswordChange=true. This endpoint validates the single-use activation token and allows the user to set their own password. **Applicable to ALL user types:** - Tenant administrators (provisioned by platform admins) - Platform administrators (created by platform admins in tenant 001) - Regular users (created by tenant admins) - Any user account requiring password setup on first login **Process:** 1. Validates activation token (signature, expiration, purpose claim) 2. Checks token hasn't been used (single-use via Redis blacklist) 3. Updates user password with secure PBKDF2 hashing 4. Marks account as active (removes requirePasswordChange flag) 5. Adds token JTI to Redis blacklist to prevent reuse **Token Requirements:** - Valid RS256 signature from Admin-API - Purpose claim = 'ACCOUNT_ACTIVATION' - Not expired (configurable, default: 24h) - Not previously used (checked via Redis blacklist) - Valid tenant and user UUIDs in claims **Security Model:** This endpoint enables zero-knowledge user provisioning where administrators never see or know user passwords. Users set their own passwords, following OWASP ASVS 2.1.1 and industry best practices (AWS Cognito, Azure AD, Auth0 pattern).
- [Get permitted algorithms for tenant (policy-filtered)](https://apidocs.ankatech.co/reference/listsupportedalgorithms.md): Returns cryptographic algorithms filtered by tenant-specific algorithm policies. ## Policy Resolution 1. Tenant-specific algorithm policy (if configured) — applied as a filter 2. When no active policy exists for the tenant, the full catalog is returned (no restriction) ## Filtering Logic - Algorithms explicitly denied by policy are excluded - Algorithms not mentioned in policy are included by default - Composite algorithms inherit filtering from component algorithms ## Query Parameters (all optional, same as Core API) | Parameter | Type | Logic | Description | |-----------|------|-------|-------------| | `category` | string | = | CLASSICAL, POST_QUANTUM, or HYBRID | | `status` | string | = | RECOMMENDED, EXPERIMENTAL, or LEGACY | | `minSecurityLevel` | int | >= | Minimum NIST level (1, 3, 5) | | `maxSecurityLevel` | int | <= | Maximum NIST level (1, 3, 5) | | `keyOps` | array | AND | Required operations (must support ALL) | | `standards` | array | AND | Required standards (must have ALL) | | `kty` | array | OR | Key types (matches ANY, including COMPOSITE) | | `alg` | array | OR | Algorithms (matches ANY) | ## Use Cases - Populate key generation UI with permitted algorithms - Validate algorithm selection against policy - Audit effective algorithm permissions ## Authentication & Authorization - JWT with tenantId claim matching path parameter - Scope: admin.tenant.algorithms.read - S2S calls from Core API: tenantId validation skipped (already validated in Core API)
- [Flush role-catalog cache across all subscribers](https://apidocs.ankatech.co/reference/flushrolecatalog.md)
- [Get scopes for a tenant-scoped role (system or custom) (S2S only)](https://apidocs.ankatech.co/reference/gettenantrolescopes.md)
- [Get scopes for a SYSTEM role (S2S only)](https://apidocs.ankatech.co/reference/getsystemrolescopes.md)
- [Re-derive a tenant's TSA usage policy (S2S only)](https://apidocs.ankatech.co/reference/resolvetsausagepolicy.md): Resolves the tenant's effective RFC 3161 TSA usage policy from the database (tenant override → deployment default → code DEFAULT DISABLED, never deny-all) and re-projects it to Redis via the single existing projector. Returns 200 with the flat scalar policy for every existing tenant. A non-existent tenant returns a uniform, oracle-safe 404 that does not confirm existence. Requires the ankasecure-core S2S issuer and the admin.s2s.tsa-usage-policy.read scope.
- [Get a deployment-default identity provider (masked)](https://apidocs.ankatech.co/reference/getplatformidp.md): Returns the provider with masked secrets. Required scope: admin.platform.idp.manage.
- [Edit the deployment-default provider configuration](https://apidocs.ankatech.co/reference/replaceplatformidp.md): Replaces the structural config. The kind is immutable (409 on change). Editing CLEARS the self-test verdict and returns the provider to DECLARED/disabled — it must re-pass the self-test before it can be re-enabled. The deployment-default secret is a Docker Swarm secret and is never persisted from the body. Required scope: admin.platform.idp.manage.
- [Delete the deployment-default provider](https://apidocs.ankatech.co/reference/deleteplatformidp.md): Deletes the provider, freeing the one-per-kind slot. Cascades all three child sets and its link intents, and UNBINDS every account bound to it across all tenants: both federation columns are cleared, a password change is required (binding had replaced the local password with an unusable value) and the revocation epoch is advanced so the deleted federation's sessions end. Refused with 409 when that would leave a tenant with no account able to sign in. Required scope: admin.platform.idp.manage.
- [List a tenant's claimed email domains (platform)](https://apidocs.ankatech.co/reference/listplatformemaildomains.md): Required scope: admin.platform.idp.manage.
- [Claim an email domain for a tenant (platform)](https://apidocs.ankatech.co/reference/createplatformemaildomain.md): Creates an UNVERIFIED claim for the path tenant. Required scope: admin.platform.idp.manage.
- [Verify a tenant's email domain (platform-admin manual)](https://apidocs.ankatech.co/reference/verifyplatformemaildomain.md): Marks the claim verified (method=PLATFORM_ADMIN) so it routes a login. Idempotent. 409 if the domain is already verified for another tenant. Required scope: admin.platform.idp.manage.
- [List deployment-default identity providers](https://apidocs.ankatech.co/reference/listplatformidp.md): Returns the deployment-scope providers (masked secrets). Required scope: admin.platform.idp.manage.
- [Declare the deployment-default identity provider](https://apidocs.ankatech.co/reference/createplatformidp.md): Creates a DEPLOYMENT-scope provider in the DECLARED/disabled state. Run the self-test, then enable. Required scope: admin.platform.idp.manage.
- [Run the connectivity self-test](https://apidocs.ankatech.co/reference/testplatformidp.md): Runs a live connectivity probe and returns a structured result. Does NOT activate the provider. Required scope: admin.platform.idp.manage.
- [Enable (activate) the identity provider](https://apidocs.ankatech.co/reference/enableplatformidp.md): Transitions DECLARED → ACTIVE, ONLY if the most-recent self-test passed (409 otherwise). Required scope: admin.platform.idp.manage.
- [Disable the deployment-default provider](https://apidocs.ankatech.co/reference/disableplatformidp.md): Sets enabled=false so the provider stops being used at login, without deleting it. Re-enable via /enable (the prior self-test verdict is retained). Local break-glass login remains governed by allow-local-fallback. Required scope: admin.platform.idp.manage.
- [Validate a candidate provider config (stateless)](https://apidocs.ankatech.co/reference/validateplatformidp.md): Runs the connectivity probe against the SUBMITTED config and returns a structured result WITHOUT persisting anything — no provider is created, loaded, or mutated, and no self-test verdict is recorded. Use it to check a configuration (e.g. from the declare wizard) before committing to declare. Required scope: admin.platform.idp.manage.
- [Unbind the accounts whose identity provider no longer exists](https://apidocs.ankatech.co/reference/reconcileplatformorphanedfederatedbindings.md): Clears idp_id and external_subject on every account reported by the companion read, and returns those bindings as they were before the repair (PRD §152.7). The accounts are unbound, never deleted — the same decision the provider deletion path makes, and for the same reason: these principals carry roles and are referenced by audit rows. Afterwards each is an ordinary local account, so "Authorize a link" can bind it to a live provider — the remedy an orphan could not reach. Idempotent by construction, not by a guard: the scan selects accounts whose idp_id names no existing provider, so a repaired account is outside its result set. A second call finds nothing, writes nothing and audits nothing, and answers 200 with empty bindings. When truncated is true there were more orphans than one pass repairs — repeat until it is false. Confirmation is required. expectedBindings must equal the number of orphans the companion read reports. A mismatch answers 409 and repairs nothing: this is a single POST that mutates principal state in every tenant at once, so it must not be reachable from a mistyped URL or a stale tab. Required scope: admin.platform.idp.manage.
- [List deployment-default group→role mappings](https://apidocs.ankatech.co/reference/listplatformmappings.md): Optionally filtered by provider id. Required scope: admin.platform.idp.manage.
- [Create a group→role mapping for the deployment-default provider](https://apidocs.ankatech.co/reference/createplatformmapping.md): Maps a raw IdP group to an ANKASecure role at the platform (ROOT) scope — the deny-by-default admission input. Unlike the tenant plane, the deployment plane uses the single admin.platform.idp.manage scope (no separate mapping scope). The mapped role is checked at declaration time so the mapping cannot be created in a state the federated login path is unable to consume: an unknown role, or a custom role owned by another tenant, is refused 404 (both causes are collapsed into one oracle-safe body); a service-to-service-only role is refused 400; and a role that is not assignable inside a tenant of this type is refused 422 federated-role-not-assignable. Cross-surface divergence, deliberate: the SAME (role, tenant type) matrix answers 400 invalid-input on assignUserRoles and 422 here. The user-assignment surface rejects a malformed assignment request, whereas this surface accepts a well-formed declaration whose semantics cannot be satisfied — which is what 422 means. Required scope: admin.platform.idp.manage.
- [List the deployment's verified email domains](https://apidocs.ankatech.co/reference/listplatformidpdomains.md): Returns the applicable verified set, ordered by domain. An EMPTY list is a meaningful state, not an error: it means no federated login can be admitted, and it is exactly what blocks enabling an identity provider (409 federation-domain-verification-required). Unlike the two mutating verbs, this read is available on every deployment type — an operator diagnosing a refused login must be able to see the set that refused it.
- [Declare an email domain this deployment federates](https://apidocs.ankatech.co/reference/declareplatformidpdomain.md): Adds one domain to the deployment's applicable verified set. Federated logins are admitted only when the domain they derive is a member of that set, compared as exact equality over the normalized label — never a suffix match, so declaring acme.com does NOT admit evil-acme.com or acme.com.attacker.test. On this plane declaring is verifying: the row is written verified = true with verificationMethod = PLATFORM_ADMIN, and verifiedBy is taken from the security context. There is no DNS challenge and no verify verb (PRD §136 D7). Re-declaring a domain already in the set is a no-op that returns the existing row — the operator's intent is already satisfied, and a 409 would send them to diagnose a conflict that does not exist.
- [List deployment-default login domain guards](https://apidocs.ankatech.co/reference/listplatformdomainguards.md): Optionally filtered by provider id. Required scope: admin.platform.idp.manage.
- [Create a login domain guard for the deployment-default provider](https://apidocs.ankatech.co/reference/createplatformdomainguard.md): When a provider has ≥1 guard, only federated identities whose email domain matches an allowlisted domain may complete login. A guard grants no role. Required scope: admin.platform.idp.manage.
- [List deployment-default email/domain admission rules](https://apidocs.ankatech.co/reference/listplatformadmissionrules.md): Optionally filtered by provider id. Required scope: admin.platform.idp.manage.
- [Create an email/domain admission rule for the deployment-default provider](https://apidocs.ankatech.co/reference/createplatformadmissionrule.md): Maps a federated email address or normalized domain to an ANKASecure role at the platform (ROOT) scope — the email/domain half of the deny-by-default admission input. A DOMAIN rule may not grant a platform-scope / wildcard role (422); an EMAIL rule is uncapped by that privilege ceiling. Independently of that ceiling, and on BOTH rule types, the admitted role is checked at declaration time so the rule cannot be created in a state the federated login path is unable to consume: an unknown role, or a custom role owned by another tenant, is refused 404 (both causes are collapsed into one oracle-safe body); a service-to-service-only role is refused 400; and a role that is not assignable inside a tenant of this type is refused 422 federated-role-not-assignable. Cross-surface divergence, deliberate: the SAME (role, tenant type) matrix answers 400 invalid-input on assignUserRoles and 422 here. The user-assignment surface rejects a malformed assignment request, whereas this surface accepts a well-formed declaration whose semantics cannot be satisfied — which is what 422 means. Required scope: admin.platform.idp.manage.
- [List accounts bound to an identity provider that no longer exists](https://apidocs.ankatech.co/reference/listplatformorphanedfederatedbindings.md): Reports every live account whose idp_id names a provider that has been deleted (PRD §152.7). Such an account is unreachable: the federated login resolves subject-first on (tenant_id, idp_id, external_subject), so the binding is never matched, every later login is treated as a first login, and it then collides with the email address the orphan itself still occupies — the operator sees a repeating 409 with no remedy, because "Authorize a link" relinks an account TO a provider and that provider is gone. Deleting a provider now unbinds its accounts, so no NEW orphan can be created. This read exists for the ones already written in environments deployed before that, and it is the read an operator inspects before invoking the repair. It mutates nothing. Results span EVERY tenant: a deployment-scope provider is stored under the platform tenant and inherited by all of them, so its orphans are not confined to one. The response is a bounded pass: { bindings, truncated }. The ceiling applies to the ORPHANS, not to the federated population, so repeating the call advances — each repaired account leaves the result set. While truncated is true, bindings remain unexamined and the call should be repeated. Required scope: admin.platform.idp.manage.
- [Get the derived federation callback/ACS URLs](https://apidocs.ankatech.co/reference/derivedurls.md): Returns the read-only OIDC callback and SAML ACS URLs the deployment derives from its public-edge base URL (base + fixed edge path, trailing-slash normalized). These are the values the operator must register with the external IdP — never operator free-text. Required scope: admin.platform.idp.manage.
- [Remove a tenant's email domain claim (platform)](https://apidocs.ankatech.co/reference/deleteplatformemaildomain.md): Cross-tenant/absent id returns 404 (oracle-safe). Required scope: admin.platform.idp.manage.
- [Remove the provider's LDAPS trust anchors](https://apidocs.ankatech.co/reference/deleteplatformidptrustanchors.md): Removes the configured trust anchors, returning the provider to the JDK default trust store at login. This is the ONLY way to clear them: an `update` whose config omits `trustAnchors` PRESERVES the configured anchors, because the read side never returns them, so an edit form opens with the field empty and omitting it must not destroy the control. Clearing changes what the login plane trusts, so — exactly like any other trust-material edit — it returns the provider to DECLARED/disabled and a passing self-test is required before it can be enabled again. Idempotent: clearing a provider that has no anchors succeeds and changes nothing. This verb resolves the provider by id ALONE and therefore also reaches a TENANT-scope provider: trust anchors are a platform-plane control, so a tenant-scope row that acquired them out of band is refused by the tenant verbs and this is the only path that can remediate it. Required scope: admin.platform.idp.manage.
- [Delete a deployment-default group→role mapping](https://apidocs.ankatech.co/reference/deleteplatformmapping.md): Cross-scope attempts return 404 (oracle-safe). Required scope: admin.platform.idp.manage.
- [Remove a domain from the deployment's verified set](https://apidocs.ankatech.co/reference/deleteplatformidpdomain.md): Removes one domain. From the next login onward, identities in that domain are no longer admitted. Existing accounts are not deleted, disabled or unlinked. Removal governs admission, not the accounts admission already created — which is why the membership read is never cached: the removal takes effect on the next login rather than at the end of a TTL. Removing the LAST domain leaves the deployment unable to admit any federated login. That is permitted and warned about, not blocked: an operator correcting a mistaken entry must not have to keep it, and blocking here would be a refusal they cannot act on.
- [Delete a deployment-default login domain guard](https://apidocs.ankatech.co/reference/deleteplatformdomainguard.md): Cross-scope attempts return 404 (oracle-safe). Required scope: admin.platform.idp.manage.
- [Delete a deployment-default email/domain admission rule](https://apidocs.ankatech.co/reference/deleteplatformadmissionrule.md): Cross-scope attempts return 404 (oracle-safe). Required scope: admin.platform.idp.manage.
- [Get deployment information](https://apidocs.ankatech.co/reference/getdeploymentinfo.md): Returns deployment model (SaaS/Private/On-Premise) and platform capabilities. Used by Admin Console to: • Show/hide tenant selector for Platform Admin • Enable/disable cross-tenant features • Display deployment-specific help text Authorization: Any authenticated user can access this endpoint.
- [List the scope catalog used by the Role Management UI](https://apidocs.ankatech.co/reference/getscopecatalog.md): Returns every leaf scope (granular `1 row = 1 endpoint` row in the roles table) that the platform admin may include in a system role's expanded scope set. Server-side filters: `is_system=false`, `s2s_only=false`, `reserved_reason IS NULL`, `deletedAt IS NULL`. Ordered by category, then scope_name. **Reserved scopes are excluded.** A scope whose catalog row carries a `reserved_reason` is declared but enforced by no handler, so composing it into a role would grant an authority that unlocks nothing; the response therefore omits the row entirely rather than flagging it, and the console does not filter this set again. Required scope: `admin.platform.scopes.read`. Required JWT tenantId: `ROOT_TENANT_ID` (defense-in-depth).
- [List Cryptographic Exchanges](https://apidocs.ankatech.co/reference/listexchanges.md): Returns a paginated list of Exchanges in the caller's tenant. Filter by status, counterparty (case-insensitive substring), application UUID, owner principal UUID, and name (case-insensitive substring). Default sort is `updatedAt DESC`. Page size is bounded to 50.
- [Create a Cryptographic Exchange in DRAFT](https://apidocs.ankatech.co/reference/createexchange.md): Creates a new Exchange with all references nullable. The 6 references are resolved by subsequent PATCH calls before validate / activate. Audit event admin.cryptographicexchange.create is published automatically by the JPA listener pattern (extends AuditableWithEventsEntity).
- [Validate Exchange (DRAFT to READY)](https://apidocs.ankatech.co/reference/validateexchange.md): Runs the 6 predicates: all references resolved, references belong to caller's tenant, dates coherent, etc. On success, the Exchange transitions to READY. Predicate 4 (capability vs Asset.key_ops) is INFORMATIONAL in v1 per OQ-4 default; the BLOCKING enforcement happens in core-api at crypto-operation time. Returns the ValidationResultResponse with violations[] and warnings[].
- [Suspend Exchange (ACTIVE -> SUSPENDED)](https://apidocs.ankatech.co/reference/suspendexchange.md): Moves an ACTIVE Cryptographic Exchange to SUSPENDED and invalidates the linked Capability Grant projection, so runtime authorization stops honouring it. The body is an OPTIMISTIC-LOCKING ENVELOPE that also carries the operator's justification. version is the discriminator last read from the exchange detail response; a stale value is refused with 409 and nothing is written. reason is MANDATORY (PRD 231). It is recorded verbatim at canonical position 21 of the signed AdminAuditEvent, which is what makes the suspension answerable later: an audit row can no longer say that an exchange was suspended without saying why. It is bounded at AuditFieldBounds.MAX_REASON_LENGTH (1,000 UTF-8 BYTES, not characters - the unit the signed preimage and every carrier downstream of it are measured in) - the same bound applied before signing, so no accepted value is silently truncated on its way into the preimage. The reason is NEVER written to a log line (231.19). Authorization: requires scope admin.tenant.exchange.suspend. The tenant boundary is resolved from the JWT claim, never from the path.
- [Revoke Exchange (any non-terminal -> REVOKED) — irreversible](https://apidocs.ankatech.co/reference/revokeexchange.md): Moves a non-terminal Cryptographic Exchange to REVOKED, populates revokedAt / revokedBy / revokedReason atomically with the transition, and invalidates the linked Capability Grant projection. Terminal and irreversible. This is the ONE operation in the family whose justification has TWO homes: the revokedReason property of the resource, and canonical position 21 of the signed AdminAuditEvent. Both are written from the SAME submitted value, so a divergence between them is a defect and not a degree of freedom. reason is MANDATORY (PRD 231), bounded at AuditFieldBounds.MAX_REASON_LENGTH (1,000 UTF-8 BYTES, not characters) - the same bound applied before signing - and NEVER written to a log line (231.19). Authorization: requires scope admin.tenant.exchange.revoke.
- [Resume Exchange (SUSPENDED -> ACTIVE) — shares scope with activate per OQ-19](https://apidocs.ankatech.co/reference/resumeexchange.md): Returns a SUSPENDED Cryptographic Exchange to ACTIVE and republishes the linked Capability Grant projection. The body carries the optimistic-lock version and NOTHING ELSE. Resume records no justification: restoring a capability generates no grievance for the audit trail to answer, which is the PRD 224 exclusion this operation inherits. A body that still carries reason is REFUSED with 400 rather than silently ignored - the request type has no such component and strict deserialization is enabled - so a caller migrating off the retired shared transition body learns that the value was never being recorded. Authorization: requires scope admin.tenant.exchange.activate, the same scope that governs activation (OQ-19). There is no .resume scope.
- [Clone Exchange to DRAFT](https://apidocs.ankatech.co/reference/cloneexchange.md): Copies references and metadata from the source Exchange into a new Exchange in DRAFT. cloned_from_exchange_id is populated for provenance. Allowed from any source status including REVOKED (OQ-17). Name defaults to source name + -clone-N when omitted.
- [Activate Exchange (READY -> ACTIVE) — atomic Grant creation](https://apidocs.ankatech.co/reference/activateexchange.md): Atomically creates the Capability Grant (resolved from the Exchange's references + the explicit `capability` field per OQ-4) and transitions the Exchange to ACTIVE. The 1:1 Exchange-Grant invariant is enforced at the DB layer via the UNIQUE constraint on grant_id. Predicate 4 (capability vs Asset.key_ops) is INFORMATIONAL in v1; authoritative enforcement happens in core-api at crypto-operation time.
- [Get Cryptographic Exchange detail](https://apidocs.ankatech.co/reference/getbyid.md): Returns the full eager-loaded Exchange detail. Returns 404 (NEVER 403) when the Exchange does not exist OR exists in a different tenant — this avoids the existence oracle described in security-pre-review §Slice 3.
- [Discard a draft (DRAFT/READY) Exchange](https://apidocs.ankatech.co/reference/discarddraft.md): Soft-deletes an Exchange that has NOT yet been activated. Sets `deletedAt = NOW()` so the row vanishes from every standard query while remaining in DB for forensic access. Allowed ONLY when the Exchange is in `DRAFT` or `READY` status (no Capability Grant emitted under it). For `ACTIVE / SUSPENDED` callers MUST use `revoke` instead (preserves the row as terminal-state evidence). Terminal states (`REVOKED / EXPIRED`) are already non-actionable. Optimistic locking: the `version` query parameter MUST match the entity's current `@Version`. Mismatch → 409 Conflict. **Scope:** `admin.tenant.exchange.delete`
- [Update Exchange in DRAFT or READY](https://apidocs.ankatech.co/reference/updateexchange.md): PATCH-style update. All fields except `version` are optional. Mutating any of the 5 references on a READY Exchange demotes it back to DRAFT (single admin.cryptographicexchange.update event with previousStatus and newStatus per OQ-2). ACTIVE / SUSPENDED / terminal Exchanges are rejected with 422. Stale `version` returns 409 Conflict (RFC 7807).
- [Get tenant lifecycle policy](https://apidocs.ankatech.co/reference/gettenantlifecyclepolicy.md): Returns the effective dual-dimension lifecycle policy for the tenant: the assigned custom policy when present, otherwise the deployment DEFAULT template. Both the alias and material dimensions are fully populated.
- [Replace tenant lifecycle policy](https://apidocs.ankatech.co/reference/replacetenantlifecyclepolicy.md): Full replacement (PUT) of the tenant's dual-dimension lifecycle policy. Both dimensions must be supplied; the body is validated for the dual-dimension shape and permission values (ALLOW/DENY only).
- [Reset tenant lifecycle policy to deployment DEFAULT](https://apidocs.ankatech.co/reference/resettenantlifecyclepolicy.md): Removes the tenant's custom lifecycle policy so it falls back to the deployment DEFAULT template.
- [Get platform-wide key usage limits report](https://apidocs.ankatech.co/reference/getplatformusagelimitsreport.md): Returns an aggregated view of keys approaching usage limits across all tenants (or a filtered subset via `tenantIds`): - Summary counts: above soft limit, above 75%, above 90% - Full key list sorted by usage percentage descending - Computed synchronously on each request from the live key inventory (not cached) **Authorization:** Requires `platform.analytics.read` scope (platform admins only).
- [Export the platform-wide key usage-limits report as CSV](https://apidocs.ankatech.co/reference/exportplatformusagelimitsreport.md): Returns the SAME report `GET /usage-limits` serves on this plane, rendered as an RFC 4180 CSV attachment for an audit package, a spreadsheet or a third party. The optional `tenantIds` filter is the one the JSON endpoint accepts and narrows the same population. **One computation, two representations.** The figures are produced once, by the same service call the JSON endpoint makes, and then written. Nothing is recomputed for the export, so the file and the screen cannot disagree about a number. **What the file states about itself.** It opens with a `#`-led preamble carrying, in the artifact rather than in a covering email: the report name; the `scope` — which states whether the read covered ALL tenants or an explicit filtered set, and names the ids when it was filtered, because a cross-tenant figure with no statement of which tenants it spans is not evidence; the `population` (keys subject to a hard `maxUsageLimit`, meaning one greater than zero — a key with no cap is outside the report, because a percentage of an absent limit does not exist, and under the platform sentinel an unlimited key is stored as `0` rather than as an absent value); the `completeness` of the rows (`COMPLETE`); and then EVERY member the JSON response publishes, keyed by that member's own JSON name, including `usageHighAbovePercent`, `usageCriticalAbovePercent` and `generatedAt`. The instant is the report's own, never a second clock reading taken at write time. The data records follow, one per key, under a header row that IS `UsageLimitKeyInfo`'s member list in declaration order. The file terminates with `# EXPORT COMPLETE`; its ABSENCE is the third state, and means the stream broke before the end — an HTTP 200 already on the wire cannot be retracted, so a short well-formed CSV with no marker must not be read as the whole report. **Why `produces` names `application/json` as well as `text/csv`:** the success representation is CSV, but every refusal this operation can answer is an RFC 9457 `application/problem+json` body. A mapping that narrowed `produces` to `text/csv` alone would be unselectable by a client sending `Accept: application/json`, which collapses those error bodies along with the success status. **Authorization:** the report's own scope — `platform.analytics.read`. Exporting a report is reading it, so it grants nothing the screen does not.
- [Get total user count across all tenants](https://apidocs.ankatech.co/reference/getplatformtotalusercount.md): Returns the total number of non-deleted human users across all non-deleted tenants, **excluding the platform (root) tenant** — its users are administered on the dedicated platform-users surface. The counted tenant set is therefore identical to the one `GET /api/v3/admin/platform/tenants` returns minus the caller's own tenant, so this count and that tenant list always share the same denominator. Tenant **status is not filtered**: a suspended tenant's users are still counted, exactly as a suspended tenant still appears in the tenant list. Soft-deleted users and soft-deleted tenants are excluded. Users pending activation are counted in `totalUsers` AND reported separately in `pendingUsers`, a **subset** of the total (`0 <= pendingUsers <= totalUsers`), so the fully-onboarded population is `totalUsers - pendingUsers`. The platform has no `PENDING_ACTIVATION` status — a pending user is stored `ACTIVE` with `requirePasswordChange = true` — and a suspended or disabled user is never counted as pending. Issues exactly two queries: tenant ID resolution and a single aggregate that yields both counts in one pass (never two `COUNT`s, which could otherwise report more pending users than total across concurrent writes). **Authorization:** Requires `platform.analytics.read` scope (platform admins only).
- [Get platform cryptographic-posture summary](https://apidocs.ankatech.co/reference/getplatformposturesummary.md): Returns the platform-wide cryptographic-posture summary across ALL active tenants — the four scalar CAPA-pillar aggregates (agility, modernization, governance, conformance). Scalars and states only: no tenant identifier, key identifier, or policy field value. Conformance enforces SIX simple-algorithm criteria — explicitAlgorithms, allowedCategories, allowedLevels, allowedOperations, mandatoryStandards, minSecurityLevel — and applies FIVE of them to composite algorithms (the per-entry allowedCategories check is not applied to composites). SIX policy fields are not yet enforced: allowedFamilies, requireComposite, allowedCompositeModes, allowedKdfs, customRules, regulatoryFramework. The conformance evaluation is therefore deliberately partial. This endpoint accepts no input and is designed for the platform CAPA-posture command center. **Authorization:** Requires `platform.analytics.read` scope AND platform (ROOT) tenant.
- [Get platform cryptographic-posture history](https://apidocs.ankatech.co/reference/getplatformposturehistory.md): Returns the platform-wide cryptographic-posture HISTORY over the requested `[from, to]` window (inclusive; window must not exceed 365 days and `to` must be after `from`, else 400). The response is an ordered per-day series (oldest first) of the four CAPA pillars' raw `{numerator, denominator}` facts and per-pillar net flow, drawn from the append-only PLATFORM_AGGREGATE snapshot chain. Scalars and facts only — no tenant identifier, key identifier, or policy field value. Set `projection=true` to also receive a per-pillar linear projection to the 100% target. Each projection is served with the framing that produced its confidence: `points`, the usable-point count it was derived from, and exactly the boundaries that decided it — `minPointsForTrend` (the lower edge of MEDIUM) and `pointsForHigh` (the lower edge of HIGH). There are FIVE outcomes, not three. `HIGH` and `MEDIUM` carry both boundaries. `LOW` carries a `lowReason` token from a closed three-member vocabulary — `SHORT_SERIES`, `NO_X_AXIS_SPREAD`, `FLAT_OR_DECLINING` — with the i18n key that names it, so the reader is told WHICH kind of "no forecast" this is. A two-point series is `SHORT_SERIES`, not a degeneracy. The two genuinely degenerate reasons carry NO boundary: there the point count is at or above the trend minimum and explains nothing, so no count is presented as the cause. This endpoint does NOT read the live current-state posture — that stays on `posture-summary`. **Authorization:** Requires `platform.analytics.read` scope AND platform (ROOT) tenant.
- [Get platform-wide most used keys](https://apidocs.ankatech.co/reference/getplatformmostusedkeysreport.md): Returns the top N most frequently used keys across ALL tenants or filtered tenants: - Key identification and algorithm - Usage count and limits - Usage percentage of max limit - Tenant identification `totalUsageCount` covers the WHOLE inventory in scope, not only the `limit` keys listed, and is served with the `usageWindow` it is measured over (`ALL_TIME`) plus the i18n key naming it. Optional filtering: specify tenantIds to view specific tenants only. Use this for platform capacity planning and identifying critical dependencies.
- [Export the platform-wide most-used-keys report as CSV](https://apidocs.ankatech.co/reference/exportplatformmostusedkeysreport.md): Returns the SAME report `GET /most-used-keys` serves on this plane, rendered as an RFC 4180 CSV attachment, for the same `tenantIds` filter and the same `limit` the JSON endpoint accepts. **One computation, two representations.** The figures are produced once, by the same service call the JSON endpoint makes, and then written. **What the file states about itself.** A `#`-led preamble carries the report name, the `scope` — ALL tenants or the explicit filtered set, named — the `population` (the top `limit` keys of the whole inventory in scope, ranked by `usageCount` descending, plus a note that `totalUsageCount` covers the WHOLE inventory and not only the rows listed), the `completeness` (`BOUNDED`: the top N the request asked for, a complete answer to THAT question and not a page of a longer one), and then EVERY member the JSON response publishes under its own JSON name — including `usageWindow` and `usageWindowKey`, so the total never appears without the span it is measured over, the two served band boundaries, and `generatedAt`, which is the report's own instant. The header row IS `KeyUsageInfo`'s member list in declaration order. An unlimited key renders an EMPTY `usagePercentage` cell, never a zero — a percentage of an absent limit does not exist. The file terminates with `# EXPORT COMPLETE`; its ABSENCE means the stream broke before the end. **Why `produces` names `application/json` as well as `text/csv`:** the success representation is CSV, but every refusal is an RFC 9457 `application/problem+json` body, which a `text/csv`-only mapping would make unselectable. **Authorization:** the report's own scope — `platform.analytics.read`.
- [Get platform-wide PQC migration candidates](https://apidocs.ankatech.co/reference/getplatformmigrationcandidatesreport.md): Returns platform-wide report of active keys using classical algorithms: - Keys using RSA, EC, EdDSA, or ECDH algorithms across all tenants - Recommended PQC replacement algorithm - Migration priority based on usage and age - Breakdown by tenant Optional filtering: specify tenantIds to view specific tenants only. Use this for planning platform-wide transition to quantum-resistant cryptography.
- [Find keys by component algorithm platform-wide](https://apidocs.ankatech.co/reference/getplatformkeysbycomponentalgorithm.md): Searches for all composite keys across ALL tenants or filtered tenants that contain a specific algorithm: - Identifies all hybrid keys using a particular algorithm - Shows the role (classical/pqc) and position of the algorithm in each key - Includes key status and usage information - Includes tenant identification Optional filtering: specify tenantIds to view specific tenants only. Use this for platform-wide impact analysis when planning algorithm deprecation.
- [Get platform-wide health summary](https://apidocs.ankatech.co/reference/getplatformhealthsummary.md): Returns aggregated health metrics across ALL tenants or filtered tenants: - Total keys by status (ACTIVE, ROTATED, EXPIRED, REVOKED) - Keys in warning period platform-wide - Keys approaching usage limits platform-wide - `totalUsageCount`, served with the `usageWindow` it is measured over (`ALL_TIME`) and the i18n key naming it, so the figure never appears without the span it covers - Breakdown by tenant Optional filtering: specify tenantIds to view specific tenants only. This endpoint is designed for platform CISO dashboards and cross-tenant monitoring.
- [Get platform-wide expiration report](https://apidocs.ankatech.co/reference/getplatformexpirationreport.md): Returns aggregated expiration report across ALL tenants or filtered tenants: - `urgencyCriticalBelowDays`, `urgencyWarningBelowDays` and `urgencyCautionBelowDays`, the strict day boundaries that band `daysUntilExpiration` into critical / warning / caution — served, so the reader is told which rule produced the urgency it is looking at - Keys in warning period (soft expiration reached) - Keys approaching expiration within threshold - Breakdown by tenant Optional filtering: specify tenantIds to view specific tenants only. Use this for proactive platform-wide key renewal planning.
- [Get per-tenant cryptographic-conformance breakdown](https://apidocs.ankatech.co/reference/getconformancebytenant.md): Returns the ROOT-gated per-tenant conformance breakdown behind the platform pillar-5 conformance scalar: one row per declared/governed active (non-ROOT) tenant, as per-tenant COUNTS — governedKeys, conformingKeys, nonConformingKeys — plus the declared algorithm-policy template and a `measured` flag. It names WHICH tenant holds non-conforming keys; it NEVER reports which individual keys or any key material. The rows reconcile with the `posture-summary` conformance scalar by construction (Σ per-tenant == aggregate), since both are folded from the SAME single conformance pass. This endpoint accepts no input. **Authorization:** Requires `platform.analytics.read` scope AND platform (ROOT) tenant.
- [Get platform-wide component distribution](https://apidocs.ankatech.co/reference/getplatformcomponentdistribution.md): Returns granular algorithm distribution within composite key components across ALL tenants or filtered tenants: - Total composite keys and component counts platform-wide - Classical vs PQC component breakdown - Distribution by algorithm within components - Distribution by role (classical/pqc) - Breakdown by tenant Optional filtering: specify tenantIds to view specific tenants only. Use this for analyzing platform-wide hybrid key composition patterns.
- [Get platform-wide algorithm usage](https://apidocs.ankatech.co/reference/getplatformalgorithmusagereport.md): Returns algorithm distribution across ALL tenants or filtered tenants: - Count of PQC, classical, and hybrid keys platform-wide - Platform PQC adoption percentage - Detailed breakdown by algorithm - Breakdown by tenant Optional filtering: specify tenantIds to view specific tenants only. Use this for platform-wide capacity planning and security posture assessment.
- [List keys for tenant (paginated)](https://apidocs.ankatech.co/reference/listkeys.md): Returns a paginated view of cryptographic keys owned by the specified tenant, optionally filtered by lifecycle status. **Response shape**: a Spring `PageactivationState and the last recorded self-test verdict — the same five-member projection PUT /backend/configuration answers with and POST /backend/activate embeds. It is composed in one place and rendered here unchanged, so a console that reloads sees exactly what the write reported. A deployment that was never configured answers 200 with activationState: NOT_CONFIGURED and null coordinates. That is an ordinary state of this resource, not an error: a 404 here would force the console to treat "not configured yet" — the state every first-run operator is in — as a failure.
activationState is derived per request and stored in no table. It is composed from the declaration, the credential presence, the last self-test verdict and the immutable binding, each read once, so the word returned is always internally consistent.
The credential is write-only and stays that way. This surface says one thing about it — credentialPresent, a boolean. No value, no mask, no prefix, no length, no fingerprint. lastSelfTest carries the closed SelfTestReason vocabulary, so a failing backend can be described without describing the backend: no ARN, Azure account URL, project path, region, account id or vendor SDK message appears in any field, in any state.
Unlike the write, this read is not refused once the backend is bound: reading changes nothing, and a bound deployment is the one whose ACTIVE state the operator most needs to see. Non-mutating, unaudited, contacts nothing. ROOT-only (platform.bootstrap).
- [Configure this deployment's key-protection backend](https://apidocs.ankatech.co/reference/replacedeploymentbackendconfiguration.md): Records the non-secret per-provider coordinates of the deployment key-protection backend and returns the resulting activation state. Replaces the previous configuration in place: the declaration is singular by database constraint, so a second call updates the single row rather than creating a second one.
This is phase ONE of a two-phase surface, and it proves nothing. The order is configure then credential then activate. Configure opens no connection, validates no credential, provisions no key and does not record which backend the deployment is running. Only activate reaches the backend, and only activate writes the declaration the platform serves. A configuration that has never been proved is reported as such — activationState: CONFIGURED, credentialPresent: false, and a self-test still reading NOT_RUN / NEVER_INVOKED.
The request body is one shape for every backend: the declared backend token, the single keyId coordinate that names the key on it, and — only when that backend accepts one — an endpoint. It replaces three per-provider shapes that asked for four structured members on GCP and two on Azure, while the per-tenant plane one rung down asked for the generic pair the whole time. What an operator types into keyId, what to CALL that field, and whether endpoint is asked for at all are published per backend on GET /backend/declarable as coordinateContract; a client renders that answer and holds no per-backend table of its own.
Unknown keys are still rejected: a projectId, a keyRing or an invented field is a 400 rather than a value silently dropped. An endpoint submitted under a backend that accepts none is a 422 naming the endpoint requirement — it used to be a 400 on an unknown key, because the GCP shape carried no endpoint member at all; the answer is now the one the per-tenant plane has always given, from the single reader of the declared requirement column. The token is carried inside the configuration object itself, which is what keeps it in one place: a wrapper repeating it alongside would be two homes for one value and a consistency check between them.
activationState is derived per request and is stored in no table. It is composed from the declaration, the credential, the last self-test verdict and the immutable binding, all read once.
Carries no credential and echoes none. ROOT-only (platform.bootstrap).
- [Test that this deployment's key-protection backend can be reached](https://apidocs.ankatech.co/reference/testplatformkeybackend.md): Runs a full wrap/unwrap round trip against the deployment's bound key-protection backend and returns the verdict, so a misconfigured or unreachable backend can be diagnosed BEFORE a key operation fails. Required scope: admin.platform.key-backend.test, plus the ROOT platform tenant. A failed test is an HTTP 200. The question this operation asks is whether the backend works; "it does not" is an answer to that question, so it arrives as a 200 carrying a FAILED verdict. A client that renders that as an error loses the distinction that makes the operation useful. There is no 502 in this operation's status list, and that is deliberate. The round trip runs IN admin-api's own process, against the backend this deployment is bound to — nothing is delegated, so no delegation can fail. The one refusal it can answer is 503, when too many connection tests are already in flight on this instance. The verdict's reason comes from a closed vocabulary (SelfTestReason) that can carry no credential, ARN, resource path, Azure account URL, region or account id, and no vendor exception text. A client MUST tolerate a reason it does not recognise and render it as an unknown verdict — never as a pass. It advances no state, writes no row and provisions no KEK. Its one lasting effect is the recorded verdict itself, which is the point: GET /api/v3/admin/platform/setup/status reports the fresh selfTestVerdict and selfTestReason immediately afterwards, with no restart.
- [Complete first-run platform provisioning](https://apidocs.ankatech.co/reference/completeplatformsetup.md): Flips the platform to PROVISIONED (administrable) and audits setup.complete, WITHOUT requiring the key-material backend to be bound (§79 readiness decouple). Idempotent. Never provisions a KEK and never shells out to the host. ROOT-only (platform.bootstrap).
- [Check key-protection backend coordinates before committing to them](https://apidocs.ankatech.co/reference/validatedeploymentbackendconfiguration.md): Checks submitted deployment key-protection backend coordinates and answers what was checked. Required scope: platform.bootstrap, plus the ROOT platform tenant.
It persists nothing, on every path. No declaration is written or changed, no configuration row is created or mutated, no credential is read, sealed or unsealed, no candidate custody envelope is created, no self-test verdict is recorded and no per-tenant KEK is provisioned. Calling it on a deployment that has already bound a backend is explicitly supported and changes nothing — that is the deployment whose replacement an operator most needs to rehearse. Its one lasting effect is an audit row recording that a configuration was submitted for checking.
A FAILED verdict is an HTTP 200. The question this operation asks is whether the submitted coordinates are admissible and, where they name an operator-specific host, whether that host answers. "It did not answer" is an answer to that question, so it arrives as a 200 carrying FAILED/UNREACHABLE. A client that renders that as a transport error loses the distinction that makes the operation useful.
It never answers PASSED, and cannot. A pass means key material was wrapped and unwrapped again and came back identical. This operation presents no credential — the request shape carries none on any provider subtype — and wraps nothing, so a pass is not something it is able to observe. The green outcome is NOT_APPLICABLE, and it is deliberately not a pass: it says the coordinates are admissible and, where a host was named, that the host is alive at the transport layer. Nothing about authorization, entitlement or key material is known until activate runs.
Which checks run depends on the provider family and the verdict names them. SHAPE and ENDPOINT_POLICY always run, and KEY_ID_GRAMMAR_PARSED runs whenever the declared family publishes a key-coordinate grammar — aws-kms, gcp-kms and gcp-kms-hsm do; neither Azure token has one, because Azure publishes no parseable key-coordinate grammar for a vault key or for a Managed HSM key, so for those two the entry is ABSENT rather than reported as vacuously passing. TRANSPORT_PROBE runs only when the coordinates carry an operator-specific host: always for the two Azure tokens, each of whose endpoint IS the backend — a vault URL for azure-kv and a Managed HSM URL for azure-kv-mhsm, which are different Azure resource types on different host families — and for aws-kms only when an endpoint is supplied. Neither GCP token accepts an endpoint at all, so nothing is contacted for them and the verdict says so — COORDINATES_ADMITTED with no TRANSPORT_PROBE among the checks that ran, which is what stops a green answer reading as "everything is fine".
An endpoint the declared backend does not accept is a 422, not a verdict, and it is refused before anything is contacted. The rule is the declared per-backend requirement the commit enforces, read here by the same single reader — so a body this operation admits is a body PUT /backend/configuration admits. Answering ENDPOINT_ANSWERED for a submission the commit refuses would make the rehearsal actively misleading, and would dial an operator-supplied host under a token the platform declares accepts none.
The probe is pinned at TCP connect plus TLS handshake and issues no application-layer request. A vault that completes the handshake and would decline an unauthenticated API call therefore answers ENDPOINT_ANSWERED, because it was never asked anything to decline.
The verdict's reason comes from a closed vocabulary (SelfTestReason) that can carry no credential, ARN, resource path, Azure account URL, region or account id, and no vendor exception text. A client MUST tolerate a reason it does not recognise and render it as an unknown verdict — never as a pass.
503 never means "the backend did not answer". It means this instance declined to start a check it could not bound, because too many outbound reachability probes are already in flight here. It carries Retry-After and no verdict member at all, because a bounded-out request contacted nothing. An endpoint that did not answer is a 200 carrying FAILED/UNREACHABLE.
Documented property of the 403. The 403 is identical for every non-ROOT caller given a request body that passes Bean Validation. The endpoint policy is enforced by a constraint on the request record, which the framework evaluates while binding the argument — strictly before the handler body, and therefore before the ROOT-tenant gate that is its first statement. A caller holding platform.bootstrap but not the ROOT tenant consequently receives a 400 for a policy-refused endpoint and a 403 for an accepted one. The narrowing is stated rather than closed: the fact so disclosed is a published rule, documented in the configuration reference, and the same ordering governs PUT /backend/configuration, which this operation deliberately shares a request shape and a validation mechanism with.
409 before any mutation and before any audit. On some backends this is the LAST step that asks the operator for anything. Where the declared token asks for no egress endpoint, asks for no key coordinate, and belongs to a family that seals no deployment credential, this operation ALSO commits the deployment key-backend descriptor and publishes its projection — in the same transaction that records the declaration, so no window exists in which the declaration stands and the descriptor does not. The next call is then POST /backend/activate directly. PUT /backend/configuration is the step where an operator supplies coordinates, and such a token has none to supply; waiting for it is how a deployment came to hold a declared backend and no descriptor at all, with the activation refusing and nothing saying why.
The three conditions hold together, and the first one alone is not the rule. SIX of the nine declarable tokens ask for no endpoint — two of them, gcp-kms and gcp-kms-hsm, still require a key coordinate and still seal a credential, so they are recorded and nothing further happens. The conjunction is satisfied today by the four PKCS#11 vendors (softhsm, nshield, luna, cloudhsm), and it names no vendor: any future backend of the same shape inherits it with no change to this operation. The per-token answers are published on GET /backend/declarable.
A second declaration REPLACES a standing derived descriptor. The superseded row is removed and its projection eviction announced BEFORE the new row is derived, so no consumer keeps serving a protector built from the descriptor that was replaced. Re-declaring is the corrective action for a wrong vendor pick. One exception: a standing descriptor whose family seals a deployment credential is left byte-identical — deleting it would put a destructive custody action behind a non-destructive verb — and the route between two such declarations is DELETE /backend/declare, which disposes of the envelope on its way out.
A mechanism this deployment does not OFFER is refused 422. A mechanism whose certification level is EXPERIMENTAL is declarable only while the platform setting ankasecure.key-protection.experimental.offered-backends names it; a CERTIFIED one is always offered and that setting cannot withdraw it. The refusal names the mechanism, its level and the setting to change, because the only caller who can reach this operation is the only person who can change it. GET /backend/declarable publishes the same verdict per token, so a console never has to discover it by submitting.
ROOT-only (platform.bootstrap).
Available while at least one bootstrap tenant lacks an active key-encryption key; refused 409 once both hold one. After that point the backend may already have wrapped key material, and withdrawing the declaration that reaches it would orphan that material irrecoverably. Every uncertainty resolves to still-bound: a contradicted claim, an unverifiable one, and a boundness read that fails are all refused.
What the operation clears — all of it, not only the row this path is named after:
deployment_info.declared_backend, the declared backend token;DEPLOYMENT-scope key-backend descriptor row, holding the saved coordinates;.previous and any stranded rotation .candidate;Returning to a backend costs a re-seal. The credential is not kept per backend, so the envelope this withdrawal disposes is gone: declaring the same backend again requires supplying its credential again, and there is no restore point across the change.
Idempotent: a deployment holding none of those artefacts answers 204 and changes nothing.
ROOT tenant only, and it requires BOTH platform.bootstrap AND admin.platform.deployment-secret.write — the same pair PATCH /backend/credential requires. Deleting a sealed envelope is a mutation of the sealed value, so it carries the scope that writes one; the bootstrap scope alone declares, configures and activates, and still cannot reach a deployment secret.
configuration records coordinates and credential proves a credential, while this operation is where the platform commits to serving what was configured. It refuses before it reaches. A platform already bound to a different backend is a 409, evaluated first, with the credential never unsealed and no connection opened — once key material has been wrapped, re-pointing the backend orphans it permanently, and that is not recoverable by any later action. A descriptor with no sealed credential is a 422 carrying DESCRIPTOR_INVALID: nothing declined anything, so the fix is the declaration rather than the cloud account. Both refusals contact nothing, which is what makes them safe to retry.
A failure that the backend itself produced is a 422 whose extensions.reason names the cause from the closed self-test vocabulary — refused, unreachable, timed out, or a round trip that did not reproduce what it wrapped. It never carries an ARN, a resource path, a vault URL, a region, an account id or a vendor message.
On the PKCS#11 family the round trip CREATES A TEMPORARY KEY on the declared security domain, and destroys it. Stated here so it is known before a backend is declared rather than discovered later in an object listing. On a from-scratch token there is nothing to wrap under — the per-tenant keys are created by the NEXT phase — so the self-test generates one AES-256 key that is sensitive and non-extractable, resolves it by its own label, confirms from the object's own attributes that it cannot leave the token, wraps and unwraps under it, and destroys it. The label is random per invocation and sits outside the tenant key namespace; the key is destroyed on every path, including every failing one. If the destruction itself fails the activation still succeeds — an undeleted temporary object is litter, not a custody violation — and the response carries undeletedSelfTestObjectLabel so it can be removed out of band. There is no option to skip this test: it is the only observation of the backend on the whole path, and the per-tenant provisioning below performs no round trip of its own precisely because this one ran.
The two backend-facing phases are attributed separately. The round trip exercises wrap and unwrap; the per-tenant key provisioning exercises key CREATION, and its 422 names create-key as the failing capability class. A credential granted Encrypt and Decrypt but not CreateKey passes the first and fails the second, and one undifferentiated failure would send an operator to widen the wrong policy.
A partial run is reconciled, not rolled back. Keys already created on the backend stay created and their metadata rows stay committed — a rolled-back row would leave a real, billable key that nothing names. Re-run after correcting the cause: existing keys are adopted without the backend being contacted for them, and tenantKeksProvisioned reaches its full value.
An existing key is adopted only by the backend that created it. A bootstrap tenant whose key-encryption key was created by a DIFFERENT backend is a 422 under its own type, deployment-backend-kek-provider-mismatch. Nothing is adopted, nothing is provisioned and no declaration is committed, and the route out is to withdraw the declaration and then activate. Without it the platform could declare one backend while a bootstrap tenant key lived on another, which becomes permanent the moment the deployment reads as bound.
On every declarable backend family this operation also COMPLETES first-run provisioning, so a successful activation leaves the platform administrable with no further step. There is nothing left for an operator to press: the keys exist, the declaration is committed, and a separate Complete setup action would perform no work. This used to exclude the PKCS#11 family, on the ground that its three moments - declare, a host-side bind command, and POST /setup/complete - were genuinely distinct. Console-first HSM activation removes the middle one, so excluding PKCS#11 would now leave an operator who completed every console step on an unprovisioned platform. It is still deliberately NOT done when the caller is the environment HOST rather than a platform operator, because the host is admitted to this path to create key material, not to declare the platform administrable. Completion is idempotent, so re-running activation adds no second audit record.
ROOT-only (platform.bootstrap).
credential and restorePrevious. The value is WRITE-ONLY. It is never returned, never logged at any level, never audited, and never echoed on a read — the status read answers credentialPresent as a boolean and nothing more. It is sealed through the platform's own Plane-2 secret custody at the ROOT scope; it is never written to an environment file, never placed on a command line, and never turned into a container secret.
Nothing is promoted before it is proved. The candidate is sealed to a transient reference and a wrap/unwrap round trip is run against it first. A rotation and a restore run the round trip cross-credential — wrapping under the credential in effect and unwrapping under the candidate — so a candidate that reaches a different key-encryption key cannot be promoted. A restore takes the same path: the credential it puts back worked against the state the backend was in before the rotation, which is exactly what may have changed.
Phase TWO of a three-step surface: configuration, then credential, then activate. A credential submitted before any configuration is a 409 and seals nothing — a sealed value with no descriptor to own it is a secret nothing references, nothing evicts and nothing deletes.
On success the change is announced on the platform-settings channel so a running consumer drops its cached plaintext and re-resolves within seconds rather than within the cache TTL. The credential does not ride that channel: what travels is the key that names it.
ROOT-only. Requires platform.bootstrap AND admin.platform.deployment-secret.write.
setupState the console renders, the last recorded self-test verdict AND the closed-vocabulary reason behind it, and the operator-facing host activation command. The verdict is the last one RECORDED in this process — written by the boot self-test, and re-written by POST /api/v3/admin/platform/setup/key-backend/test when an operator runs one. This read never runs a round trip itself, so polling it costs the backend nothing.
Three of these are closed enumerations and carry no free text, so a broken backend can be described without describing the backend: no Azure account URL, ARN, resource path, region, account id, credential-presence flag or vendor SDK message appears in any field of this response, in any state.
backendReDeclarationBlocked is the GATE and backendBound is the REPORT. They sit beside each other and they are not interchangeable. The gate answers "would this server accept a re-declaration or a withdrawal right now?", resolving every uncertainty — a timeout, an unreachable backend, a contradicted bind, a corroboration read that throws — to "still bound". The report withholds boundness on that same uncertainty. Example 6 below is the divergence: backendBound is false while backendReDeclarationBlocked is true. A client that offers a backend selector or a withdrawal button on backendBound offers both there and is answered 409.
backendBound and keyBackendRuntimeReady answer different questions and are both needed. The first says a backend is bound; the second says this instance is serving it AND its boot round trip did not fail. A deployment can be bound and not runtime-ready — that is the post-bind restart window, and it is also the state a placeholder credential produces.
Non-mutating; provisions nothing, contacts the backend only through a throttled read-only resolve, and never shells out to the host. ROOT-only (platform.bootstrap).
- [List the key-protection backends this deployment may declare](https://apidocs.ankatech.co/reference/listplatformdeclarablebackends.md): Returns the closed set of declarable key-protection backend tokens - the four PKCS#11 HSM vendors and the five Cloud KMS backends - as the server's own vocabulary, so a first-run console never carries its own copy of the list. Required scope: platform.bootstrap, plus the ROOT platform tenant.
A row reports admissible: false with refusalReason: NOT_OFFERED when this deployment does not OFFER the mechanism - its certification level is EXPERIMENTAL and the platform setting ankasecure.key-protection.experimental.offered-backends does not name it. That verdict is the SAME one POST /backend/declare enforces, so a token this operation reports admissible is a token the declare accepts, and one it refuses is one the declare answers 422 for. A client never has to discover the difference by submitting.
The vocabulary is stable: the same nine tokens, in the same order, in every deployment state. Only the verdict moves, and it moves the moment an operator changes the setting - there is no cache and no restart. It reads no domain row, contacts no backend and changes nothing.
- [List this tenant's identity providers (masked)](https://apidocs.ankatech.co/reference/listidp.md): Required scope: admin.tenant.idp.override. - [Declare a tenant identity provider](https://apidocs.ankatech.co/reference/createidp.md): Creates a TENANT-scope provider in the DECLARED/disabled state. Required scope: admin.tenant.idp.override. - [Run the connectivity self-test](https://apidocs.ankatech.co/reference/testidp.md): Runs a live connectivity probe and returns a structured result. Does NOT activate the provider. Required scope: admin.tenant.idp.override. - [Enable (activate) a tenant identity provider](https://apidocs.ankatech.co/reference/enableidp.md): Transitions DECLARED → ACTIVE, ONLY if the most-recent self-test passed (409 otherwise). Required scope: admin.tenant.idp.override. - [Disable a tenant identity provider](https://apidocs.ankatech.co/reference/disableidp.md): Sets enabled=false so the provider stops being used at login, without deleting it. Re-enable via /enable (the prior self-test verdict is retained). Local break-glass login remains governed by allow-local-fallback. Required scope: admin.tenant.idp.override. - [Validate a candidate provider config (stateless)](https://apidocs.ankatech.co/reference/validateidp.md): Runs the connectivity probe against the SUBMITTED config and returns a structured result WITHOUT persisting anything — no provider is created, loaded, or mutated, and no self-test verdict is recorded. Use it to check a configuration (e.g. from the declare wizard) before committing to declare. Required scope: admin.tenant.idp.override. - [List group→role mappings](https://apidocs.ankatech.co/reference/listmappings.md): Optionally filtered by provider id. Required scope: admin.tenant.idp.mapping.manage. - [Create a group→role mapping](https://apidocs.ankatech.co/reference/createmapping.md): Maps a raw IdP group to an ANKASecure role for this tenant (the deny-by-default admission input). The mapped role is checked at declaration time so the mapping cannot be created in a state the federated login path is unable to consume: an unknown role, or a custom role owned by another tenant, is refused 404 (both causes are collapsed into one oracle-safe body); a service-to-service-only role is refused 400; and a role that is not assignable inside a tenant of this type — a PLATFORM_* composite, for instance — is refused 422 federated-role-not-assignable. Cross-surface divergence, deliberate: the SAME (role, tenant type) matrix answers 400 invalid-input on assignUserRoles and 422 here. The user-assignment surface rejects a malformed assignment request, whereas this surface accepts a well-formed declaration whose semantics cannot be satisfied — which is what 422 means. Required scope: admin.tenant.idp.mapping.manage. - [List this tenant's claimed email domains](https://apidocs.ankatech.co/reference/listemaildomains.md): Required scope: admin.tenant.idp.override. - [Claim an email domain for this tenant](https://apidocs.ankatech.co/reference/createemaildomain.md): Creates an UNVERIFIED domain claim. A verified platform admin (or the DNS-TXT self-service flow) verifies it before it routes a login. Required scope: admin.tenant.idp.override. - [List login domain guards](https://apidocs.ankatech.co/reference/listdomainguards.md): Optionally filtered by provider id. Required scope: admin.tenant.idp.override. - [Create a login domain guard](https://apidocs.ankatech.co/reference/createdomainguard.md): When a provider has ≥1 guard, only federated identities whose email domain matches an allowlisted domain may complete login. A guard grants no role. Required scope: admin.tenant.idp.override. - [List email/domain admission rules](https://apidocs.ankatech.co/reference/listadmissionrules.md): Optionally filtered by provider id. Required scope: admin.tenant.idp.mapping.manage. - [Create an email/domain admission rule](https://apidocs.ankatech.co/reference/createadmissionrule.md): Maps a federated email address or normalized domain to an ANKASecure role for this tenant (the email/domain half of the deny-by-default admission input). A DOMAIN rule may not grant a platform-scope / wildcard role (422); an EMAIL rule is uncapped by that privilege ceiling. Independently of that ceiling, and on BOTH rule types, the admitted role is checked at declaration time so the rule cannot be created in a state the federated login path is unable to consume: an unknown role, or a custom role owned by another tenant, is refused 404 (both causes are collapsed into one oracle-safe body); a service-to-service-only role is refused 400; and a role that is not assignable inside a tenant of this type — a PLATFORM_* composite, for instance — is refused 422 federated-role-not-assignable. Cross-surface divergence, deliberate: the SAME (role, tenant type) matrix answers 400 invalid-input on assignUserRoles and 422 here. The user-assignment surface rejects a malformed assignment request, whereas this surface accepts a well-formed declaration whose semantics cannot be satisfied — which is what 422 means. Required scope: admin.tenant.idp.mapping.manage. - [Get a tenant identity provider (masked)](https://apidocs.ankatech.co/reference/getidp.md): Cross-tenant probes return 404 (oracle-safe). Required scope: admin.tenant.idp.override. - [Delete a tenant identity provider](https://apidocs.ankatech.co/reference/deleteidp.md): Deletes the provider and its sealed service secret, freeing the one-per-kind slot. Cascades all three child sets and its link intents, and UNBINDS every account bound to it: both federation columns are cleared, a password change is required and the revocation epoch is advanced. Refused with 409 when that would leave the tenant with no account able to sign in. - [Whether this tenant's edition includes tenant-configured federation](https://apidocs.ankatech.co/reference/gettenantidpentitlement.md): Reports the entitlement verdict together with the required edition, this tenant's effective edition, andresolution — HOW that effective edition was arrived at. resolution is required, not decorative. It lets the console distinguish "your edition does not include this" (LICENSED) from "we could not confirm your edition" (FLOORED) — two states that route the operator to two different people. On FLOORED the effective edition is the fail-closed floor and the licence is never assumed upward. FLOORED does not imply entitled: false, and the two axes must be read separately. On a SaaS deployment it does, because there the verdict IS the edition. On a customer-operated deployment the verdict does not come from the edition at all (see below), so a licence-read failure floors the REPORTED edition while entitled stays true — the honest pair, since flooring the verdict too would deny a plane that is not licence-gated on that deployment class. On a customer-operated deployment (private cloud, on-premise) the tenant is entitled unconditionally and no edition gates this plane: the operator runs the platform and owns the directory, and the edition-assignment endpoint is SaaS-only, so gating here would be a lock with no key. This READ is deliberately more forthcoming than the write refusal. It answers an already-scoped, in-boundary tenant admin asking about their own tenant; the 403 on declare answers a write attempt and never discloses a licence-read failure. Required scope: admin.tenant.idp.override (existing — §136 introduces no scope).
- [Get the derived federation callback/ACS URLs](https://apidocs.ankatech.co/reference/getidpderivedurls.md): Returns the read-only OIDC callback URL, SAML ACS URL and SAML SP entityID the deployment derives from its public-edge base URL (base + fixed edge path, trailing-slash normalized). These are the values a tenant administrator must register with the external IdP — never operator free-text. The three values are deployment-wide and identical for every tenant. DEGRADED-VALUE POSTURE — read this before presenting a value for registration: the deriver does NOT validate the deployment's public-edge base URL. When PUBLIC_EDGE_BASE_URL is unset, blank, or reaches the service as an unsubstituted placeholder, this read still answers 200 and the derived values carry that raw setting through unchanged — an empty or path-only URL, or the literal placeholder text as the SAML SP entityID. Nothing refuses and nothing is logged. A client MUST treat any of the three values that is not an absolute https URL as unusable and warn, rather than present it for registration with an external IdP. Required scope: admin.tenant.idp.override.
- [Delete a group→role mapping](https://apidocs.ankatech.co/reference/deletemapping.md): Cross-tenant attempts return 404 (oracle-safe). Required scope: admin.tenant.idp.mapping.manage.
- [Remove an email domain claim](https://apidocs.ankatech.co/reference/deleteemaildomain.md): Cross-tenant attempts return 404 (oracle-safe). Required scope: admin.tenant.idp.override.
- [Delete a login domain guard](https://apidocs.ankatech.co/reference/deletedomainguard.md): Cross-tenant attempts return 404 (oracle-safe). Required scope: admin.tenant.idp.override.
- [Delete an email/domain admission rule](https://apidocs.ankatech.co/reference/deleteadmissionrule.md): Cross-tenant attempts return 404 (oracle-safe). Required scope: admin.tenant.idp.mapping.manage.
- [Read one of this tenant's trusted issuers](https://apidocs.ankatech.co/reference/gettenanttrustedissuer.md): One declaration, with the derivedAudience recomputed from the current public edge URL — so the value shown here is always the value the validation path requires right now, and a change to that URL can never leave a stale one behind. 🔴 An issuer id belonging to a different tenant answers 404, not 403. That is not a rounding of the refusal: a 403 for a row that exists and a 404 for one that does not is an existence oracle, and what it would enumerate on a shared deployment is which authorization servers other customers trust. The two cases are byte-identical here.
Ungated. - [Replace one of this tenant's trusted issuers](https://apidocs.ankatech.co/reference/replacetenanttrustedissuer.md): A full replacement, never a partial merge: every rule is re-applied to the whole submitted descriptor, so a field left out reverts to its default rather than keeping a value nothing re-checked.
🔴 The issuer URL may not change. Everything else on a declaration is a rule ABOUT one issuer and is editable in place; the URL is the issuer's identity. Every actor binding resolves on the pair (issuer, subject), so rewriting it here would stop every one of those bindings matching — and it would do so silently, because nothing fails: the row updates cleanly, the console shows green, and the next token is refused with the same undiagnosable 401 as a bad signature. Declare the new issuer, move the bindings, then withdraw this one.
This changes a live authentication rule. Narrowing permittedAlgorithms takes effect on the very next request, and in-flight workloads whose tokens are signed with a removed algorithm begin being refused immediately. The pre-change values are recorded in the audit trail, so an incident can be answered with what the rules WERE.
The derivedAudience does not change: it follows the declaration's scope, not its URL.
Entitlement-gated. - [Withdraw one of this tenant's trusted issuers](https://apidocs.ankatech.co/reference/deletetenanttrustedissuer.md): Withdraws trust. The declaration leaves the trust set on the very next request and its issuer URL becomes declarable again.
🔴 Refused with 409 while any actor binding still depends on it, and the refusal names how many. There is deliberately no cascade: a cascade would delete, in one unremarkable-looking DELETE, the entire authentication path of every workload bound to that issuer — at a moment the operator was tidying a registry rather than one they had chosen to cut those workloads off. Remove the bindings first, or disable the issuer instead, which stops it being trusted immediately and keeps everything.
Nothing withdraws trust on a timer. There is no sweep, no grace window and no automatic suspension anywhere in this feature. A schedule that cut a customer's production authentication N days after a billing event would do it at a moment no operator chose and with no human in the loop.
Ungated, like disable: reducing your own exposure is never something a billing state should block.
- [List this tenant's trusted issuers](https://apidocs.ankatech.co/reference/listtenanttrustedissuers.md): Every issuer this tenant has declared, whether it is currently in the trust set or not, ordered by canonical issuer URL.
Disabled declarations are included deliberately. A configuration surface that showed only the enabled rows would hide a disabled issuer from the operator who disabled it, and there would be no way back to it.
The list does not include the deployment-scoped issuers this tenant also trusts. Those are the operator's declarations, not this tenant's, and they are composed into the effective trust set at verification time — a tenant's own issuers are additional to them, never a replacement for them.
Ungated. A tenant whose edition does not include workload identity can still see, and still act on, what it already configured.
- [Declare a trusted issuer for this tenant](https://apidocs.ankatech.co/reference/createtenanttrustedissuer.md): Registers an external authorization server this tenant is willing to accept workload tokens from, and returns the derivedAudience that must be configured in that server.
The declaration lands switched off. enabled is server-set to false and cannot be sent: a body carrying it is rejected, and admitting the issuer into the trust set is the separate enable verb. That separation is what makes an abandoned half-finished form harmless.
🔴 No credential is submitted here, and none exists. ANKASecure verifies tokens from this issuer against the public key set the issuer itself publishes, so there is no secret to store, seal, mask or rotate. Revoking the workload in your own IdP is therefore sufficient to stop it authenticating.
The issuer is stored in canonical form — lowercase scheme and host, the default port removed, no trailing slash — because that string is both the uniqueness key and the value a presented token's iss claim is compared against. The response reports the canonical form, which is frequently not the string that was typed. The path keeps its case: Keycloak realm names and Okta authorization server ids are case-sensitive.
derivedAudience is computed by this server from the declaration's scope and is never stored. Configure it verbatim as the API identifier / audience of the machine-to-machine application in the external IdP. A one-character divergence between what is configured there and what is required here produces a 401 that cannot be diagnosed from either side, which is why the value is projected rather than composed by any client.
Entitlement-gated. Declaring is one of the verbs a tenant's edition governs. Reading, disabling, withdrawing and validating are not.
- [Probe a trusted issuer this tenant declared](https://apidocs.ankatech.co/reference/testtenanttrustedissuer.md): Reaches the issuer once, through the same transport the authentication path uses, and reports a PARTITION of the runtime checks: the ones it exercised and the ones it could not. Without a sample token every runtime check is reported as not exercised and the verdict is REACHABLE_UNVERIFIED, which is deliberately not a green — reachability is not authentication. Supply a token the issuer minted to exercise the rest; it is evaluated and discarded. Nothing about the declaration's trust state changes on either outcome.
Entitlement-gated. It is the only verb on this surface that leaves the deployment, and egress on a tenant's behalf is a capability the edition sells. Asked AFTER the tenant boundary and BEFORE any socket, so an unentitled caller never causes a packet. - [Admit one of this tenant's trusted issuers into the trust set](https://apidocs.ankatech.co/reference/enabletenanttrustedissuer.md): Switches the declaration on. From the very next request, a token minted by this issuer — carrying the derived audience, signed with an admitted algorithm, and naming a subject bound to an actor — authenticates that actor.
There is nothing to invalidate and no propagation delay: the trust set is composed from the registry on every verification, and the enabled flag is part of the query rather than a filter applied afterwards.
Testing first is not required. test and enable are separate verbs and the product does not force an order; an operator who skips the probe simply learns at the first authentication instead of at the probe.
Entitlement-gated. This is the verb that actually widens what this deployment accepts, which is why it is the one the edition governs rather than the declaration that precedes it. - [Remove one of this tenant's trusted issuers from the trust set](https://apidocs.ankatech.co/reference/disabletenanttrustedissuer.md): Switches the declaration off. From the very next request, tokens from this issuer are refused — indistinguishably from every other cause, so a client cannot learn from the refusal that the issuer was ever trusted.
Everything else is kept: the row, its bindings and its history. The workloads bound to it still exist as actors and keep any other credentials they hold, so re-enabling restores the previous state exactly.
Ungated, and that is a deliberate asymmetry with enable. A tenant must be able to reduce its own exposure whatever its billing state has done. Gating the off switch behind an entitlement would mean a downgraded customer could not turn off trust they no longer want — the opposite of what a commercial state should produce.
- [Check a candidate declaration without persisting or contacting anything](https://apidocs.ankatech.co/reference/validatetenanttrustedissuer.md): A dry run. It answers whether this deployment would accept the declaration, and reports the canonical issuer URL and the effective rules it would resolve to.
Nothing is written and nothing is contacted: no row, no DNS lookup, no discovery document, no key set. Whether the issuer is actually REACHABLE is the separate test verb, whose egress is why the platform gates it. Both verbs nevertheless demand the same .test scope: the distinction drawn here is about EGRESS, not about the scope assignment, and sharing the scope is the conservative direction at the cost of an operator holding only .write being unable to dry-run before declaring.
🔴 It runs exactly the admission the declaration runs — the same expression, not an equivalent one. That is the recorded §117 defect stated in reverse: there, a configuration-time self-test checked one endpoint while the runtime path checked four, so a provider passed green and returned 503 at sign-in. A green verdict here therefore means createTenantTrustedIssuer will succeed, including the platform token-lifetime ceiling and the derived audience, both of which are properties of this deployment rather than of the descriptor. The visible consequence, stated rather than hidden: on a deployment with no public edge base URL configured, this refuses too — truthfully, because the declaration would not succeed either.
A refusal is a 200 carrying admitted: false and the rule's own sentence, not a 400. The operator asked a question and got an answer; a console renders the reason inline as they type. A body that is structurally malformed — a missing required field, an unknown key — is still a 400, because that is not a candidate to have an opinion about.
Ungated. A tenant that has not bought the edition can still find out whether its descriptor is well-formed before it does.
- [Get tenant by ID](https://apidocs.ankatech.co/reference/gettenant.md): Retrieves a tenant by its UUID. Tenant Administrators can only access their own tenant. Platform Administrators can access any tenant.
- [Partially update a tenant](https://apidocs.ankatech.co/reference/patchtenant.md): Applies a JSON Merge Patch to the tenant's name. Only provided fields are updated. Note: deploymentType is NOT patchable. It is a platform-managed attribute auto-populated from deployment_info. Any request containing deploymentType is rejected with HTTP 400. **status is NOT patchable here, and never was legitimately.** The tenant lifecycle is a state machine with exactly one entry point per transition, on the PLATFORM plane: PATCH /api/v3/admin/platform/tenants/{tenantId}/activate (PROVISIONING → ACTIVE), .../suspend (ACTIVE → SUSPENDED) and .../reactivate (SUSPENDED → ACTIVE). Each asserts its exact source state and answers HTTP 422 otherwise, and each is gated on its own admin.tenant.lifecycle.* scope. The terminal CLOSED state is reached only by DELETE /api/v3/admin/platform/tenants/{tenantId}, which pairs it with the soft-delete. A request carrying status is rejected with HTTP 400 (Jackson FAIL_ON_UNKNOWN_PROPERTIES) rather than silently ignored. **Access Control:** Tenant Administrators can only update their own tenant. Platform Administrators can update any tenant (ROOT_TENANT requires ROLE_PLATFORM_ROOT).
- [List credentials (metadata only — never plaintext)](https://apidocs.ankatech.co/reference/listactorcredentials.md)
- [Mint a new credential](https://apidocs.ankatech.co/reference/createactorcredential.md): Generates a server-side 256-bit secret, stores a PBKDF2-HMAC-SHA256 hash, and returns the plaintext exactly once. Subsequent reads never include the plaintext — if lost, rotate. The request body is optional; when omitted, defaults apply (validFrom = now(), validUntil = no expiry).
- [Rotate credential with grace window or INLINE hard cutover](https://apidocs.ankatech.co/reference/rotateactorcredential.md): Mints a new credential at version N+1. Existing active credentials transition based on graceWindowHours: when 0, INLINE TX hard cutover to REVOKED (no intermediate state observable); when 1..720 (default 24), transition to ROTATING_OUT with revoke_at = now + graceWindowHours and continue to authenticate until the @Scheduled finalizer flips them to REVOKED. The request body is optional (defaults apply when omitted).
- [Immediately revoke a specific credential version](https://apidocs.ankatech.co/reference/deleteactorcredential.md)
- [Get supported algorithms (crypto-agility)](https://apidocs.ankatech.co/reference/listplatformsupportedalgorithms.md): Returns the algorithm catalogue with full metadata for crypto-agility decisions. Supports optional server-side filtering via query parameters. ## Query Parameters (all optional) | Parameter | Type | Logic | Description | |-----------|------|-------|-------------| | `category` | string | = | CLASSICAL, POST_QUANTUM, or HYBRID | | `status` | string | = | RECOMMENDED, EXPERIMENTAL, or LEGACY | | `minSecurityLevel` | int | >= | Minimum NIST level (1, 3, 5) | | `maxSecurityLevel` | int | <= | Maximum NIST level (1, 3, 5) | | `keyOps` | array | AND | Required operations (must support ALL) | | `standards` | array | AND | Required standards (must have ALL) | | `kty` | array | OR | Key types (matches ANY, including COMPOSITE) | | `alg` | array | OR | Algorithms (matches ANY) | ## Response fields | Field | Type | Description | |-------|------|-------------| | `kty` | string | Key type: ML-KEM, ML-DSA, RSA, EC, oct (simple) or the single coarse token COMPOSITE (composite) | | `compositeMode` | string | How a composite key is constructed. Present only on composite entries | | `alg` | string | Algorithm identifier: ML-KEM-768, RSA-4096 (simple) or X25519+ML-KEM-768 (composite) | | `keyOps` | array | Permitted operations: encrypt, decrypt, sign, verify | | `status` | string | RECOMMENDED, EXPERIMENTAL, or LEGACY | | `sunsetDate` | date | Planned deprecation date (ISO 8601, optional) | | `advisory` | string | Migration or security guidance (optional) | | `securityLevel` | integer | NIST security level (1-5) | | `standards` | array | Endorsing standards: NIST, BSI, ANSSI, ENISA, ISO, ETSI, NSA | | `category` | string | CLASSICAL, POST_QUANTUM, or HYBRID | ## Composite algorithm entries Composite algorithms appear as first-class entries with: - `category: "HYBRID"` - `kty`: the coarse key type `COMPOSITE` — every composite entry carries the same token - `compositeMode`: how the key is constructed, on its own axis - `alg`: Combined identifier (e.g., "X25519+ML-KEM-768", "Ed25519+ML-DSA-65") Use `category=HYBRID` to filter composite entries only. ## Example queries ``` # Post-quantum algorithms only GET /api/v3/admin/platform/supported-algorithms?category=POST_QUANTUM # Hybrid/composite algorithms only GET /api/v3/admin/platform/supported-algorithms?category=HYBRID # High security, recommended algorithms GET /api/v3/admin/platform/supported-algorithms?minSecurityLevel=3&status=RECOMMENDED # Signature algorithms with NIST compliance GET /api/v3/admin/platform/supported-algorithms?keyOps=sign,verify&standards=NIST # Find specific composite algorithm GET /api/v3/admin/platform/supported-algorithms?alg=X25519+ML-KEM-768 ```
- [List tenant-wide credentials (metadata only — never plaintext)](https://apidocs.ankatech.co/reference/listtenantcredentials.md): Returns every Actor credential in the tenant across all actors, with the owning actor's display name joined into `actorName`. Optionally narrow by `status` (ACTIVE / ROTATING_OUT / REVOKED) and/or `expiresWithinDays` (return only credentials whose `validUntil` falls within the next N days — credentials with no expiry are excluded from the window). Filters are applied server-side before pagination, so the page totals are filtered counts. A tenant with no matching credentials returns an EMPTY page with HTTP 200 — never 404.
- [Read a tenant's effective licensed Edition (platform)](https://apidocs.ankatech.co/reference/getplatformtenantedition.md): Returns the tenant's CURRENT effective Edition view — the deployment edition, the per-tenant override (null when the tenant inherits), the effective edition, and the backend token plus routing source resolved by the same precedence the S2S resolve seam applies. Mutates nothing. Unlike the assign, this read is available on EVERY deployment type, so a converted environment can still see the override it needs to clear. Required scope: admin.platform.tenant.edition.assign.
- [Assign a tenant's licensed Edition override (platform)](https://apidocs.ankatech.co/reference/assignplatformtenantedition.md): Sets the per-tenant Edition override, pinning the tenant to the ANKA-managed backend the edition maps to (STANDARD→softhsm, PROFESSIONAL→aws-kms, ENTERPRISE→cloudhsm). A backend-changing assign after the tenant's first key wrap is rejected 409; a same-backend relabel is permitted. Required scope: admin.platform.tenant.edition.assign.
- [Clear a tenant's licensed Edition override (platform)](https://apidocs.ankatech.co/reference/clearplatformtenantedition.md): Removes the per-tenant Edition override, reverting the tenant to the deployment edition. A backend-changing clear after the tenant's first key wrap is rejected 409; a no-override tenant is an idempotent no-op. Required scope: admin.platform.tenant.edition.assign.
- [Re-derive an Application's effective status (S2S only)](https://apidocs.ankatech.co/reference/resolveapplication.md): Reads the application's status from the database and re-publishes the HMAC-signed envelope via the single publisher. Requires the ankasecure-core S2S issuer and the admin.s2s.subject-status.resolve scope.
- [Re-derive an Actor's effective status (S2S only)](https://apidocs.ankatech.co/reference/resolveactor.md): Reads the actor's status from the database and re-publishes the HMAC-signed envelope via the single publisher. Requires the ankasecure-core S2S issuer and the admin.s2s.subject-status.resolve scope.
- [Find which bindable declaration a presented issuer names](https://apidocs.ankatech.co/reference/resolvebindableissuer.md): Answers the one question the binding form asks about a token an operator is holding: which of the issuers this tenant may bind to does its iss name, if any?
🔴 The comparison is not string equality, which is why this operation exists. A declaration is stored in canonical form — lowercase scheme and host, default port removed, every trailing slash stripped from the path, percent-triplets upper-cased — while a token's iss arrives exactly as its issuer wrote it. Auth0, which carries a first-class preset in the declaration form, always emits the trailing slash. So a raw comparison reports NO MATCH for a configuration that is perfectly correct and will authenticate at runtime, and there is no spelling of the declaration an operator could choose that would make it agree. This operation runs the SAME selection the operator probe and authentication run, so a client renders a verdict instead of computing one.
🔴 It is a POST, and that is deliberate — do not "correct" it to a GET. The edge and environment NGINX both log "$request", which is the full request line including the query string. An iss in a query parameter — or in a path segment, which is the same thing doubly encoded — would therefore become a durable tenant-to-issuer correlation in an access log whose retention this surface does not control. The identifier travels in a body so that it is written nowhere. Reverting to a GET silently reintroduces that exposure.
🔴 It carries the READ scope, not a write one. The method is POST for the logging reason above; the operation persists nothing, emits no audit event, spends no outbound-fetch allowance and is safe to retry. Attaching a write scope because the verb looks like one would widen authorization for a read.
No match is a 200 with matched: null — never a 404 and never a 204. A distinct status would be a third meaning of 404 on this controller, alongside the tenant-boundary 404, and a caller could then tell them apart and learn what the uniform refusal withholds. A blank, malformed or inadmissible iss is likewise a 200/null rather than a 400: it names no declaration, which is an answer, and an exception escaping a lookup would turn a refusal into a fault. The one 400 this operation can answer is TRANSPORT — a body it cannot read, or one carrying a field it does not declare — which says the request was not one this operation accepts and says nothing about the tenant.
🔴 The maxLength the request schema publishes is DOCUMENTARY, not a second refusal. It names the bound canonicalizeIssuerUrl applies before it parses anything; an oversized iss is still answered 200 with matched: null, exactly like every other value nothing declares. A generated client or schema-validating gateway that refused an oversized value LOCALLY would author on the client side the very second refusal shape this operation deliberately does not have — so read it as the rule's own bound, published for a reader, never as a precondition to enforce.
It resolves over the bindable set — the same rows ../bindable-issuers lists, with no enabled filter, so a declared-but-not-yet-enabled issuer matches. That is the state an operator is actually in while binding. A matched row is identical to the row that list publishes for it, including the withheld displayName on an inherited declaration, so this reveals strictly less than the read beside it.
Ungated. Reading which issuer a token names is not the act that grants a workload access, and the refusal it will meet at submit is the one that states the real reason. - [List the external identities bound to this actor](https://apidocs.ankatech.co/reference/listtenantactorissuerbindings.md): Every external identity that can authenticate as this actor, ordered by subject.
This is the answer to "who can act as this workload", and it is the whole answer for the federated path: a token matches one of these bindings or it authenticates nothing. Credentials the actor holds directly are a separate surface.
The canonical issuer URL is projected beside each issuer id, because an operator matching a binding to a configuration in an external IdP needs the URL a token's iss claim carries rather than a UUID. It is null only when this tenant cannot reach the referenced declaration at all — a binding that can never resolve, and one to remove.
An actor id that does not exist in this tenant answers 404 rather than an empty array. An actor with no bindings and an actor that is not there are different facts, and answering the second with the first would tell an operator their workload has no bindings when what is true is that they are reading the wrong actor.
Ungated. A tenant whose edition does not include workload identity can still see, and still act on, what it already configured.
- [Bind an external identity to this actor](https://apidocs.ankatech.co/reference/createtenantactorissuerbinding.md): Grants one external identity — an issuer this tenant trusts, plus the sub claim its tokens carry — the ability to authenticate as this Cryptographic Actor.
🔴 This is the act that actually grants access. Declaring a trusted issuer says only that this deployment is willing to accept tokens from it; nothing can authenticate until a binding exists. From the moment this call returns, a token naming that issuer and that subject — carrying the derived audience, signed with an admitted algorithm, from an enabled issuer — authenticates this actor with every role, capability grant and exchange context the actor holds.
The issuer may be one this tenant declared or a deployment-scoped one every tenant inherits; both are equally bindable. It does not have to be enabled yet: the natural order of work is declare, bind the workloads, then enable, and requiring the issuer to be live first would mean widening the trust set before the bindings that make it useful exist.
🔴 Uniqueness is per tenant. The same (issuer, subject) pair already bound in THIS tenant is a 409; the identical pair under a different tenant succeeds. Two customers whose workloads carry the same subject from the same public issuer is legitimate and common, and a global constraint would answer the second one with the existence of the first one's binding.
🔴 No credential is submitted here, and none exists. The workload keeps the credential its own identity provider issues it. ANKASecure stores nothing that could authenticate it, so revoking it in your own IdP is sufficient.
The actor is never created. An actor id that does not exist in this tenant answers 404 and writes nothing — an actor provisioned by this call would exist with defaults this endpoint chose rather than the authorization surface an administrator reviewed, and it would have been created by the external identity being bound to it.
Entitlement-gated. Reading the bindings and removing them are not.
- [List the issuers a binding here may name](https://apidocs.ankatech.co/reference/listtenantbindableissuers.md): Every trusted-issuer declaration a binding for this tenant may name: the ones the DEPLOYMENT declares, which every tenant inherits, concatenated with the ones this tenant declared for itself. Each row carries scope, so a caller can tell an inherited declaration from the tenant's own without a second call.
🔴 This is the set the bind call accepts, not the tenant's own registry. The registry read at ../workload-identity/issuers answers a different question — what this tenant may edit and withdraw — and a form that offered its answer would omit every deployment-scoped issuer, which on a deployment that declares its issuers centrally is the whole list.
There is no enabled filter, and that is deliberate. Enabling is a separate verb and the supported order of work is declare, bind the workloads, then enable; withholding a declared-but-not-yet-enabled issuer here would hide exactly the rows an operator is in the middle of setting up. A withdrawn (soft-deleted) declaration is not returned, which is the one filter this read and the runtime verification path share.
displayName is null on an inherited row. It is a label a platform operator wrote for their own purposes; the row's identifying fact here is its canonical issuer URL.
Ungated. A tenant whose edition does not include workload identity may still read this list — reading which issuers exist is not the act that grants a workload access, and the refusal it will meet at submit is the one that states the real reason. - [Remove an external identity's ability to authenticate as this actor](https://apidocs.ankatech.co/reference/deletetenantactorissuerbinding.md): Deletes the binding. From the very next request, a token from that issuer naming that subject authenticates nothing — indistinguishably from every other cause, so a client cannot learn from the refusal that the binding ever existed.
Everything else is kept. The actor still exists with its roles, capability grants and exchange context, and any credentials it holds directly are untouched; only this one external identity stops resolving to it.
A hard delete, not a soft one. A binding exists or it is gone, so revoking access has exactly one shape and an operator under pressure has exactly one thing to do. The audit trail is what preserves the fact that it existed.
🔴 Ungated, and that is a deliberate asymmetry with the create. A tenant must be able to reduce its own exposure whatever its billing state has done. Gating this behind an entitlement would trap a downgraded customer with a live binding they cannot remove — a workload that keeps authenticating precisely because the tenant stopped paying for the feature that would let them stop it.
Nothing removes a binding on a timer. There is no sweep, no grace window and no automatic suspension anywhere in this feature: a schedule that cut a customer's production authentication N days after a billing event would do it at a moment no operator chose and with no human in the loop.
- [Read one of a tenant's trusted issuers (platform)](https://apidocs.ankatech.co/reference/getplatformtenanttrustedissuer.md): One declaration belonging to the tenant in the path, including its current derivedAudience — the value that must be configured as the audience of the machine-to-machine application in the external IdP.
🔴 An issuer id belonging to a DIFFERENT tenant answers 404, never 403, and the two answers are identical. A 403 for a row that exists and a 404 for one that does not is an existence oracle, and the operator holding this scope must not be able to walk one customer's issuer ids into another customer's registry. The lookup carries the tenant, so the service never learns the difference either.
A DEPLOYMENT-scoped issuer id is also 404 here, for the same reason read from the other side: this route reads one tenant's own declarations, and the deployment registry is a different resource with a different plane.
Not entitlement-gated. The operator is the party that sells the edition, so refusing them on it would be the platform refusing itself. The tenant's own verdict is readable at ../workload-identity/entitlement (SR-10.6).
- [Replace a tenant's trusted-issuer declaration (platform)](https://apidocs.ankatech.co/reference/replaceplatformtenanttrustedissuer.md): Replaces the declaration whole. Every field is taken from the body, so an omitted optional field is CLEARED rather than kept — a partial update would leave an operator unable to see, from the request alone, what the issuer will admit afterwards.
🔴 The issuer URL is immutable. A body naming a different one answers 409, because renaming the issuer is not an edit to this trust relationship — it is a different one, and doing it in place would move every existing actor binding to an authorization server nobody reviewed. Withdraw and declare again.
A replace that narrows permittedAlgorithms or lowers maximumTokenLifetime takes effect on the very next token presented. That is a change to a live authentication rule made by an operator on a customer's behalf, which is why the audit row for it carries the pre-change state as well as the new one, and is visible to the customer.
Not entitlement-gated. The operator is the party that sells the edition, so refusing them on it would be the platform refusing itself. The tenant's own verdict is readable at ../workload-identity/entitlement (SR-10.6).
- [Withdraw a tenant's trusted issuer (platform)](https://apidocs.ankatech.co/reference/deleteplatformtenanttrustedissuer.md): Withdraws the declaration. Tokens from that issuer stop authenticating on the very next request — there is no grace window and no scheduled sweep, because a timer that withdrew trust at a moment no operator chose would cut a customer's production authentication without anyone deciding to.
🔴 Refused with 409 while actor bindings still name this issuer. The bindings are what actually grant workloads access, and withdrawing the issuer under them would leave rows pointing at a trust relationship that no longer exists — invisible until a workload fails to authenticate. Remove the bindings first; the refusal reports how many there are.
An operator withdrawing a customer's issuer is stopping that customer's workloads from authenticating. The audit row is filed under the customer, so their own trail shows who did it and when.
Not entitlement-gated. The operator is the party that sells the edition, so refusing them on it would be the platform refusing itself. The tenant's own verdict is readable at ../workload-identity/entitlement (SR-10.6).
- [List one tenant's trusted issuers (platform)](https://apidocs.ankatech.co/reference/listplatformtenanttrustedissuers.md): Every issuer THAT TENANT has declared, whether it is currently in the trust set or not, ordered by canonical issuer URL.
Disabled declarations are included deliberately. A configuration surface that showed only the enabled rows would hide a disabled issuer from the operator who disabled it, and there would be no way back to it.
The list does not include the deployment-scoped issuers this tenant also trusts. Those are the operator's own declarations, live at the deployment plane, and they are composed into the effective trust set at verification time — a tenant's issuers are additional to them, never a replacement.
This is the tenant's configuration, not its data: no principal, credential, key or audit content is reachable through it. That is what makes an operator read here consistent with can_access_tenant_data being false.
The ROOT tenant is a valid target for this READ, unlike on every write of this plane. It owns no TENANT-scoped declarations, so the honest answer is an empty list — which is information, where a 4xx would be an error about a well-formed question.
Not entitlement-gated. The operator is the party that sells the edition, so refusing them on it would be the platform refusing itself. The tenant's own verdict is readable at ../workload-identity/entitlement (SR-10.6).
- [Declare a trusted issuer on a tenant's behalf](https://apidocs.ankatech.co/reference/createplatformtenanttrustedissuer.md): Registers an external authorization server THIS TENANT is willing to accept workload tokens from, written by a platform operator, and returns the derivedAudience that must be configured in that server.
The row is TENANT-scoped and owned by the tenant in the path. It is not a deployment-wide declaration and never becomes one: no other tenant's trust set moves. The deployment registry has its own plane at /api/v3/admin/platform/workload-identity/issuers, and choosing between them is choosing whether one customer or all of them will trust this issuer.
The declaration lands switched off, exactly as on the tenant plane. enabled is server-set to false and cannot be sent; admitting the issuer into the trust set is the separate enable verb.
🔴 No credential is submitted here, and none exists. Verification uses the public key set the issuer itself publishes, so there is no secret to store, seal, mask or rotate — which is why an operator can complete this configuration without ever holding customer key material.
Not entitlement-gated. A tenant whose edition does not include workload identity is still provisioned here. The operator is the party that sells the edition. The verdict is readable at ../workload-identity/entitlement on this same plane, as information.
The audit row is filed under the tenant in the path, with actorPlane = PLATFORM and the operator's own username — so a tenant-scoped audit query run by that customer returns it.
- [Probe a trusted issuer on a tenant's behalf](https://apidocs.ankatech.co/reference/testplatformtenanttrustedissuer.md): Reaches the issuer once, through the same transport the authentication path uses, and reports a PARTITION of the runtime checks: the ones it exercised and the ones it could not. Without a sample token every runtime check is reported as not exercised and the verdict is REACHABLE_UNVERIFIED, which is deliberately not a green — reachability is not authentication. Supply a token the issuer minted to exercise the rest; it is evaluated and discarded. Nothing about the declaration's trust state changes on either outcome.
Not entitlement-gated. The operator is the party that sells the edition, so refusing them on it would be the platform refusing itself. The tenant's own verdict is readable at ../workload-identity/entitlement (SR-10.6).
- [Admit a tenant's issuer into its trust set (platform)](https://apidocs.ankatech.co/reference/enableplatformtenanttrustedissuer.md): Puts the declaration into the tenant's effective trust set. Until this runs, a token from that issuer is refused however correctly the declaration describes it — which is what makes an abandoned half-finished onboarding harmless.
This is the verb that actually widens what the deployment will accept for this customer, so it is the one an operator should be able to point at in an audit. The row is filed under the customer.
Idempotent: enabling an already-enabled issuer returns the same 200 and the same body. Trust is a state, not a counter.
Not entitlement-gated. The operator is the party that sells the edition, so refusing them on it would be the platform refusing itself. The tenant's own verdict is readable at ../workload-identity/entitlement (SR-10.6).
- [Remove a tenant's issuer from its trust set (platform)](https://apidocs.ankatech.co/reference/disableplatformtenanttrustedissuer.md): Takes the declaration out of the effective trust set, keeping the row and everything configured on it. Tokens from that issuer are refused on the very next request.
This is the operator's fastest lever when a customer's authorization server is compromised: one call, immediate, and reversible without retyping the declaration. The bindings that name the issuer are left in place, so re-enabling restores exactly the access that was suspended rather than a re-derived approximation of it.
Idempotent, for the same reason as its counterpart.
Not entitlement-gated. The operator is the party that sells the edition, so refusing them on it would be the platform refusing itself. The tenant's own verdict is readable at ../workload-identity/entitlement (SR-10.6).
- [Check a declaration before writing it (platform)](https://apidocs.ankatech.co/reference/validateplatformtenanttrustedissuer.md): Runs the admission rules against a candidate declaration and reports the verdict. Persists nothing and contacts nothing — no row is written, no audit row is emitted and the issuer is never reached over the network, so this is not an egress surface and cannot be used to probe one.
It answers 200 with admitted: false for a descriptor the rules refuse, rather than 4xx: a refusal is the ANSWER to the question that was asked, and an operator filling in an onboarding form needs the reason next to the field, not an error page.
The verdict reports the CANONICAL issuer the declaration would be stored under, which is frequently not the string that was typed. Seeing it before writing is the point: an operator who expected a trailing slash to survive finds out here rather than after the customer's tokens start being refused.
It is still a WRITE-plane surface in the sense that matters for the ROOT target — it is refused for the ROOT tenant so that the answer never describes a declaration that could not be written anyway.
Not entitlement-gated. The operator is the party that sells the edition, so refusing them on it would be the platform refusing itself. The tenant's own verdict is readable at ../workload-identity/entitlement (SR-10.6).
- [Get one platform setting detail with its effective value](https://apidocs.ankatech.co/reference/getsetting.md)
- [Create or update a platform setting override](https://apidocs.ankatech.co/reference/updatesetting.md)
- [Clear a platform setting: reset an override, or clear a console-managed secret](https://apidocs.ankatech.co/reference/deletesetting.md)
- [Preview the validator outcome for a candidate value without persisting it](https://apidocs.ankatech.co/reference/previewvalidator.md)
- [List settings awaiting a restart to take effect](https://apidocs.ankatech.co/reference/getrestartpending.md)
- [List settings that currently carry a value this product holds](https://apidocs.ankatech.co/reference/getoverrides.md)
- [Bulk-resolve effective values for a set of keys (cold-start fetch, max 100)](https://apidocs.ankatech.co/reference/geteffectivevalues.md): Returns the effective value of each requested key in one call — the cold-start bulk fetch a restarting consumer re-reads the world through. Platform-gated and read-scoped. Capped at 100 keys; an over-limit or empty request is rejected with 400 BEFORE any resolution. A key the catalog does not declare is OMITTED from the result rather than answered 404, so the endpoint is not a key-existence oracle. A **reserved routing key** — one an own-table subsystem owns rather than the deployment catalog, such as the deployment key-protection descriptor — additionally requires the read scope that subsystem declares for itself, because its descriptor names cloud topology and is not a deployment scalar. A human caller that does not hold it is refused **403 for the whole request**: the gate aborts the call, so a request for 50 catalog keys plus one reserved key returns 403 and no partial map. Serving the other 50 and silently omitting the reserved one would be indistinguishable from that key not being configured, which is the oracle the omission rule exists to prevent. Service-to-service settings-warm callers are bounded differently: their per-issuer allow-list is narrower and key-exact, an out-of-list key is DROPPED exactly as an unknown key is, and no reserved-key scope is tested. **The two audiences receive different answers for the same key, and that is the contract.** For the human platform reader, a catalog entry whose effective value is admin-api's own compiled default and whose `affectedServices` does not name admin-api is answered `source: DEPLOYMENT_NOT_VISIBLE`, `value: null` and a `notVisibleReason` — the same answer `/catalog`, `/overrides` and `GET /{key}` give, because admin-api's compiled default is not the deployment's value and publishing it as one is what this refusal removes. For a settings-warm S2S caller the raw precedence answer is returned **unchanged**: that value is what the warming service is starting up to get, and a null in its place would be dropped by the consumer as an absent key and leave it on compiled defaults for its whole process life. Reserved routing keys are outside the rule entirely.
- [List the platform settings catalog with effective values](https://apidocs.ankatech.co/reference/getcatalog.md)
- [Read one deployment-wide trusted issuer](https://apidocs.ankatech.co/reference/getdeploymenttrustedissuer.md): One declaration, with the derivedAudience recomputed at read time rather than read from a column.
That recomputation is why the value here is always the one the verification path will require: nothing stores it, so changing the deployment's public edge URL cannot leave a stale audience behind on any row.
A TENANT-scoped issuer id answers 404, not 403. The lookup names the DEPLOYMENT scope, so a customer's own declaration is simply not reachable through this plane.
Never entitlement-gated. No Edition widens or narrows this plane. These are the deployment operator's own declarations rather than a tenant's, so there is no Edition to consult (FR-181.16). - [Replace a deployment-wide trusted issuer's rules](https://apidocs.ankatech.co/reference/replacedeploymenttrustedissuer.md): Replaces the declaration in full. Not a partial merge: every admission rule is re-applied to the whole submitted descriptor, so a replace cannot leave a field at a value nothing checked.
🔴 The issuer URL may not change. Everything else on a declaration is a rule about one issuer and is editable in place; the URL is the issuer's identity. Every actor binding, in every tenant, resolves on the pair (issuer, subject), so rewriting it would stop all of them matching — silently, because nothing fails: the row updates cleanly, the console shows green, and the next token is refused with the same undiagnosable 401 as a bad signature. Withdraw and declare the new URL instead.
Narrowing permittedAlgorithms or lowering maximumTokenLifetime takes effect on the very next request, for every tenant at once. The pre-change rules are recorded in the audit trail, because an entry naming only the new state cannot answer the question an incident asks.
Never entitlement-gated. No Edition widens or narrows this plane. These are the deployment operator's own declarations rather than a tenant's, so there is no Edition to consult (FR-181.16). - [Withdraw a deployment-wide trusted issuer](https://apidocs.ankatech.co/reference/deletedeploymenttrustedissuer.md): Withdraws the declaration. Tokens from that issuer stop verifying for every tenant from the next request onward.
🔴 Refused with a 409 while any binding still depends on it — in any tenant. Never a cascade. A cascade would delete, in one unremarkable-looking DELETE, the entire authentication path of every workload bound to this issuer across every customer on the deployment, at a moment the operator was tidying a registry rather than one they had chosen to cut those workloads off. The refusal names the count, so "you cannot do this" becomes "here is how much there is to do first"; the count is published as an extension member so a console need not parse the sentence.
The count spans every tenant, deliberately. A dependency check that only looked at one tenant would let a withdrawal that orphans forty other customers' workloads answer that nothing depends on this issuer. It is a count and not a list: the number is what the refusal needs, while the rows would tell a platform operator which customers run which workloads.
To stop trusting an issuer immediately without removing anything, use disable — it takes effect on the next request and is reversible.
Never entitlement-gated. No Edition widens or narrows this plane. These are the deployment operator's own declarations rather than a tenant's, so there is no Edition to consult (FR-181.16). - [List the deployment-wide trusted issuers](https://apidocs.ankatech.co/reference/listdeploymenttrustedissuers.md): Every issuer declared for the whole deployment, whether it is currently in the trust set or switched off.
The disabled ones are included deliberately. A configuration surface that showed only what is live would hide a row from the operator who disabled it, and there would be no way back to it.
Only the deployment's own declarations appear. A tenant's own issuers are on the tenant plane and are not readable here — the platform token is the most privileged on the deployment, and this surface still must not become a way to read every customer's registry.
Ordered by canonical issuer URL, so the list is stable across calls rather than dependent on what the database happened to return.
Never entitlement-gated. No Edition widens or narrows this plane. These are the deployment operator's own declarations rather than a tenant's, so there is no Edition to consult (FR-181.16).
- [Declare a deployment-wide trusted issuer](https://apidocs.ankatech.co/reference/createdeploymenttrustedissuer.md): Registers an external authorization server this whole deployment is willing to accept workload tokens from, and returns the derivedAudience that must be configured in that server.
🔴 Every tenant inherits this declaration. A tenant's effective trust set is the deployment-scoped issuers union its own, so enabling one row here widens what every tenant on the deployment will accept a token from. Declare an issuer here when the operator also owns the workloads; declare it on the tenant when one customer owns them.
The declaration lands switched off. enabled is server-set to false and cannot be sent: a body carrying it is rejected, and admitting the issuer into the trust set is the separate enable verb. On this plane that separation is what keeps an abandoned half-finished form from becoming a deployment-wide change.
derivedAudience is the deployment audience here, not a per-tenant one — it follows the SCOPE of the declaration rather than the deployment type. Configure it verbatim as the API identifier of the machine-to-machine application in the external IdP.
🔴 No credential is submitted here, and none exists. ANKASecure verifies tokens from this issuer against the public key set the issuer itself publishes, so there is nothing to store, seal, mask or rotate.
Never entitlement-gated. No Edition widens or narrows this plane; the only condition is that the deployment is customer-operated.
- [Probe a deployment-wide trusted issuer](https://apidocs.ankatech.co/reference/testdeploymenttrustedissuer.md): Reaches the issuer once, through the same transport the authentication path uses, and reports a PARTITION of the runtime checks: the ones it exercised and the ones it could not. Without a sample token every runtime check is reported as not exercised and the verdict is REACHABLE_UNVERIFIED, which is deliberately not a green — reachability is not authentication. Supply a token the issuer minted to exercise the rest; it is evaluated and discarded. Nothing about the declaration's trust state changes on either outcome.
Never entitlement-gated. No Edition widens or narrows this plane. These are the deployment operator's own declarations rather than a tenant's, so there is no Edition to consult (FR-181.16). - [Admit a deployment-wide trusted issuer into every tenant's trust set](https://apidocs.ankatech.co/reference/enabledeploymenttrustedissuer.md): Admits the issuer into the effective trust set of every tenant on the deployment, effective on the very next request.
This is the verb that actually widens what the deployment will accept a token from, which is why declaring and enabling are separate acts. Nothing here is written per tenant: a tenant's trust set is composed at read time as the deployment-scoped issuers union its own, so one row becoming enabled is the whole change.
Enable it after the bindings that make it useful exist. A binding may name a disabled issuer deliberately, so the natural order is declare, bind the workloads, then enable.
Never entitlement-gated. No Edition widens or narrows this plane. These are the deployment operator's own declarations rather than a tenant's, so there is no Edition to consult (FR-181.16). - [Remove a deployment-wide trusted issuer from every tenant's trust set](https://apidocs.ankatech.co/reference/disabledeploymenttrustedissuer.md): Removes the issuer from the effective trust set of every tenant at once, effective on the very next request, and keeps everything else.
This is the fastest lever the deployment has. There is no cache to invalidate and no projection to re-drive: the enabled predicate is part of the query that composes a tenant's trust set, so a compromised issuer stops verifying anything immediately.
The declaration, its bindings and its history are retained, so the act is reversible by the operator who performed it. Use delete when the relationship is over rather than paused.
Never entitlement-gated. No Edition widens or narrows this plane. These are the deployment operator's own declarations rather than a tenant's, so there is no Edition to consult (FR-181.16). - [Check a candidate deployment-wide declaration without persisting anything](https://apidocs.ankatech.co/reference/validatedeploymenttrustedissuer.md): Answers whether a candidate declaration would be admitted, and reports the rules it would be admitted under. Writes nothing, reads nothing from the registry, and contacts nothing over the network.
The candidate goes through the identical admission expression the real declare runs, on a candidate built with the same DEPLOYMENT scope. That identity is the point: a dry run that checked less than the real path is the recorded defect where a configuration-time self-test passed green and the runtime failed at the moment that mattered. A green verdict here means the declaration will succeed.
The reported derivedAudience is therefore the deployment audience — the value a workload bound through this issuer will actually have to present — and not a tenant-shaped one.
A refused candidate is still a 200. The verdict is the answer to the question that was asked, not an error; the refusal reason travels in the body. Whether the issuer can be REACHED is the separate test verb, which is the one that opens a socket.
Never entitlement-gated. No Edition widens or narrows this plane. These are the deployment operator's own declarations rather than a tenant's, so there is no Edition to consult (FR-181.16). - [List tenants with a live support-access authorisation](https://apidocs.ankatech.co/reference/listplatformsupportaccessgrants.md): Answers exactly one operational question: which tenants may support act as, and until when.
Tenant identifiers and windows only. Three fields are deliberately absent and each absence is a control:
reason text — it was written by the tenant for the tenant, and this list is precisely the screen that would aggregate every tenant's stated support rationale onto one page;sessionActive distinguishes a tenant support is permitted to act as from one it is acting as right now.
- [Re-derive a tenant's dual lifecycle policy (S2S only)](https://apidocs.ankatech.co/reference/resolvedualpolicy.md): Resolves the tenant's effective dual-dimension lifecycle policy from the database (tenant policy → deployment ceiling → DEFAULT template, never deny-all) and re-projects it to Redis via the single existing projector. Returns 200 with the flat dual policy for every existing tenant. A non-existent tenant returns a uniform, oracle-safe 404 that does not confirm existence. Requires the ankasecure-core S2S issuer and the admin.s2s.lifecycle-policy.read scope.
- [Get one of the tenant's observability destinations (key masked)](https://apidocs.ankatech.co/reference/getobservabilitybackend.md)
- [Update a tenant observability destination (rotate the key by supplying a new value)](https://apidocs.ankatech.co/reference/replaceobservabilitybackend.md)
- [Delete a tenant observability destination](https://apidocs.ankatech.co/reference/deleteobservabilitybackend.md)
- [List the tenant's observability destinations (keys masked)](https://apidocs.ankatech.co/reference/listobservabilitybackends.md)
- [Create a tenant observability destination](https://apidocs.ankatech.co/reference/createobservabilitybackend.md)
- [Rotate a tenant observability destination's export key (credential-only)](https://apidocs.ankatech.co/reference/updateobservabilitybackendcredential.md)
- [Replace a platform alert destination](https://apidocs.ankatech.co/reference/replaceplatformalertdestination.md): Replaces a destination in place. The id is immutable, because the custody reference is derived from it. Omit the signing secret, or send the mask sentinel, to leave the sealed secret untouched — which is what lets a console round-trip its form without asking the operator to retype a secret they cannot see. Supplying a new value rotates it. That holds while the endpoint is unchanged. Moving a webhook that HAS a sealed secret to a different URL without supplying a new one is refused with 400: keeping the sealed secret would let it sign deliveries to a host it was never issued for, and dropping it would silently un-sign a webhook whose receiver is still verifying. A webhook with no sealed secret moves freely. To move a signed one unsigned, delete the destination and declare it again without a secret. Authorization: `admin.platform.alerts.write`.
- [Delete a platform alert destination](https://apidocs.ankatech.co/reference/deleteplatformalertdestination.md): Deletes a destination and retires its sealed signing secret in the SAME transaction. Left behind, the envelope would be an unreferenced credential that nothing lists and nothing rotates. Authorization: `admin.platform.alerts.write`.
- [Reconfigure one platform alert condition](https://apidocs.ankatech.co/reference/replaceplatformalertcondition.md): Replaces the tunable configuration of one seeded condition: whether it is evaluated, its threshold, its dwell and its cooldown. The condition KEY and its SEVERITY are not settable. A settable severity would let a destination's severity filter be bypassed by editing the condition instead of the destination — the same authorization decision expressed in two places, which is how the two come to disagree. Disabling a condition that is currently FIRING closes it: `resolved_at` is set and one RESOLVED notification is emitted. Left open it would sit firing in the console forever, because nothing evaluates it any more. Authorization: `admin.platform.alerts.write`.
- [List the platform alert destinations](https://apidocs.ankatech.co/reference/listplatformalertdestinations.md): Returns every configured destination. A webhook's signing secret is NEVER returned. `secretConfigured` reports only whether one is sealed — the presence, which is the single fact a custody read may disclose. Authorization: `admin.platform.alerts.read`.
- [Declare a platform alert destination](https://apidocs.ankatech.co/reference/declareplatformalertdestination.md): Creates a destination. The endpoint is validated BEFORE anything is persisted: a rejected URL leaves no row and opens no socket. A webhook's signing secret is supplied here as the MATERIAL, and is MANDATORY on create: a webhook declared without one is rejected with 400 rather than persisted. The platform never sends an unsigned webhook, so an unsigned destination is one no alert could ever be delivered to — a saved row and a green form that go quiet forever. (On `replace` the secret is optional, where omitting it means "leave the sealed value alone".) Its custody reference is derived server-side from the created row's immutable id and is never accepted from the caller — the custody registry is shared with the IdP, event-forwarding and observability subsystems, so a caller-supplied reference would let this destination bind to a credential it has no claim to. A webhook URL is validated STRICTLY regardless of deployment type, as is any destination whose purpose is HEARTBEAT. Authorization: `admin.platform.alerts.write`.
- [Send a test delivery to a platform alert destination](https://apidocs.ankatech.co/reference/testplatformalertdestination.md): Performs a REAL outbound delivery to the configured recipient and records a verdict. This is why `admin.platform.alerts.test` is a distinct scope and is NOT implied by `.write`: the recipient is outside the deployment and cannot be un-notified, so being able to configure a destination and being able to make the platform message it are different capabilities with different blast radii. Rate-limited per destination. Without a limit this endpoint is an amplifier: an authenticated caller could drive unbounded outbound messages to an address they chose, at the platform's reputation and cost. The recorded verdict is a CLOSED classification, never a reason relayed from the delivery service — the value is stored, read back and rendered. Authorization: `admin.platform.alerts.test`.
- [List the live firing state of every alert condition](https://apidocs.ankatech.co/reference/listplatformalertstate.md): Returns the durable firing state of all eleven conditions, computed from the SAME stored evaluation the System Health panel is served from. `evaluatedAt` is that evaluation's identity and is returned by both surfaces, which is what makes "the panel and the alerter cannot disagree" a checkable property rather than an aspiration. Authorization: `admin.platform.alerts.read`.
- [List the platform alert conditions](https://apidocs.ankatech.co/reference/listplatformalertconditions.md): Returns the eleven seeded alert conditions with their current configuration. The catalogue is CLOSED: conditions are seeded by the bootstrap SQL and cannot be created or deleted through the API. An operator tunes when a condition fires, not which conditions exist — the eleven are the ones that can be answered from signals the platform already produces. Authorization: `admin.platform.alerts.read` (platform/ROOT administrators only).
- [Suspend Exchange Context (invalidates all dependent grants)](https://apidocs.ankatech.co/reference/suspendexchangecontext.md): Transitions the Exchange Context to SUSPENDED and publishes an invalidation message on the `capability-grant-updates` Redis Pub/Sub channel so every Core API replica drops its L1 cache entry for every grant bound to this context. A body is now REQUIRED and carries a mandatory `reason` (PRD 231, operation 2). It is recorded verbatim at canonical position 21 of the signed AdminAuditEvent, so an audit row can no longer say that an Exchange Context was suspended without saying why. It is bounded at AuditFieldBounds.MAX_REASON_LENGTH (1,000 UTF-8 BYTES, not characters -- the unit the signed preimage and every carrier downstream of it are measured in) -- the same bound applied before signing, so no accepted value is silently truncated on its way into the preimage -- and it is NEVER written to a log line. IDEMPOTENT NO-OP: an already-SUSPENDED context answers 200 OK and the submitted reason is recorded NOWHERE -- no status write, no audit event, no admin_audit_log row -- because the act did not happen. That is deliberate, not a lost value: recording it would put a justification for a suspension into the trail on a call that suspended nothing.
- [Resume SUSPENDED Exchange Context back to ACTIVE](https://apidocs.ankatech.co/reference/resumeexchangecontext.md)
- [Close Exchange Context (permanent, non-reversible)](https://apidocs.ankatech.co/reference/closeexchangecontext.md): Moves the Exchange Context to CLOSED terminal state. Cannot be reverted.
- [List Exchange Contexts under a specific Application](https://apidocs.ankatech.co/reference/listapplicationexchangecontexts.md)
- [Create an Exchange Context under an Application](https://apidocs.ankatech.co/reference/createexchangecontext.md): Creates a governed interaction scenario scoped to a specific Application. The Exchange Context starts in PENDING_ACTIVATION and can later be activated, suspended, resumed or closed via dedicated endpoints.
- [Get Exchange Context details](https://apidocs.ankatech.co/reference/getexchangecontext.md)
- [Soft-delete a CLOSED or PENDING_ACTIVATION Exchange Context](https://apidocs.ankatech.co/reference/deleteexchangecontext.md)
- [Update Exchange Context metadata (PATCH)](https://apidocs.ankatech.co/reference/updateexchangecontext.md)
- [List Exchange Contexts in tenant](https://apidocs.ankatech.co/reference/listtenantexchangecontexts.md)
- [Whether this tenant may declare its own trusted issuers](https://apidocs.ankatech.co/reference/gettenantworkloadidentityentitlement.md): The verdict the console renders, resolved SERVER-side from the deployment type and the tenant's effective Edition. The console never re-derives it: a surface that decided its own entitlement would be a second party deciding the same thing, and the two would eventually disagree.
On a private-cloud or on-premise deployment the answer is always entitled: true — the operator runs the platform and owns the workloads that authenticate against it, so there is nothing for an ANKA-issued Edition to entitle. On an ANKA-operated deployment it is true only for the Enterprise edition.
resolution is required reading beside the verdict: LICENSED means the licence answered, FLOORED means it could not be read and the reply is the fail-closed floor. "Your edition does not include this" and "we could not confirm your edition" send an operator to two different people, and a bare entitled: false sends half of them to the wrong one.
This read is deliberately NOT gated by the entitlement it reports, and it never fails with a 500: a licence outage is reported as FLOORED, not raised.
🔴 A tenant WITHOUT the entitlement keeps every workload that already authenticates through a declared issuer. Authentication never consults this verdict, so a licence outage cannot become an authentication outage. What the entitlement buys is the right to declare NEW trust.
Ungated. The verdict has to be readable by the tenant it is about: gating it would mean a tenant that lost the edition could not find out why its own surface changed. - [Re-derive a tenant's lifecycle status (S2S only)](https://apidocs.ankatech.co/reference/resolvestatus.md): Reads the tenant's status from the database and re-publishes the HMAC-signed envelope via the single existing publisher. Returns 200 for every resolvable tenant (including SUSPENDED/CLOSED). A non-existent tenant returns a uniform, oracle-safe 404 that does not confirm existence. Requires the ankasecure-core S2S issuer and the admin.s2s.tenant-status.resolve scope. - [Get one of the tenant's notification providers](https://apidocs.ankatech.co/reference/getnotificationprovider.md) - [Replace a tenant provider's configuration (clears its verdict)](https://apidocs.ankatech.co/reference/replacenotificationproviderconfig.md): The recorded verdict described the PREVIOUS configuration, so it is cleared and the provider must be tested again before it can be activated. If the provider was ACTIVE, it stops being so — the database refuses an ACTIVE row whose verdict was measured against different configuration.
A chainPosition in the body is applied with insertion + renumber semantics: the provider moves to that index of this scope's chain for its channel and the others keep their relative order, renumbered contiguously from 0 in one transaction. An ABSENT chainPosition leaves the position unchanged — it is never defaulted to 0, which would silently promote an edited provider to primary.
- [Seal this tenant's provider credential](https://apidocs.ankatech.co/reference/replacenotificationprovidercredential.md): Seals the secret this provider authenticates with, and returns the provider to UNVERIFIED.
The credential is write-only: no endpoint on any plane returns it, and it is never stored on the provider row — it goes to secret custody, sealed under this tenant's own KEK.
The connectivity verdict is discarded. The staleness rule is a fingerprint over the CONFIGURATION, and the credential is deliberately not part of it — so without this, a key rotated to a wrong value would leave the provider ACTIVE on a verdict that proved the PREVIOUS secret worked: a green console, stopped mail, and a passing test in the trail. The provider must pass a test again before it delivers. - [Destroy this tenant's provider credential](https://apidocs.ankatech.co/reference/deletenotificationprovidercredential.md): Destroys the secret this provider authenticates with and returns the provider to DECLARED. The provider itself SURVIVES: its mechanism, sender identity, template coverage and chain position are untouched, so a revocation does not alter delivery topology — which is what makes this usable during an incident rather than only during a rebuild.
The connectivity verdict is discarded with the credential. Re-sealing is not re-testing: after a fresh credential is sealed the provider is still DECLARED and must pass a connectivity test again before enable will accept it.
Idempotent, and never a presence oracle. Destroying a credential that was never sealed answers exactly as destroying one that was. There is deliberately no 409: a refusal that reported "nothing to destroy" would disclose whether material is present to a caller who holds the write scope but not the read scope.
The envelope destruction and the demotion are ONE transaction. There is no state in which the material is gone and a provider bound to it still reads ACTIVE — and that includes the second provider when two mechanisms share one credential, as the two Twilio channels do.
- [List the tenant's own notification providers](https://apidocs.ankatech.co/reference/listnotificationproviders.md): Returns this tenant's declared providers with their most recent connectivity verdict. A tenant with none inherits the deployment's active provider; that inheritance is resolved server-side at send time and is not represented as a row here.
- [Declare a provider for this tenant (inert until tested)](https://apidocs.ankatech.co/reference/declarenotificationprovider.md): The provider is created DECLARED and delivers nothing; until it is activated the tenant continues to inherit the deployment's provider. Activation requires a passing connectivity probe measured against this exact configuration.
- [Run a connectivity test against one of the tenant's providers](https://apidocs.ankatech.co/reference/testnotificationprovider.md): Executed by notification-service — the container that will actually deliver — using the same adapter and the same configuration resolve a real send uses.
A FAILED test is still HTTP 200: it is a verdict about a configuration.
PROBE connects, negotiates TLS, authenticates and delivers nothing. SEND delivers exactly one message to the explicit recipient supplied in the request; there is no default recipient, and the recipient is masked in the audit row.
- [Activate a tenant provider that has passed a current probe](https://apidocs.ankatech.co/reference/enablenotificationprovider.md): Refused with 409 when the verdict is absent, failed, or was measured against different configuration. Refused with 422 when a WhatsApp provider does not map an approved template for every eligible notification type, naming the uncovered ones.
- [Deactivate a tenant provider (retains its verdict)](https://apidocs.ankatech.co/reference/disablenotificationprovider.md): Disabling changes availability, not configuration, so the recorded verdict is RETAINED. The tenant falls back to inheriting the deployment's provider.
- [Check a configuration for this tenant without persisting it](https://apidocs.ankatech.co/reference/validatenotificationproviderconfig.md): Persists nothing and contacts nothing. It answers whether the configuration would be ACCEPTED — shape, required fields, and the mechanism's own pre-contact rules, including the SSRF classification of an operator-supplied relay host and the refusal of plaintext submission to a routable one. The same rules run on the write path, so an accepted answer here is the answer POST will give.
The attempt IS audited, through an independent transaction, because the fact that an operator submitted a configuration for checking is worth keeping even when the answer is no.
- [Whether this tenant's notifications would be delivered right now](https://apidocs.ankatech.co/reference/gettenantnotificationdeliveryreadiness.md): Answers the question the delivery path itself asks — would a message for this tenant go out right now — and never the question does a provider row exist. The two diverge on the state this endpoint exists for: a provider declared with a valid credential and never probed is present, populated and syntactically valid, and delivers nothing, because activation requires a connectivity verdict measured against the configuration the row currently holds. deliverable is therefore the emptiness of the ACTIVE delivery chain and nothing else; every other member exists to EXPLAIN a false and none of them may change it.
The chain is resolved exactly as delivery resolves it: this tenant's own ACTIVE rows first, and the deployment's whole chain as the fallback when the tenant has none. effectiveScope reports which of the two answered — and it reads DEPLOYMENT whenever the fallback was taken, including when that fallback is itself empty.
One entry is returned per reported channel: EMAIL unconditionally — account activation and password reset are email-borne — plus every other chain-borne channel for which a provider row exists at any status, in either scope.
The reported set is EMAIL, SMS and WHATSAPP — the three channels whose delivery resolves the provider chain this verdict is computed from. WEBHOOK is a fourth NotificationChannel value and is reported in NO state, even when a WEBHOOK provider row is declared: a webhook resolves an alert DESTINATION, whose endpoint and signing secret are per-destination and are not in this chain, so a verdict derived here would be an answer to a question webhook delivery never asks.
The blocking vocabulary is ordered, and the order is the precedence, so a scope in more than one condition yields the earliest, which is always the first action the operator must take:
NO_PROVIDER_DECLARED — no row for this channel. Declare one.PROVIDER_UNVERIFIED — rows exist and none carries a PASSED verdict measured against its CURRENT configuration: never probed, last probe FAILED, or probed and then edited. Run the connectivity test.PROVIDER_NOT_ACTIVATED — a current PASSED verdict exists and no row is ACTIVE. Enable the provider.NOT_REMEDIABLE_HERE — substituted for one of the three above when the rows that explain the block are the DEPLOYMENT's, which this administrator does not administer. Contact the platform operator.blockingProviderId naming its own row — even while effectiveScope reads DEPLOYMENT, because activating the provider it already declared is what would restore delivery. NOT_REMEDIABLE_HERE is reserved for a tenant holding no row of its own for that channel, the one case in which the remedy genuinely is not theirs. blockingProviderId is present only when the blocking row lies in the scope this read is about — so, on this plane, only when the tenant holds rows of its own. No deployment provider is ever named here, and no mechanism, status, verdict or count about the deployment is disclosed either.
Absence is key omission, never null. A deliverable channel carries no blocking key at all, rather than a token meaning "nothing is wrong" — a rendered all-clear cannot be distinguished from a read that never happened.
The non-blocking notice member is orthogonal to blocking: it has its own condition, never suppresses and is never suppressed by a blocking token, and both may ride one channel entry. INHERITING_DEPLOYMENT_CHAIN states that this tenant declared rows of its own for the channel and is nonetheless riding the deployment chain, because none of its own rows is currently ACTIVE. A tenant that declared nothing is inheriting by design and receives no notice.
Nothing here is cached. Every call is a fresh resolution, so the verdict moves on the read immediately after the remedy — no invalidation call exists because no cache does. The read is available to every tenant: it is not gated by the edition that governs whether a tenant may CONFIGURE its own providers, because a tenant that cannot configure one is precisely the tenant for whom this verdict is the only thing there is to say. - [Whether this tenant may configure its own delivery providers](https://apidocs.ankatech.co/reference/getnotificationentitlement.md): The verdict the console renders, resolved SERVER-side from the deployment type and the tenant's effective Edition. The console never re-derives it: a surface that decided its own entitlement would be a second party deciding the same thing, and the two would eventually disagree.
On a private-cloud or on-premise deployment the answer is always entitled: true — the operator runs the platform and owns the relay, so there is nothing for an ANKA-issued Edition to entitle. On SaaS it is true only for the Enterprise edition.
resolution is required reading beside the verdict: LICENSED means the licence answered, FLOORED means it could not be read and the reply is the fail-closed floor. "Your edition does not include this" and "we could not confirm your edition" send an operator to two different people, and a bare entitled: false sends half of them to the wrong one.
A tenant WITHOUT the entitlement still receives mail: it inherits the deployment's delivery chain whole.
- [Read a tenant's TSA connection override (platform, masked)](https://apidocs.ankatech.co/reference/listplatformtimestampingoverride.md): Returns the tenant's own TSA connection override (credential always masked ••••••), or 404 when the tenant inherits the deployment default. Required scope: admin.platform.settings.timestamping.override.read.
- [Set a tenant's TSA connection override (platform, blind-provisioning)](https://apidocs.ankatech.co/reference/replaceplatformtimestampingoverride.md): Seals the tenant's OPTIONAL TSA client credential under KEK_SECRET_TENANT_ A Omitting the reason, or sending it blank or whitespace-only, is refused with 400 Nothing is revoked. The Capability Grants, the Actor Credential, the owned Exchange Context and every constraint counter survive the transition untouched, so The cascade runs inside this request's transaction: if the actor transition is refused, the aggregate is not written. An actor already Idempotent on an already-SUSPENDED aggregate → 422. A concurrent transition answers 422 as well, never 409: the aggregate row is taken under A body is now REQUIRED, and it carries a mandatory justification (PRD §231, operation 1). Accepting a body introduces two arms this operation did not have: 400 for a blank, absent or over-long reason, and 415 for a request whose Because suspend revoked nothing, resume recreates nothing: the same Capability Grants, the same never-rotated Actor Credential and the same constraint counters are still in place. Resume restores the actor unconditionally. An actor suspended directly on the actor plane — an incident response, say — is cleared by this call too; the platform stores no reason or owner that would distinguish the two suspensions. The actor's pre-transition status is recorded in the audit trail under this request's correlation id. The cascade runs inside this request's transaction: an actor already A The credential is write-only: no endpoint on any plane returns it, and it is never stored on the provider row — it goes to secret custody, sealed under the DEPLOYMENT KEK — this chain belongs to no tenant. The connectivity verdict is discarded. The staleness rule is a fingerprint over the CONFIGURATION, and the credential is deliberately not part of it — so without this, a key rotated to a wrong value would leave the provider ACTIVE on a verdict that proved the PREVIOUS secret worked: a green console, stopped mail, and a passing test in the trail. The provider must pass a test again before it delivers.
- [Destroy the deployment provider's credential](https://apidocs.ankatech.co/reference/deleteplatformnotificationprovidercredential.md): Destroys the secret this provider authenticates with and returns the provider to DECLARED. The provider itself SURVIVES: its mechanism, sender identity, template coverage and chain position are untouched, so a revocation does not alter delivery topology — which is what makes this usable during an incident rather than only during a rebuild. The connectivity verdict is discarded with the credential. Re-sealing is not re-testing: after a fresh credential is sealed the provider is still DECLARED and must pass a connectivity test again before Idempotent, and never a presence oracle. Destroying a credential that was never sealed answers exactly as destroying one that was — the same status, the same empty body, the same headers. There is deliberately no The envelope destruction and the demotion are ONE transaction. There is no state in which the material is gone and a provider bound to it still reads ACTIVE — and that includes the second provider when two mechanisms share one credential, as the two Twilio channels do.
- [List the deployment's notification providers](https://apidocs.ankatech.co/reference/listplatformnotificationproviders.md): Returns every declared deployment-plane provider with its most recent connectivity verdict and whether that verdict still describes the configuration the row holds. A FAILED test is still HTTP 200: it is a verdict about a configuration, and reporting it as 5xx would make the transport layer answer a configuration question. The attempt IS audited, through an independent transaction: the row records that a configuration was submitted for checking, which is worth keeping even when the answer is no.
- [Whether the deployment would deliver notifications right now](https://apidocs.ankatech.co/reference/getplatformnotificationdeliveryreadiness.md): Answers the question the delivery path itself asks — would a message go out right now — and never the question does a provider row exist. The two diverge on the state this endpoint exists for: a provider declared with a valid credential and never probed is present, populated and syntactically valid, and delivers nothing, because activation requires a connectivity verdict measured against the configuration the row currently holds. One entry is returned per reported channel: The reported set is The blocking vocabulary is ordered, and the order is the precedence, so a plane in more than one condition yields the earliest, which is always the first action the operator must take: Absence is key omission, never The non-blocking Nothing here is cached. Every call is a fresh resolution, so the verdict moves on the read immediately after the remedy — no invalidation call exists because no cache does.
- [Get tenant rotation policy](https://apidocs.ankatech.co/reference/getrotationpolicy.md): Retrieves the rotation policy for a specific tenant. This policy controls algorithm transitions during key rotation operations. Rotation policies enforce rules such as: • Operations subset: new key must support operations of original • No downgrade: prevent transitions to weaker algorithms • Classical to PQC: control quantum-safe transitions • Algorithm family: restrict changes within same family The 'includeInherited' parameter controls the response: • false: Returns only custom tenant policy (404 if not configured) • true: Returns custom policy if exists, otherwise deployment policy
- [Set tenant rotation policy](https://apidocs.ankatech.co/reference/setrotationpolicy.md): Creates or updates the rotation policy for a specific tenant. This operation is idempotent - it will create if not exists, or update if exists. Policy rules control algorithm transitions: • requireOpsSubset: New key must support subset of original operations • requireNoDowngrade: Prevent transitions to weaker algorithms • allowClassicalToPqc: Allow RSA/EC → ML-KEM/ML-DSA transitions • allowPqcToClassical: Allow reverse transitions (usually disabled) • allowDifferentFamily: Allow ML-KEM → ML-DSA (different families) Use cases: • Enforce quantum-safe migration path • Prevent algorithm downgrades • Maintain compliance during transitions
- [Reset tenant rotation policy](https://apidocs.ankatech.co/reference/deleterotationpolicy.md): Removes the custom rotation policy for the tenant. After deletion, the tenant will inherit the deployment-level policy.
- [Get tenant algorithm policy](https://apidocs.ankatech.co/reference/getalgorithmpolicy.md): Retrieves the algorithm availability policy for a specific tenant. This policy controls which cryptographic algorithms the tenant can use. The policy acts as an allow-list that affects: • Algorithm catalogue visible to the tenant • Key generation and import operations • Key rotation requests The 'includeInherited' parameter controls the response: • false: Returns only custom tenant policy (404 if not configured) • true: Returns custom policy if exists, otherwise deployment policy Supported algorithm families: ML-KEM, HQC, FRODO, ML-DSA, SLH-DSA, FALCON, XMSS, LMS, EC, RSA, oct Categories: CLASSICAL, POST_QUANTUM NIST Security Levels: L0, L1, L3, L5 (no L2 or L4) Standards: NIST, ETSI, NSA, ISO, BSI, ANSSI, ENISA Operations: encrypt, decrypt, sign, verify
- [Set tenant algorithm policy](https://apidocs.ankatech.co/reference/setalgorithmpolicy.md): Creates or updates the algorithm availability policy for a specific tenant. This operation is idempotent - it will create if not exists, or update if exists. The tenant policy overrides the deployment-level policy for this tenant only. Policy evaluation follows this order: 1. explicitAlgorithms (if non-empty, immediate ALLOW) 2. Composite test: family → category → level → standards → operations Empty arrays ([]) mean no restriction for that dimension. Use cases: • Restrict tenant to quantum-safe algorithms only • Allow specific algorithms for compliance • Enforce higher security levels than deployment default
- [Reset tenant algorithm policy](https://apidocs.ankatech.co/reference/deletealgorithmpolicy.md): Removes the custom algorithm policy for the tenant. After deletion, the tenant will inherit the deployment-level policy. This operation is idempotent - deleting a non-existent policy returns success. Use this when: • Tenant should align with deployment defaults • Removing special restrictions • Simplifying policy management
- [Toggle the tenant policy override flag](https://apidocs.ankatech.co/reference/patchoverrideflag.md): Atomically sets the `allowsTenantOverride` flag on the three active tenant policy rows (algorithm / rotation / lifecycle) for the given tenant. The mutation runs inside a single `@Transactional` boundary; JPA `@Version` optimistic-lock guards detect concurrent modifications and emit HTTP 409. Required scope: `admin.tenant.policy.override.toggle` (platform-tenant principal only — non-platform JWTs that happen to carry the scope are rejected with 403 by `validatePlatformAccess()`). **Lock-state guarantee**: when the deployment is LOCKED (any deployment policy has `is_immutable=true`), this endpoint returns HTTP 409 with the documented RFC 7807 body BEFORE any tenant lookup is performed. This makes the LOCKED contract a global invariant of the endpoint — callers cannot disambiguate known/unknown tenants by observing 409-vs-other responses. **Root tenant immutability**: attempts to target the root platform tenant are rejected with 403 before the lock-state check is consulted.
- [Get all tenant policies](https://apidocs.ankatech.co/reference/getallpolicies.md): Retrieves all policies configured for a specific tenant in a single response. This includes algorithm availability, rotation, and lifecycle policies. The 'showEffective' parameter determines what is returned: • true: Shows the effective policy (custom if exists, inherited if not) • false: Shows only custom policies (null if not configured) This endpoint is useful for: • Displaying all tenant policies in a dashboard • Understanding policy inheritance • Exporting tenant configuration Policy inheritance chain: Deployment → Tenant → Application
- [Get effective policies](https://apidocs.ankatech.co/reference/geteffectivepolicies.md): Retrieves the effective policies that actually apply to a tenant. This considers the inheritance chain and returns the final policy that would be enforced for cryptographic operations. Effective policy resolution: 1. If tenant has custom policy → use tenant policy 2. If no tenant policy → use deployment policy 3. If no deployment policy → use system defaults This endpoint is essential for: • Understanding what policies are actually enforced • Debugging policy inheritance issues • Validating policy configuration
- [Get deployment lifecycle policy](https://apidocs.ankatech.co/reference/getdeploymentlifecyclepolicy.md): Returns the effective deployment dual-dimension lifecycle ceiling: the persisted active deployment policy when present, otherwise the code-built DEFAULT template. Platform-admin only.
- [Replace deployment lifecycle policy](https://apidocs.ankatech.co/reference/replacedeploymentlifecyclepolicy.md): Full replacement (PUT) of the deployment dual-dimension lifecycle ceiling. The deployment policy is NOT subset-validated — it IS the ceiling tenant policies are validated against. Platform-admin only.
- [Reset deployment lifecycle policy to DEFAULT](https://apidocs.ankatech.co/reference/resetdeploymentlifecyclepolicy.md): Soft-deletes the persisted deployment lifecycle policy so the ceiling falls back to the code-built DEFAULT template. Platform-admin only.
- [Create a cryptographic key](https://apidocs.ankatech.co/reference/createkeywithpermissions.md): Creates a cryptographic key in the tenant's keystore. Workflow: 1. Validates the caller's tenant boundary 2. Calls Core-API to create the cryptographic key (S2S) 3. Returns the created key's identity, algorithm and lifecycle status Authorization is a SEPARATE concern and is not granted here: issue and read access to the key through the Capability Grant endpoints, or compose an Internal Crypto Use Case over the returned `kid`. The endpoint grants nothing and names no application - the `grantTo` field that once carried that half of the operation was removed in the PRD §40 greenfield cutover. Available in every tenant type. A SIMPLE tenant creates keys through exactly this endpoint (PRD §168 FR-168.10): its Internal Crypto Use Cases are an authorization overlay over keys that already exist, so key creation is the one key-plane operation a SIMPLE tenant must be able to perform for itself. Error handling: - If key creation fails, the entire operation is aborted (404, 409, 504 returned)
- [List applications (paginated)](https://apidocs.ankatech.co/reference/listapplications.md): Returns a paginated view of machine-to-machine applications registered under the tenant. **Response shape**: Spring `Page 🔴 The comparison is not string equality, which is why this operation exists. A declaration is stored in canonical form — lowercase scheme and host, default port removed, every trailing slash stripped from the path — while a token's 🔴 It is a 🔴 It carries the READ scope, not a write one, and it does not acquire a write's gates: No match is a 🔴 The It resolves over the bindable set — the same rows Not entitlement-gated. The operator is the party that sells the edition, so refusing them on it would be the platform refusing itself.
- [List an actor's external identities (platform)](https://apidocs.ankatech.co/reference/listplatformtenantactorissuerbindings.md): Every external identity currently able to authenticate as this actor. This is the answer to "which workloads can act as this principal", which is the question an incident asks first. The canonical issuer URL is reported beside the issuer id, because the id alone forces the reader to resolve it against a registry that may since have changed. An actor with no bindings answers an empty array. That is a complete answer, not a missing one: an actor may authenticate through a credential this feature knows nothing about. The ROOT tenant is a valid target for this READ, unlike on the write. A read creates no row and emits no audit, so the ROOT-target refusal has nothing to protect. Not entitlement-gated. The operator is the party that sells the edition, so refusing them on it would be the platform refusing itself. The tenant's own verdict is readable at The The 🔴 No actor is created. A binding naming an actor that does not exist in this tenant is a 404 and writes nothing — on this plane most of all, since creating it would be a platform operator provisioning a customer's principal as a side effect of granting an external identity access to it. Not entitlement-gated. The operator is the party that sells the edition. The audit row is filed under the tenant in the path, with 🔴 It is the set the bind call accepts, not the tenant's own registry. The registry read at No The ROOT tenant is a valid target for this READ, unlike on the write. A read creates no row and emits no audit, so the ROOT-target refusal has nothing to protect. Not entitlement-gated. The operator is the party that sells the edition, so refusing them on it would be the platform refusing itself.
- [Withdraw one workload's access to a tenant's actor (platform)](https://apidocs.ankatech.co/reference/deleteplatformtenantactorissuerbinding.md): Removes the binding. That external identity stops authenticating as this actor on the very next request — no grace window, because the reason an operator removes a binding is usually that it should already have stopped. This is the narrowest revocation this feature has: it withdraws ONE workload without touching the issuer, the other bindings, or anything the customer's other workloads depend on. Disabling the issuer is the wider lever and is a different call. Rebinding is a delete and a create rather than a mutation, because a binding either exists or is gone and there is no intermediate state worth recording. Not entitlement-gated. The operator is the party that sells the edition, so refusing them on it would be the platform refusing itself. The tenant's own verdict is readable at A wrong code is not an error. This endpoint answers the question "is this code correct?", and This matches how the industry treats enrollment verification on an already-authenticated session. Twilio Verify migrated this exact case from Retry limits and lockout are enforced by the enrollment service, not by this status. It rides the declare scope rather than a read scope deliberately: the operation has a side effect an attacker would want, an outbound connection to a host the caller names, and that is a capability someone entitled to declare a backend already has while a reader must not gain it. It persists nothing and it reads nothing. No declaration is created or mutated, no credential is read, sealed or unsealed, no key-encryption key is moved and no lifecycle state changes. Equally, nothing about the tenant's current declaration takes part in the verdict: every value checked travels in the request body, so the answer is a statement about the coordinates being proposed and never about the ones the tenant already holds. Calling it on a tenant that has already bound a backend is explicitly supported and changes nothing. Its one lasting effect is an audit row recording that a configuration was submitted for checking. A FAILED verdict is an HTTP 200. The question is whether the submitted coordinates are admissible and, where they name an operator-specific host, whether that host answers. "It did not answer" is an answer, so it arrives as a 200 carrying It never answers PASSED, and cannot. A pass means key material was wrapped and unwrapped again and came back identical. This operation presents no credential - the request shape carries none - and wraps nothing, so a pass is not something it is able to observe. The green outcome is Which checks run depends on the selected family and on what was submitted, and the verdict names them. An endpoint the declared backend does not accept is a The probe is pinned at TCP connect plus TLS handshake and issues no application-layer request. A vault that completes the handshake and would decline an unauthenticated API call therefore answers 503 never means "the backend did not answer". It means this instance declined to start a check it could not bound, because too many outbound reachability probes are already in flight here. It carries The named backend family is checked against this tenant's admissibility before anything is contacted, so a family the tenant may never declare cannot drive an outbound dial. That refusal is a 422 identical to the declare verb's. Each row carries four independent facts, and their independence is the point. A mechanism can be This is not a connection test. It states whether ANKATech has ever exercised a mechanism, not whether your configured endpoint is reachable right now. The two are independent, and an Non-selectable rows are returned rather than filtered: the server states the status and the caller decides what to render, so a picker and a documentation consumer read the same response. No tenant dimension: the response never varies by tenant, and two callers holding the same authority receive the same bytes. The evidence axes are release-fixed, so The window is fixed at declaration. Default 8 hours (one support shift), hard ceiling 24. A request above the ceiling is refused with 400 and no row is written; it is never silently reduced, because the governance value of a declaration is that its stated terms are its terms. At most one authorisation per tenant. A second declaration while one is LIVE is refused 409 — an authorisation is never replaced, so a repeated request cannot silently extend the window it was granted for. A LAPSED authorisation is cleared and the declaration proceeds; refusing on a lapsed row would leave the tenant with nothing to withdraw and no way to re-declare. The The body carries no session identifier, no session JTI (it is a live revocation key) and no impersonator identity. The actions taken during a session are accounted for in the tenant's audit trail, which is derived from signed events. Always 204, never 404. Withdrawal is a row deletion, so idempotency is structural rather than guarded: withdrawing when nothing is authorised is a successful no-op. A 404 would report whether an authorisation exists, to a caller who is entitled to ask but should not learn it from a status code they can poll. A session already past its own expiry closes as The session is resolved from the caller's own token, never from an identifier in the request. "Current" means the session whose impersonator is this token's A caller running under no impersonation context is refused 403 — it is not a request this endpoint can answer, and answering it as a no-op would report success for a session that was never the caller's. Always 204, never 404. Ending when nothing is open is a successful no-op; a 404 would report whether a session exists.
- [Whether one tenant's edition includes workload identity (platform)](https://apidocs.ankatech.co/reference/getplatformtenantworkloadidentityentitlement.md): The verdict the platform console renders next to that tenant's trust configuration, resolved SERVER-side from the deployment type and the tenant's effective Edition. It is the SAME projection the tenant's own plane returns for the same tenant, from the same resolution — the operator and the customer must not read two verdicts that can disagree. 🔴 It gates nothing here. A tenant reading On a private-cloud or on-premise deployment the answer is always It never fails with a Not entitlement-gated. The operator is the party that sells the edition, so refusing them on it would be the platform refusing itself. The tenant's own verdict is readable at It rides the declare scope rather than a read scope deliberately: the operation has a side effect an attacker would want, an outbound connection to a host the caller names, and that is a capability someone entitled to declare a backend already has while a reader must not gain it. It persists nothing and it reads nothing. No declaration is created or mutated, no credential is read, sealed or unsealed, no key-encryption key is moved and no lifecycle state changes. Equally, nothing about the tenant's current declaration takes part in the verdict: every value checked travels in the request body, so the answer is a statement about the coordinates being proposed and never about the ones the tenant already holds. Calling it on a tenant that has already bound a backend is explicitly supported and changes nothing. Its one lasting effect is an audit row recording that a configuration was submitted for checking. A FAILED verdict is an HTTP 200. The question is whether the submitted coordinates are admissible and, where they name an operator-specific host, whether that host answers. "It did not answer" is an answer, so it arrives as a 200 carrying It never answers PASSED, and cannot. A pass means key material was wrapped and unwrapped again and came back identical. This operation presents no credential - the request shape carries none - and wraps nothing, so a pass is not something it is able to observe. The green outcome is Which checks run depends on the selected family and on what was submitted, and the verdict names them. An endpoint the declared backend does not accept is a The probe is pinned at TCP connect plus TLS handshake and issues no application-layer request. A vault that completes the handshake and would decline an unauthenticated API call therefore answers 503 never means "the backend did not answer". It means this instance declined to start a check it could not bound, because too many outbound reachability probes are already in flight here. It carries The named backend family is checked against this tenant's admissibility before anything is contacted, so a family the tenant may never declare cannot drive an outbound dial. That refusal is a 422 identical to the declare verb's. On an ANKA-operated deployment the endpoint is judged more narrowly here than on the platform-plane operation. Private and unique-local addresses are refused with a 400: the network they name belongs to the platform operator, not to the tenant. On a customer-operated deployment, where the operator is the customer's own staff, they are accepted exactly as everywhere else - a VPC interface endpoint and a Private Link vault are ordinary production shapes there. An ACTIVE provider whose configuration is replaced becomes unverified again and stops delivering until it is retested and re-enabled. A The credential is write-only: no endpoint on any plane returns it, and it is never stored on the provider row — it goes to secret custody, sealed under that TENANT's KEK — the PATH tenant, never the caller's. The connectivity verdict is discarded. The staleness rule is a fingerprint over the CONFIGURATION, and the credential is deliberately not part of it — so without this, a key rotated to a wrong value would leave the provider ACTIVE on a verdict that proved the PREVIOUS secret worked: a green console, stopped mail, and a passing test in the trail. The provider must pass a test again before it delivers.
- [Destroy a tenant provider's credential (platform)](https://apidocs.ankatech.co/reference/deleteplatformtenantnotificationprovidercredential.md): Destroys the secret this provider authenticates with and returns the provider to DECLARED. The material removed is the PATH tenant's, sealed under that tenant's own KEK — never the caller's. The provider itself SURVIVES: its mechanism, sender identity, template coverage and chain position are untouched, so a revocation does not alter delivery topology. The connectivity verdict is discarded with the credential. Re-sealing is not re-testing: after a fresh credential is sealed the provider is still DECLARED and must pass a connectivity test again before Idempotent, and never a presence oracle. Destroying a credential that was never sealed answers exactly as destroying one that was. There is deliberately no The envelope destruction and the demotion are ONE transaction. There is no state in which the material is gone and a provider bound to it still reads ACTIVE — and that includes the second provider when two mechanisms share one credential, as the two Twilio channels do.
- [List a tenant's delivery chain (platform)](https://apidocs.ankatech.co/reference/listplatformtenantnotificationproviders.md): The ordered provider rows a tenant has declared, across every channel, with each row's most recent connectivity verdict. An EMPTY list is the normal and common answer, and it does not mean the tenant receives no mail: a tenant that declares no chain inherits the deployment's chain whole, resolved per send. The list says what this tenant has configured OF ITS OWN, nothing more. Credentials are never returned, on any plane.
- [Blind-provision a delivery provider for a tenant (platform)](https://apidocs.ankatech.co/reference/declareplatformtenantnotificationprovider.md): Declares a provider on the TENANT's chain, on the operator's authority, without reading the tenant's data. The row is created The ROOT platform tenant is not a valid target: its chain IS the deployment plane, reached at The verdict is recorded against the configuration the row holds NOW, and an edit invalidates it. A provider can only be enabled on a passing verdict measured against its current configuration.
- [Activate a tenant's provider (platform)](https://apidocs.ankatech.co/reference/enableplatformtenantnotificationprovider.md): Activation is the gate, not saving. A provider becomes ACTIVE only on a PASSING verdict measured against the configuration it holds now — never tested, tested and failed, and tested against different configuration are three different refusals sending an operator to three different next actions, and the 409 detail says which.
- [Deactivate a tenant's provider (platform)](https://apidocs.ankatech.co/reference/disableplatformtenantnotificationprovider.md): Removes the provider from delivery and RETAINS its verdict: availability changed, configuration did not, so re-enabling needs no new test. Disabling the tenant's last ACTIVE provider does not stop the tenant's mail — the tenant falls back to inheriting the deployment chain whole.
- [Check a configuration for a tenant without persisting it (platform)](https://apidocs.ankatech.co/reference/validateplatformtenantnotificationproviderconfig.md): Answers whether a configuration would be ACCEPTED, and records the attempt. It persists nothing and contacts nothing — the pre-contact rules (internal or unresolvable host, plaintext transport) are evaluated locally, so a refusal costs no socket in either container. Scoped The attempt is audited either way. A refused check is the row worth having: it is the same action repeated while probing what the SSRF guard permits.
- [Whether this tenant's notifications would be delivered right now (platform)](https://apidocs.ankatech.co/reference/getplatformtenantnotificationdeliveryreadiness.md): Answers the question the delivery path itself asks — would a message for this tenant go out right now — and never the question does a provider row exist. The two diverge on the state this endpoint exists for: a provider declared with a valid credential and never probed is present, populated and syntactically valid, and delivers nothing, because activation requires a connectivity verdict measured against the configuration the row currently holds. This is the platform plane's read of a tenant's verdict, and it is what a surface acting INTO a tenant consumes — the create-user and force-password-reset flows an operator runs on a tenant's behalf. The tenant-plane twin ( The chain is resolved exactly as delivery resolves it: the tenant's own ACTIVE rows first, and the deployment's whole chain as the fallback when the tenant has none. One entry is returned per reported channel: The reported set is The blocking vocabulary is ordered, and the order is the precedence, so a scope in more than one condition yields the earliest, which is always the first action that must be taken: Absence is key omission, never The non-blocking The ROOT platform tenant is a valid target HERE, unlike on every write of this plane: a read creates no row and emits no audit, so the ROOT-target refusal has nothing to protect, and the surfaces bound to the platform tenant must be able to render a verdict. The ROOT tenant owns no provider rows, so its answer is the deployment chain's, reported with Nothing here is cached. Every call is a fresh resolution, so the verdict moves on the read immediately after the remedy — no invalidation call exists because no cache does.
- [Whether this tenant could configure its own delivery providers (platform)](https://apidocs.ankatech.co/reference/getplatformtenantnotificationentitlement.md): The verdict the tenant's OWN surface is gated on — exposed here as a READ so an operator provisioning on a tenant's behalf knows whether that tenant can also do it themselves. It gates nothing on this plane. A negative verdict does not stop the operator writing; it tells them they will remain the only writer. The lever that changes the verdict is the Edition assignment ( The tenant-plane twin of this endpoint answers 403 for a SaaS operator, which is the whole reason this one exists. It persists nothing and it reads nothing. No tier row is created or mutated, no settings row is written, no credential is read, sealed or unsealed, no key-encryption key is moved and no lifecycle state changes. Equally, nothing about the tier's current configuration takes part in the verdict: every value checked travels in the request body, so the answer is a statement about the coordinates the operator is proposing and never about the ones the tier already holds. Calling it on an ACTIVE tier is explicitly supported and changes nothing - that is the tier whose replacement an operator most needs to rehearse. Its one lasting effect is an audit row recording that a configuration was submitted for checking. A FAILED verdict is an HTTP 200. The question is whether the submitted coordinates are admissible and, where they name an operator-specific host, whether that host answers. "It did not answer" is an answer, so it arrives as a 200 carrying It never answers PASSED, and cannot. A pass means key material was wrapped and unwrapped again and came back identical. This operation presents no credential - the request shape carries none - and wraps nothing, so a pass is not something it is able to observe. The green outcome is Which checks run depends on the selected family and the verdict names them. Whether an endpoint may be supplied at all is the backend's own declared requirement, and it is enforced here before anything is judged or dialled - so an endpoint under a family that accepts none, or a missing one under a family that requires it, is a The probe is pinned at TCP connect plus TLS handshake and issues no application-layer request. A vault that completes the handshake and would decline an unauthenticated API call therefore answers 503 never means "the backend did not answer". It means this instance declined to start a check it could not bound, because too many outbound reachability probes are already in flight here. It carries It is still called This operation selects the READER axis, not the subject axis. It returns the rows whose An impersonated read names the operating human. The row SET is unchanged by all of this: the new members are columns on rows this token could already read, and no row becomes visible that was not visible before. The response carries the period it speaks for and the total across ALL identities in that period — never the sum of the rows shown. A client that summed a truncated ranking would understate the period by exactly the tail it could not see. Both denominators are served, so the console derives neither. This does NOT answer "whose audit trail was read". That is the subject axis, and it has its own operation — The operationId and the ranking itself are unchanged — only the label is. The defect was the label. A deliberately separate operation from An empty ranking is a legible answer, not a fault. A read that addressed no single tenant contributes no row at all — absence is honest, and an empty-string bucket would collide with the reader-tenant total. Where every read is platform-scoped the ranking is correctly empty and the total is 0. The period total counts the TARGET_TENANT population — the reads that addressed a tenant — and never the reader-tenant total, because a platform-scoped read appears in the second and not the first. The endpoint values are exactly the vocabulary the explorer's The two are independent, and that is the point. No signal takes a threshold from the caller. Every bound is derived from the data — a percentile over the window's own sample, a reader against their own trailing median — because a threshold a caller can move is a signal a caller can silence. Each signal carries the explorer filter the server actually used, so a click opens the contributing rows with nothing re-derived in the browser. A signal whose population IS one row predicate carries that predicate in The contributor list is BOUNDED and Every contributor filter is expressed in Every derivation travels as three fields, always all three. The verdict is PROJECTED, never recomputed. The integrity subsystem already verifies this plane's WORM chain and publishes the result under Three states, not two. Besides No hash, signature, sequence number or key version appears in this body. Every member of it also appears on the platform integrity verdict, which is what makes serving it under the lesser The verdict names its populations. The filters are grouped on two axes and every parameter names the axis it belongs to, because the two answer different questions and conflating them is the defect this operation replaces: The An oversized Send Cells beginning The query is keyed on the INTEGRATION, never on a live entity id — which is the whole point. A query keyed on an entity id can only be composed while the entity still exists, so a deletion became unreadable the moment it succeeded. Here the CREATE, the UPDATEs and the DELETE of a resource that no longer exists all come back. The addressable slug set is the TENANT set, and it is narrower than the platform one on two independent counts: a platform-administered integration (licensing, geolocation, the RapidAPI gateway) is not addressable here at all, and an integration none of whose entity types is a disclosed governance family is not offered either — because a filter that can only ever answer with an empty page is the silent failure this feature exists to remove. Read the addressable set from Evaluation order is 401 → 403 (scope) → 403 (tenant) → 404 (slug). A caller refused at either 403 never reaches the slug, so it cannot learn the deployment's integration inventory by comparing 404 against 403. A slug that exists but is platform-administered receives the SAME 404 body as a nonsense one. The projection is plane-scoped. A caller holding any It is also source-scoped. Each dimension belongs to an event source and is projected only when the caller's scopes admit that source: Every offered value is accepted by this plane's queries; the reverse does not hold, and deliberately so. The query is keyed on the INTEGRATION, never on a live entity id. That is the point of the endpoint: a query keyed on an entity id can only be composed while the entity still exists, so a deletion became unreadable the moment it succeeded. Here the CREATE, the UPDATEs and the DELETE of a destination that no longer exists all come back. The Evaluation order is 401 → 403 (scope) → 403 (plane) → 404 (slug). A caller without a platform audit scope, or without the ROOT tenant, is refused BEFORE the slug is looked at — so an unauthorized caller cannot learn which integrations this deployment has by comparing 404 against 403.reason is mandatory. Suspension is the transition that removes a paying customer's capability and the one a customer disputes, so the operator's justification is recorded verbatim on the admin audit event and signed with it. The sibling verbs activate and reactivate take no body: both restore capability and generate no grievance to answer.application/problem+json and the tenant's status is unchanged. A request carrying no Content-Type, or a non-JSON one, is refused with 415 before the body is read at all.keyKid and targetKeyKid are references to keys the tenant already owns; provision keys with POST /api/v3/admin/tenants/{tenantId}/keys (scope admin.keys.orchestrate) and name them here. The key keeps its own lifecycle: revoking or deleting this use case leaves it untouched, and several use cases may reference one key. A key reference that names no key in the tenant is 404. A key that exists but cannot carry the declared operations is 422, with a distinct type per cause. Every refusal happens before anything is provisioned, so a rejected request creates nothing. The credential plaintext is returned ONCE in the 201 response body — capture it immediately. Scope required: admin.tenant.internal-use-case.create
- [Suspend an ACTIVE use case](https://apidocs.ankatech.co/reference/suspendinternalcryptousecase.md): Transitions the aggregate ACTIVE → SUSPENDED and cascades to the owned Cryptographic Actor, which is what actually stops the workload: the actor moves to SUSPENDED and its signed effective-status envelope is published, so core-api answers 403 actor-suspended on the next crypto call — including one presented with an already-issued, still-valid JWT — and auth-api answers 401 invalid_client for a new token request. PATCH /resume restores the same workload rather than provisioning a new one.SUSPENDED is a satisfied step (200, no second write); an actor DISABLED is refused 422 under its OWN problem type, .../terminal-actor-state, so a client can tell “retrying is pointless, revoke instead” from “you sent the wrong verb” without string-matching the detail. The exit is still named in the detail for a human reader.PESSIMISTIC_WRITE, so the second caller blocks and then reads COMMITTED state — an already-SUSPENDED aggregate — and is refused as an invalid transition. The 409 optimistic-lock arm exists on this service but is unreachable from this path.reason is recorded verbatim at canonical position 21 of the signed AdminAuditEvent, so an audit row can no longer say that a workload was stopped without saying why. It is bounded at AuditFieldBounds.MAX_REASON_LENGTH (1,000 UTF-8 BYTES, not characters) — the same bound applied before signing, so no accepted value is silently truncated on its way into the preimage — and it is NEVER written to a log line.Content-Type is not application/json. Every refusal on this path — 404, the two state refusals, the DISABLED-actor refusal and both new ones — records the submitted justification NOWHERE, because the act did not happen. Scope: admin.tenant.internal-use-case.suspendACTIVE and has its signed effective-status envelope republished, so crypto operations and new token requests both succeed again. ACTIVE is a satisfied step (200, no second write); an actor DISABLED is refused 422 under its OWN problem type, .../terminal-actor-state, so a client can tell “retrying is pointless, revoke instead” from “you sent the wrong verb” without string-matching the detail. Idempotent on an already-ACTIVE aggregate → 422, and a concurrent transition → 422 as well, never 409 — the row is held under PESSIMISTIC_WRITE, so the loser reads COMMITTED state rather than failing an optimistic-lock check. Scope: admin.tenant.internal-use-case.resumeeffective names the rung that won and the backend token it resolves to. rungs carries all three, highest first, exactly one of them WINNER — and each of the other two carries a machine-readable reason from a closed vocabulary, so a client can distinguish this tenant has no Edition override from this tenant's Edition override is being overruled by its own declaration. Those lead to different operator actions. changeRefusals is the part an operator arriving from a refusal needs. There is no re-wrap path on this platform: once key material is wrapped under a backend it stays there, so every rung change is PREVENTED rather than migrated. Five transitions can be refused and all five answer 409, which is precisely why a client cannot tell them apart by status. Each row names the transition, the rung whose gate refuses it, the population that refusal protects, and the verdict for this tenant right now. An unverifiable key-material probe is a 503, never a permissive answer. The verdicts are read from the same probe the write plane is gated on. If it cannot be verified the refusal posture is unknown, and this read fails closed rather than reporting a transition as permitted that the write plane is about to refuse. It is not a free read. Building changeRefusals drives up to TWO outbound service-to-service probes of core-api: the tenant-scoped key-material check on every call, and the tier-scoped population check when this tenant carries an Edition override. Neither passes through the reachability permit set that bounds the connection tests, so a console that POLLS this endpoint fans out to core-api on every poll. Read it on navigation and on an explicit refresh — not on a timer. This plane is ROOT-only and discloses the full ladder, tokens included — the operator reading it is the deployment's own. It writes nothing: no audit row, no event, no state change. Required scope: admin.platform.key-backend.byok.read, plus the ROOT platform tenant.
- [List constraint policies in tenant](https://apidocs.ankatech.co/reference/listconstraintpolicies.md)
- [Create constraint policy](https://apidocs.ankatech.co/reference/createconstraintpolicy.md): Constraint types: `max_usage` (absolute cap), `valid_from`/`valid_until` (temporal gate), `rate_limit` (per-window throttle — 0 = emergency freeze, null = unlimited), `revoke_on_use` (single-use, auto-revoked after success).
- [Get constraint policy](https://apidocs.ankatech.co/reference/getconstraintpolicy.md)
- [Soft-delete constraint policy](https://apidocs.ankatech.co/reference/deleteconstraintpolicy.md): Does NOT cascade to Capability Grants referencing this policy — grants keep their existing policy snapshot. Use revoke/re-grant to swap enforced policy on existing grants.
- [Update constraint policy (PATCH semantics — full replace of editable fields)](https://apidocs.ankatech.co/reference/updateconstraintpolicy.md)
- [Regenerate activation token (cross-tenant)](https://apidocs.ankatech.co/reference/regenerateactivationtokencrosstenant.md): Regenerates activation token for a user in any tenant. **Platform Administrator Operation:** This endpoint allows platform administrators to regenerate activation tokens for users across all tenants, bypassing tenant isolation. Use this when: • Token has expired and user cannot activate • User lost activation email • Troubleshooting activation issues **Authorization:** Requires `platform.users.regenerate-activation-token` scope. Restricted to users in System Administration tenant (001).
- [Force password reset (cross-tenant)](https://apidocs.ankatech.co/reference/forcepasswordreset.md): Forces a password reset for an already-activated user in any tenant. **Use Cases:** • User has forgotten their password and cannot access their email for self-service reset • Security incident response: credentials may have been compromised • Support-assisted password recovery with identity verification **What This Operation Does:** 1. Sets `requirePasswordChange=true` on the user account 2. Generates a new activation token (valid for configured expiration period) 3. Invalidates ALL existing sessions for the user (forces logout from all devices) 4. Returns activation token to send to the user **Important:** The user will NOT be able to login until they use the activation link sent to their email address to set a new password via the Admin Console. **Difference from regenerate-activation-token:** - `regenerate-activation-token`: Only works for users who are ALREADY pending activation - `force-password-reset`: Works for users who have ALREADY activated their account **OWASP Compliance:** Follows the OWASP Forgot Password Cheat Sheet recommendation for support-assisted password recovery with session invalidation. **Authorization:** Requires `platform.users.force-password-reset` scope. Restricted to users in System Administration tenant (001).
- [Correct a tenant user's login identity (cross-tenant)](https://apidocs.ankatech.co/reference/correcttenantuseremail.md): Corrects the email address of an existing user of the named tenant. That address is the user's login identity, so this operation changes what they sign in with. **What this operation does** 1. Refuses if the TARGET tenant is the platform tenant (00000000-0000-0000-0000-000000000001). A caller holding only this scope must never be able to redirect a platform-plane account's login identity to a mailbox they control. 2. Refuses unless the deployment is customer-operated and the caller is on the platform plane. 3. Refuses unless the target user has a live row in the named tenant. 4. Normalises the address to lower case and stores it. The address is the login identity from that moment; the previous one no longer authenticates. 5. Revokes the target's live access and refresh tokens, so a session opened under the old identity cannot be refreshed. 6. **If the user has not activated yet** (requirePasswordChange = true) **and both that account and its tenant are ACTIVE**, invalidates the outstanding activation link and issues a NEW one to the corrected address. **Step 6 is the reason this operation exists.** A tenant provisioned with a mistyped adminEmail is unreachable: the activation link went to a mailbox nobody can open, and that administrator is by construction still pending. Correcting the address without re-issuing the link would leave the tenant exactly as unreachable as before. **The outstanding link is not left alive.** The previous activation token is invalidated before the replacement is minted, and if it cannot be invalidated the whole operation is refused 503 with nothing changed — a 200 here would mean two live activation links, one of them addressed to the wrong mailbox. Independently, a token whose loginId is no longer this account's login identity is refused at redemption, which is what makes a link issued before the platform tracked activation JTIs unusable after a correction. **Three outcomes, and activationReissued separates only one of them.** Two of the three report false: * **re-issued** — the user was pending and both statuses admit a link. activationReissued = true. * **withheld** — the user is still pending, but the account or its tenant is not ACTIVE. The address IS corrected and nothing is minted, because a live activation link for a non-ACTIVE account is a credential that begins working the moment the account is resumed. activationReissued = false. The operator resumes the account and then uses the activation-token regeneration operation; the link sent to the previous address is refused at redemption regardless, because its loginId no longer names this account's login identity. * **already activated** — nothing is minted, because there is no pending link to replace. activationReissued = false. message is what distinguishes the last two. **Authorization:** requires platform.users.appoint-tenant-administrator, a platform-exclusive scope. Restricted to System Administration tenant (001) users on PRIVATE_CLOUD / ON_PREMISE deployments.
- [Appoint a tenant user as tenant administrator (cross-tenant)](https://apidocs.ankatech.co/reference/appointtenantadministrator.md): Grants the built-in TENANT_ADMINISTRATOR composite role to an existing, activated user of the named tenant, from the platform plane. **What this operation does** 1. Resolves the built-in TENANT_ADMINISTRATOR catalogue row — the global one (tenant_id IS NULL AND is_system AND deleted_at IS NULL), never a tenant-local role that merely shares the name. 2. Refuses if the TARGET tenant does not admit that role. The platform tenant (00000000-0000-0000-0000-000000000001) does not: a TENANT_ composite is a tenant-context role, and admitting it there would let a platform caller grant itself admin.keys.orchestrate, which no platform composite carries. 3. Refuses unless the deployment is customer-operated and the caller is on the platform plane. 4. Refuses unless the target user exists, lives in the named tenant, and both the user and the tenant are ACTIVE. 5. **Adds** the role. Roles the user already holds are kept — this is not the replace-set semantics of POST /tenants/{tenantId}/users/{userId}/roles. **Idempotency:** re-appointing a user who already holds the role answers 409 rather than silently succeeding, so an operator learns the state they are in. **Authorization:** requires platform.users.appoint-tenant-administrator, a platform-exclusive scope. Restricted to System Administration tenant (001) users on PRIVATE_CLOUD / ON_PREMISE deployments.
- [Get paginated pending activations (cross-tenant)](https://apidocs.ankatech.co/reference/getallpendingactivations.md): Returns a paginated envelope of users across ALL tenants who are pending account activation (requirePasswordChange=true). Includes activation status (PENDING or EXPIRED), time remaining, and tenant names for easy identification. **Pagination:** Use `page` (0-based) and `size` query parameters. Default: page=0, size=15. **Response envelope fields:** `content`, `totalElements`, `totalPages`, `page`, `size`. **Platform Administrator Operation:** This endpoint provides global visibility into pending activations across the entire platform. Useful for identifying expired tokens, monitoring activation completion rates, and prioritizing support interventions. **Authorization:** Requires `platform.users.pending-activations.read` scope. Restricted to users in System Administration tenant (001).
- [Get one deployment notification provider](https://apidocs.ankatech.co/reference/getplatformnotificationprovider.md)
- [Replace a deployment provider's configuration (clears its verdict)](https://apidocs.ankatech.co/reference/replaceplatformnotificationproviderconfig.md): The recorded verdict described the PREVIOUS configuration, so it is cleared and the provider must be tested again before it can be activated. An edit that produces byte-identical configuration keeps its verdict — the same rule stated precisely, not a second rule. chainPosition in the body is applied with insertion + renumber semantics: the provider moves to that index of this scope's chain for its channel and the others keep their relative order, renumbered contiguously from 0 in one transaction. An ABSENT chainPosition leaves the position unchanged — it is never defaulted to 0, which would silently promote an edited provider to primary.
- [Seal the deployment provider's credential](https://apidocs.ankatech.co/reference/replaceplatformnotificationprovidercredential.md): Seals the secret this provider authenticates with, and returns the provider to UNVERIFIED. enable will accept it. 409: a refusal that reported "nothing to destroy" would disclose whether material is present to a caller who holds the write scope but not the read scope. verdictIsCurrent is computed server-side: it is what decides whether an activation will be accepted, and a console that derived it independently would be a second party deciding the same thing.
- [Declare a deployment notification provider (inert until tested)](https://apidocs.ankatech.co/reference/declareplatformnotificationprovider.md): The provider is created DECLARED and delivers nothing. Activation requires a passing connectivity probe measured against this exact configuration — there is no field on this request that can skip that, because the database refuses an ACTIVE row whose verdict was measured against different configuration.
- [Run a connectivity test against a deployment provider](https://apidocs.ankatech.co/reference/testplatformnotificationprovider.md): Executed by notification-service — the container that will actually deliver — using the same adapter and the same configuration resolve a real send uses. A verdict produced anywhere else would be a verdict about something else. PROBE connects, negotiates TLS, authenticates and delivers nothing. SEND delivers exactly one message to the explicit recipient supplied in the request; there is no default recipient.
- [Activate a deployment provider that has passed a current probe](https://apidocs.ankatech.co/reference/enableplatformnotificationprovider.md): Refused with 409 when the verdict is absent, failed, or was measured against different configuration — the remedy is to run a test. Refused with 422 when a WhatsApp provider does not map an approved template for every eligible notification type, naming the uncovered ones: partial coverage is not partial activation, because an unmapped type is refused by the provider at send time.
- [Deactivate a deployment provider (retains its verdict)](https://apidocs.ankatech.co/reference/disableplatformnotificationprovider.md): Disabling changes availability, not configuration, so the recorded verdict is RETAINED and a later re-enable does not require re-testing something that was never edited.
- [Check a configuration without persisting it](https://apidocs.ankatech.co/reference/validateplatformnotificationproviderconfig.md): Persists nothing and contacts nothing. It answers whether the configuration would be ACCEPTED — shape, required fields, and the mechanism's own pre-contact rules, including the SSRF classification of an operator-supplied relay host and the refusal of plaintext submission to a routable one — so an operator learns about a rejection before committing to it. The same rules run on the write path, so an accepted answer here is the answer POST will give. deliverable is therefore the emptiness of the ACTIVE delivery chain and nothing else; every other member exists to EXPLAIN a false and none of them may change it. EMAIL unconditionally — account activation and password reset are email-borne — plus every other chain-borne channel for which a provider row exists at any status. EMAIL, SMS and WHATSAPP — the three channels whose delivery resolves the provider chain this verdict is computed from. WEBHOOK is a fourth NotificationChannel value and is reported in NO state, even when a WEBHOOK provider row is declared: a webhook resolves an alert DESTINATION, whose endpoint and signing secret are per-destination and are not in this chain, so a verdict derived here would be an answer to a question webhook delivery never asks.
NO_PROVIDER_DECLARED — no row for this channel. Declare one.PROVIDER_UNVERIFIED — rows exist and none carries a PASSED verdict measured against its CURRENT configuration: never probed, last probe FAILED, or probed and then edited. Run the connectivity test.PROVIDER_NOT_ACTIVATED — a current PASSED verdict exists and no row is ACTIVE. Enable the provider.NOT_REMEDIABLE_HERE, the fourth member of the vocabulary, is never emitted on this read: it names an operator who owns a remedy the caller does not, and on the deployment plane the caller administers every row that could be blocking. null. A deliverable channel carries no blocking key at all, rather than a token meaning "nothing is wrong" — a rendered all-clear cannot be distinguished from a read that never happened. notice member reports an informational state that is orthogonal to blocking and never suppresses it. It cannot arise on this read, which is about the deployment and therefore cannot be inheriting. iss identifies, or none. iss arrives exactly as its issuer wrote it, and Auth0 always emits the trailing slash. A raw comparison therefore reports NO MATCH for a configuration that is correct and will authenticate at runtime, and no spelling of the declaration can make it agree. This runs the SAME selection the operator probe and authentication run, so a client renders a verdict instead of computing one. POST, and that is deliberate — do not "correct" it to a GET. The edge and environment NGINX both log "$request", the full request line including the query string, so an iss in a query parameter — or in a path segment, which is the same thing doubly encoded — would become a durable tenant-to-issuer correlation in an access log whose retention this surface does not control. The identifier travels in a body so that it is written nowhere. rejectRootTarget is NOT called, because that rule exists to keep an audit row from being mis-attributed and this operation writes no row and emits none. The ROOT tenant is a valid target here, exactly as it is for the reads beside it. 200 with matched: null — never a 404 and never a 204. A distinct status would be a third meaning of 404 on this controller, alongside the tenant-existence 404, and a caller could then tell them apart. A blank, malformed or inadmissible iss is likewise a 200/null rather than a 400. The one 400 this operation can answer is TRANSPORT — a body it cannot read, or one carrying a field it does not declare — which says the request was not one this operation accepts and says nothing about the tenant. maxLength the request schema publishes is DOCUMENTARY, not a second refusal. It names the bound canonicalizeIssuerUrl applies before it parses anything; an oversized iss is still answered 200 with matched: null, exactly like every other value nothing declares. A generated client or schema-validating gateway that refused an oversized value LOCALLY would author on the client side the very second refusal shape this operation deliberately does not have — so read it as the rule's own bound, published for a reader, never as a precondition to enforce. ../bindable-issuers lists, with no enabled filter — and a matched row is identical to the row that list publishes for it, inherited displayName withheld and all. ../workload-identity/entitlement (SR-10.6).
- [Bind an external identity to a tenant's actor (platform)](https://apidocs.ankatech.co/reference/createplatformtenantactorissuerbinding.md): Grants one external workload identity — an issuer plus the sub claim its tokens carry — the right to authenticate as one of this tenant's Cryptographic Actors. This is the act that completes an onboarding: until it exists, a token from a trusted issuer authenticates nothing. issuerId must be one THIS TENANT trusts — either a declaration of its own or a deployment-scoped one every tenant inherits. An id it cannot reach answers 404, including one belonging to another tenant, answered identically. subject is compared verbatim and case-sensitively against the token's sub. Take it from the machine-to-machine application in the external IdP rather than typing it: Auth0 emits <clientId>@clients, Keycloak the service-account user id, and a SPIFFE-shaped issuer a full spiffe:// URI. actorPlane = PLATFORM and the operator's own username, so the customer's own audit query returns it.
- [List the issuers a binding in this tenant may name (platform)](https://apidocs.ankatech.co/reference/listplatformtenantbindableissuers.md): Every trusted-issuer declaration a binding for THIS TENANT may name: the ones the DEPLOYMENT declares, which every tenant inherits, concatenated with the ones this tenant declared for itself. Each row carries scope, so an operator can tell an inherited declaration from the customer's own without a second call. ../workload-identity/issuers answers what this tenant may edit and withdraw; an onboarding form offering that answer would omit every deployment-scoped issuer, which on a centrally declared deployment is the whole list. enabled filter: the supported order of work is declare, bind the workloads, then enable, so a declared-but-not-yet-enabled issuer is bindable and is returned. A withdrawn (soft-deleted) declaration is not. displayName is null on an inherited row — it is a label written for the deployment's own purposes and is not part of what this tenant is being shown. ../workload-identity/entitlement (SR-10.6).
- [List lifecycle policy templates](https://apidocs.ankatech.co/reference/listlifecycletemplates.md): Returns the available dual-dimension lifecycle templates (DEFAULT and STRICT), each fully populated with the alias and material dimensions. The Admin Console binds its template picker to this payload (UC-5).
- [Verify MFA setup](https://apidocs.ankatech.co/reference/verifymfa.md): Verify a TOTP code to complete MFA enrollment and enable two-factor authentication. false is a legitimate answer to it: the request was well-formed, the caller is authenticated, and the check ran to completion. The outcome is carried in the body's verified field with a 200, never as a failure status.403 to 200, reasoning that an incorrect code is normal behaviour from a user mistyping rather than a bad request or a system issue that warrants an error. Okta and Auth0 do answer with an error status, but for factor verification performed during authentication, where the authentication itself is what failed. This endpoint is guarded by isAuthenticated() and takes its user and tenant from the JWT, so nothing about the caller is in question.admin.platform.key-backend.byok.declare, plus the ROOT platform tenant. FAILED/UNREACHABLE. A client that renders that as a transport error loses the distinction that makes the operation useful.NOT_APPLICABLE. Nothing about authorization, entitlement to the named key, or key material is known until the backend is declared and bound.SHAPE and ENDPOINT_POLICY always run. KEY_ID_GRAMMAR_PARSED runs when a keyId was supplied AND the declared family publishes a grammar for it - aws-kms, gcp-kms and gcp-kms-hsm do; Azure key names and PKCS#11 labels have none, and for those the entry is ABSENT rather than reported as vacuously passing. TRANSPORT_PROBE runs only when an endpoint is supplied - so for the four PKCS#11 tokens and the two GCP tokens, which name no operator-specific host, nothing is contacted and the verdict carries no TRANSPORT_PROBE entry at all. That is what stops a green answer reading as "everything is fine".422, not a verdict, and neither is one omitted where the backend requires it. The rule is the declared per-backend requirement the DECLARE verb enforces, read here by the same single reader - so a body this check admits is a body the declare admits, and a rehearsal cannot come back green for a submission the commit refuses.ENDPOINT_ANSWERED, because it was never asked anything to decline.Retry-After and no verdict member at all, because a bounded-out request contacted nothing.admin.platform.key-backend.byok.test, plus the ROOT platform tenant. A failed test is an HTTP 200. The question this operation asks is whether the backend works; "it does not" is an answer to that question, so it arrives as a 200 carrying a FAILED verdict. A non-2xx means something different and incompatible: the test could not be run at all. A client that renders both as "error" loses the distinction that makes this operation useful, because the two call for opposite operator actions. The verdict's reason comes from a closed vocabulary (SelfTestReason) that can carry no credential, ARN, resource path, vault URL, region or account id, and no vendor exception text. A client MUST tolerate a reason it does not recognise and render it as an unknown verdict — never as a pass. It changes nothing: no state advances, no key material is written, no KEK is provisioned on either the delegated BYOK path or the in-process managed-tier path, and the backend remains changeable afterwards. It is therefore admissible in EVERY backend state, including CHANGING — which is the state in which an operator most needs it — and it never answers 409. Like the other platform-plane verbs it is blind: the tenant comes from the PATH and no tenant-data read is required. Not every status below is reachable for every tenant. A tenant on an ANKA-managed tier is tested IN THIS SERVICE's own process rather than delegated, so the 502 and 504 — which describe a delegation — cannot occur for it. They apply to a tenant that brought its own backend, and to a tenant that inherits the deployment's.
- [Bind a tenant's declared BYOK backend (platform)](https://apidocs.ankatech.co/reference/bindplatformbyok.md): Advances DECLARED_PENDING_BIND -> BINDING -> ACTIVE. Required scope: admin.platform.key-backend.byok.bind.
- [Rotate a tenant's BYOK backend credential (platform)](https://apidocs.ankatech.co/reference/updateplatformbyokcredential.md): Reseals the Plane-2 credential without touching the backend descriptor or immutability. Required scope: admin.platform.key-backend.byok.declare, plus the ROOT platform tenant. A call that supplies a real credential cannot currently succeed: it answers 503. The one exception is a body whose primary secret is the write-only mask sentinel: that is the documented "leave the sealed credential unchanged" no-op, which runs no self-test, reseals nothing and answers 200. The reseal is gated on a self-test of the CANDIDATE credential, and the only per-tenant reachability probe this platform has is addressed by TENANT ID — it resolves the credential the tenant's row NAMES, which during a rotation is the credential being replaced. Round-tripping that one and answering PASSED would let an unverified credential commit over a working one, so the self-test refuses a rotation candidate outright instead of attesting the wrong credential. The refusal is the safe outcome; restoring a real rotation self-test needs a probe contract that can NAME the credential to authenticate with, which is an open decision with its own unsealing-oracle surface. Do not read the 503 as transient. It carries Retry-After because the status code does, and a retry will not clear it. While the backend is pre-bind (NOT_DECLARED or DECLARED_PENDING_BIND) the supported correction is to DELETE the declaration and re-declare it with the corrected credential. Once the backend is ACTIVE there is no self-service path and the condition must be escalated.
- [Get all deployment policies](https://apidocs.ankatech.co/reference/getalldeploymentpolicies.md): Retrieves all deployment-level policies in a single response. This includes algorithm availability, rotation, and lifecycle policies. Use this endpoint when you need to: • Display all platform defaults in a dashboard • Export deployment configuration • Clone policies to another deployment Empty fields indicate no policy is configured (system defaults apply).
- [Set all deployment policies](https://apidocs.ankatech.co/reference/setalldeploymentpolicies.md): Sets all deployment-level policies in a single atomic operation. This is useful for initial configuration or bulk updates. Any null fields will not be updated (existing policy retained). To remove a policy, explicitly set it to an empty configuration. Operation is atomic: all policies succeed or all fail.
- [Get deployment rotation policy](https://apidocs.ankatech.co/reference/getdeploymentrotationpolicy.md): Retrieves the rotation policy configured at the deployment level. This policy controls algorithm transitions during key rotation operations. Rotation policies enforce rules such as: • Operations subset: new key must support operations of original • No downgrade: prevent transitions to weaker algorithms • Classical to PQC: control quantum-safe transitions • Algorithm family: restrict changes within same family If no policy exists, system defaults allow all transitions.
- [Set deployment rotation policy](https://apidocs.ankatech.co/reference/setdeploymentrotationpolicy.md): Creates or updates the rotation policy at the deployment level. This operation is idempotent - it will create if not exists, or update if exists. Policy rules control algorithm transitions: • requireOpsSubset: New key must support subset of original operations • requireNoDowngrade: Prevent transitions to weaker algorithms • allowClassicalToPqc: Allow RSA/EC → ML-KEM/ML-DSA transitions • allowPqcToClassical: Allow reverse transitions (usually disabled) • allowDifferentFamily: Allow ML-KEM → ML-DSA (different families)
- [Get deployment algorithm policy](https://apidocs.ankatech.co/reference/getdeploymentalgorithmpolicy.md): Retrieves the algorithm availability policy configured at the deployment level. This policy controls which cryptographic algorithms are available system-wide. The policy acts as an allow-list that affects: • Algorithm catalogue (GET /algorithms) • Key generation and import operations • Key rotation requests If no policy is configured, the system behaves as "ALLOW ALL". Supported algorithm families: ML-KEM, HQC, FRODO, ML-DSA, SLH-DSA, FALCON, XMSS, LMS, EC, RSA, oct Categories: CLASSICAL, POST_QUANTUM NIST Security Levels: L0, L1, L3, L5 (no L2 or L4) Standards: NIST, ETSI, NSA, ISO, BSI, ANSSI, ENISA Operations: encrypt, decrypt, sign, verify
- [Set deployment algorithm policy](https://apidocs.ankatech.co/reference/setdeploymentalgorithmpolicy.md): Creates or updates the algorithm availability policy at the deployment level. This operation is idempotent - it will create if not exists, or update if exists. Policy evaluation follows this order: 1. explicitAlgorithms (if non-empty, immediate ALLOW) 2. Composite test: family → category → level → standards → operations Empty arrays ([]) mean no restriction for that dimension.
- [Reset deployment rotation policy to DEFAULT](https://apidocs.ankatech.co/reference/resetdeploymentrotationpolicy.md): Resets the deployment rotation policy to the DEFAULT template configuration. This operation ensures the deployment always has a valid policy in place by: 1. Deactivating the current custom policy (if any) 2. Creating a new policy based on the DEFAULT template 3. Applying secure transition rules from the template Unlike DELETE operations in tenant policies, deployment policies cannot be truly deleted as they serve as the fallback for all tenants. This reset operation guarantees system stability while returning to a known-good configuration. The DEFAULT template provides secure algorithm transitions: • requireOpsSubset: true (new key must support original operations) • requireNoDowngrade: true (prevents weaker algorithms) • allowClassicalToPqc: true (enables quantum-safe migration) • allowPqcToClassical: false (prevents security downgrade) • allowDifferentFamily: true (allows ML-KEM ↔ ML-DSA)
- [Reset deployment algorithm policy to DEFAULT](https://apidocs.ankatech.co/reference/resetdeploymentalgorithmpolicy.md): Resets the deployment algorithm policy to the DEFAULT template configuration. This operation ensures the deployment always has a valid policy in place by: 1. Deactivating the current custom policy (if any) 2. Creating a new policy based on the DEFAULT template 3. Applying the template's allow-all configuration Unlike DELETE operations in tenant policies, deployment policies cannot be truly deleted as they serve as the fallback for all tenants. This reset operation guarantees system stability while returning to a known-good configuration. The DEFAULT template allows all algorithms without restrictions.
- [Eager hydrate tenant algorithm policies](https://apidocs.ankatech.co/reference/hydratealltenantsalgorithmpolicy.md): Iterates every active tenant in the deployment and creates a new tenant algorithm policy row copied from the active deployment policy for tenants that don't already have one. Idempotent — rerunning has no effect on tenants that already have an active row. This is the architect Phase 1.5 deployment-safety net for the Slice 6 fail-closed cutover in core-api. Invoke BEFORE promoting core-api Slice 6. Scope: admin.policy.deployment.algorithm.write (platform-admin only).
- [Get deployment lock state](https://apidocs.ankatech.co/reference/getdeploymentlockstate.md): Returns whether the deployment is locked (any of the three deployment policy types is immutable) and, when locked, which template each policy type uses. Used by the Admin Console to conditionally render the policy configuration UI: LOCKED deployments hide tenant-side edit affordances and surface a read-only badge naming the compliance preset; UNLOCKED deployments render the full editor surface. The lock state is platform-global (NOT per-tenant) and is read from a single source of truth (the three deployment policy tables). Results are cached for up to 60 seconds; cache misses execute one read per deployment policy table. Required scope: `admin.policy.deployment.all.read`.
- [Revoke a single material version](https://apidocs.ankatech.co/reference/revokekeymaterialversion.md): Allowed only when material status is RETIRED or PENDING_PRIMARY. Revoking a PRIMARY material directly is blocked with 422 `material-status-invalid-transition` (FR-11); rotate or revoke the Stable KID first.
- [Mark material for destruction (start cooling-off)](https://apidocs.ankatech.co/reference/markkeymaterialversionfordestruction.md)
- [Destroy material (only after cooling-off elapsed)](https://apidocs.ankatech.co/reference/destroykeymaterialversion.md)
- [Cancel a pending destruction (revert to previous state)](https://apidocs.ankatech.co/reference/cancelkeymaterialversiondestruction.md)
- [List materials in the lineage (version descending)](https://apidocs.ankatech.co/reference/listkeymaterialversions.md)
- [Get a specific material version](https://apidocs.ankatech.co/reference/getkeymaterialversion.md)
- [Create tenant with administrator](https://apidocs.ankatech.co/reference/provisionplatformtenant.md): Creates a new tenant organization and its first administrator account in a single atomic operation. This ensures the tenant is immediately accessible after creation. The operation includes: - Tenant creation with unique name validation - Post-quantum cryptography keystore generation - Administrator account setup with secure password hashing - Automatic role assignment (ADMIN role) - Full audit trail creation **Authorization:** Requires `platform.bootstrap` scope (available to Super Admins and Core-API S2S)
- [List all tenants](https://apidocs.ankatech.co/reference/listtenants.md): Returns all non-deleted tenants across the entire platform. This is a Super Admin operation providing cross-tenant visibility.
- [Get tenant by id](https://apidocs.ankatech.co/reference/gettenantbyid.md): Returns full details of a single tenant identified by its UUID. Platform-level read endpoint that complements GET /platform/tenants (list) with a single-fetch path keyed by id. Returns 404 when the tenant does not exist or has been soft-deleted. **Authorization:** Requires `admin.tenant.list` scope (same as the list endpoint).
- [Delete tenant (soft-delete)](https://apidocs.ankatech.co/reference/deletetenant.md): Marks the tenant as CLOSED for audit purposes. This operation is restricted to Platform Administrators only. Tenant Administrators cannot delete their own organization.
- [Get tenant closure impact](https://apidocs.ankatech.co/reference/gettenantclosureimpact.md): Returns the measured blast radius of closing one tenant, in the three categories the closure cascade already uses: the assertions it removes, the subjects it disables, and the evidence it retains and renders permanently unusable. **Absent is not zero.** A figure that could not be measured is ABSENT from the body; a figure that measured zero is PRESENT carrying `0`. The two are distinguishable by shape alone — never by a status field beside a zero — because rendering them identically turns an availability failure into a confident falsehood about a tenant that is one irreversible click away from closure. **Coverage is per block.** Each of the four measurement blocks carries its own `status`, its own `measuredAt`, and — when it is not COMPLETE — a `reason` drawn from a closed two-member vocabulary that discloses nothing about the failure. One block degrading leaves its siblings untouched, and a resolved tenant always answers 200: the envelope read is the floor. **No snapshot is claimed.** The blocks are read sequentially, `generatedAt` is at or after every block's `measuredAt`, and the body carries no member asserting mutual consistency. **Authorization:** Requires `admin.tenant.delete` — the scope of the operation this read precedes.
- [Get per-tenant cryptographic statistics](https://apidocs.ankatech.co/reference/gettenantstatssummaries.md): Returns key counts, PQC adoption percentages, and application counts per active tenant in a single cross-tenant aggregation. Designed for the Platform Overview Control Plane panel.
- [Aggregate policy state for all tenants](https://apidocs.ankatech.co/reference/getpoliciessummary.md): Returns the effective policy state for every active tenant in the deployment in a single response. One JPQL aggregate query across the three tenant policy tables — no N+1 per-tenant fetches. Used by the Tenants Management Policies column. **Lock-state-aware shape:** when the deployment is LOCKED, the `allowsTenantOverride` field is omitted from each element (uniform deployment-level state, no per-tenant differential). When UNLOCKED, every element carries the per-tenant template assignment. **Authorization:** requires `admin.tenant.list` scope (same as `/platform/tenants` list).
- [List integration certification status](https://apidocs.ankatech.co/reference/listintegrationcertifications.md): Returns the evidence status of every integration mechanism in this release: what ANKATech has exercised against the real external system, how, and what stands in the way of the rest. CERTIFIED, reached by a MANUAL probe, and still IAM_GRANT-blocked from automation — three simultaneously true statements that a single ranking could not express. selectable is COMPOSED by the server from the level AND, for a key-protection mechanism, this deployment's grant of the EXPERIMENTAL mechanisms it offers today — so the same release answers differently on two deployments, and an operator's edit takes effect on the next call with no restart. That deployment half is carried only to a caller already holding key-protection authority; every other caller receives the level's own answer for those rows, which is identical on every deployment. A client filters on this flag either way and never compares level itself.EXPERIMENTAL mechanism whose connection test passes is a correct state.contentSha256 identifies the RELEASE; selectable is composed per DEPLOYMENT and is the one field two deployments reporting that same digest can disagree on — and the one field a caller without key-protection authority is answered from the release rather than from this deployment.PLATFORM_SUPER_ADMIN role to impersonate this tenant. While the authorisation is live — and ONLY while it is live — the standing SaaS refusal is lifted. reason is recorded for the tenant's own governance record. It is never shown on the platform-wide list and never carried in an audit event.
- [Read this tenant's live support-access authorisation](https://apidocs.ankatech.co/reference/getsupportaccessgrant.md): Returns the LIVE authorisation, or 204 No Content when there is none or the window has lapsed. Liveness is computed against the clock at read time — there is no status column, so a lapsed authorisation can never read as a live one because a sweeper has not run yet. sessionActive answers the question the authorisation itself cannot: whether a support operator is inside the window RIGHT NOW, as opposed to merely being permitted to be. The two are different facts to a tenant administrator and only the second warrants an interruption. reason is ABSENT when the reader is impersonating. The text is the tenant's own, written for the tenant's own record. An impersonation token authorises as the tenant administrator and therefore carries admin.tenant.support-access.read, so without this the operator would read the tenant's private justification for admitting them. The decision is fail-closed: a claim that cannot be read withholds. Every stored grant has a reason (it is mandatory at declaration), so an absent field means it was not disclosed and never that there is none.
- [Withdraw this tenant's support-access authorisation](https://apidocs.ankatech.co/reference/deletesupportaccessgrant.md): Withdraws the authorisation and terminates every session still open under it — each token blacklisted, each row stamped, each termination signed into the tenant's audit trail, and the tenant notified. expired, not as revoked — that is what happened to it, and the tenant's remedy differs.
- [End the impersonation session the caller is currently inside](https://apidocs.ankatech.co/reference/endimpersonationsession.md): Ends the support session the CALLER is running under — the operator closing their own session when the work is done, without waiting for the token to expire or the tenant to withdraw the authorisation. act.sub, on the tenant in the path. There is no session id and no JTI anywhere in the request: accepting either would let a caller holding one impersonation token terminate a different operator's session by guessing an identifier, and a JTI in a request is also a JTI in a log. entitled: false is still provisioned by a ROOT operator through this plane, and the write succeeds. The verdict predicts what the TENANT self-service plane would do, and it tells the operator whether the customer could also manage this themselves — which changes who they tell, not what they may do. resolution is required reading beside the verdict: LICENSED means the licence answered, FLOORED means it could not be read and the reply is the fail-closed floor. "Their edition does not include this" and "we could not confirm their edition" send an operator to two different people, and a bare entitled: false sends half of them to the wrong one. entitled: true — the operator runs the platform and owns the workloads that authenticate against it, so there is nothing for an ANKA-issued Edition to entitle. On an ANKA-operated deployment it is true only for the Enterprise edition. 500: a licence outage is reported as FLOORED, not raised. And it never becomes an authentication outage — verification never consults this verdict, so a tenant WITHOUT the entitlement keeps every workload that already authenticates through a declared issuer. ../workload-identity/entitlement (SR-10.6).
- [Read this tenant's key-protection precedence chain](https://apidocs.ankatech.co/reference/gettenantkeybackendprecedence.md): Answers which backend protects my keys, and why in one read, so a tenant administrator does not have to infer the chain from a status screen that can only say "mine" or "inherited". The platform decides the backend as a three-rung precedence: a backend the tenant brought itself wins; otherwise a per-tenant licensed-Edition override routes the tenant to that tier's ANKA-managed backend; otherwise the tenant rides the deployment instance. effective names the rung that won and the backend token it resolves to. rungs carries all three, highest first, exactly one of them WINNER — and each of the other two carries a machine-readable reason from a closed vocabulary, so a client can distinguish I have no Edition override from my Edition override is being overruled by my own declaration. Those lead to different actions. What this plane is told depends on who operates the deployment, not on who is asking. On an ANKA-operated (SaaS) install every rung except TENANT_BYOK — the WINNER included — carries its outcome and reason and no backendType: that token names the DEPLOYMENT's key-custody configuration, which is the same thing the admissibility refusal beside it withholds byte-identically so that a caller cannot probe it. It is the RUNG that decides, not whether the rung won — winning does not make the deployment's token the tenant's. TENANT_BYOK keeps its token on every posture, and it is the only rung that does, because that token is the tenant's own declaration submitted through this API. On a customer-operated install (PRIVATE_CLOUD, ON_PREMISE) every rung carries its token — the operator is the customer's own staff, and withholding would hide the customer's configuration from the customer. So effective.backendType is OPTIONAL and MUST be modelled as nullable. A tenant with no declaration of its own and no Edition override wins on DEPLOYMENT_INHERITED, so on a SaaS deployment the response WITHOUT effective.backendType is the ordinary one, not an edge case. The same holds for every rungs[].backendType: an outcome of WINNER does not imply a token is present. effective.keyBackendSource is always present, so the tenant is always told WHICH rung decides its custody. changeRefusals — which rung transitions the platform would refuse right now — is not part of this plane's body. Those verdicts are read from an outbound key-material probe, and a tenant-reachable read does not drive one. The read is scoped to the caller's OWN tenant: the path tenant must equal the tenant in the token. It writes nothing — no audit row, no event, no state change. Required scope: admin.tenant.key-backend.byok.read.
- [Get the tenant's own BYOK key-material backend status](https://apidocs.ankatech.co/reference/listbyok.md): Required scope: admin.tenant.key-backend.byok.read. This read NEVER answers 404: a tenant that has declared nothing is reported as state NOT_DECLARED with source INHERITED, which is a valid state and not a missing resource.
- [Declare the tenant's own BYOK key-material backend](https://apidocs.ankatech.co/reference/replacebyok.md): Required scope: admin.tenant.key-backend.byok.declare.
- [Revert the tenant's pre-bind BYOK declaration](https://apidocs.ankatech.co/reference/deletebyok.md): Deletes the sealed credential and returns the backend to NOT_DECLARED. PRE-BIND only — see the 409. Idempotent, and it NEVER answers 404: reverting a tenant that has declared nothing is a no-op that returns the NOT_DECLARED status view. Required scope: admin.tenant.key-backend.byok.declare.
- [Check the tenant's own BYOK backend coordinates before declaring them](https://apidocs.ankatech.co/reference/validatetenantbyokbackend.md): Checks coordinates this tenant is considering for its own (BYOK) key-material backend, and answers what was checked. Required scope: admin.tenant.key-backend.byok.declare, and the path tenant must be the caller's own. FAILED/UNREACHABLE. A client that renders that as a transport error loses the distinction that makes the operation useful.NOT_APPLICABLE. Nothing about authorization, entitlement to the named key, or key material is known until the backend is declared and bound.SHAPE and ENDPOINT_POLICY always run. KEY_ID_GRAMMAR_PARSED runs when a keyId was supplied AND the declared family publishes a grammar for it - aws-kms, gcp-kms and gcp-kms-hsm do; Azure key names and PKCS#11 labels have none, and for those the entry is ABSENT rather than reported as vacuously passing. TRANSPORT_PROBE runs only when an endpoint is supplied - so for the four PKCS#11 tokens and the two GCP tokens, which name no operator-specific host, nothing is contacted and the verdict carries no TRANSPORT_PROBE entry at all. That is what stops a green answer reading as "everything is fine".422, not a verdict, and neither is one omitted where the backend requires it. The rule is the declared per-backend requirement the DECLARE verb enforces, read here by the same single reader - so a body this check admits is a body the declare admits, and a rehearsal cannot come back green for a submission the commit refuses.ENDPOINT_ANSWERED, because it was never asked anything to decline.Retry-After and no verdict member at all, because a bounded-out request contacted nothing.admin.tenant.key-backend.byok.test. A failed test is an HTTP 200. The question this operation asks is whether the backend works; "it does not" is an answer to that question, so it arrives as a 200 carrying a FAILED verdict. A non-2xx means something different and incompatible: the test could not be run at all. A client that renders both as "error" loses the distinction that makes this operation useful, because the two call for opposite operator actions. The verdict's reason comes from a closed vocabulary (SelfTestReason) that can carry no credential, ARN, resource path, vault URL, region or account id, and no vendor exception text. A client MUST tolerate a reason it does not recognise and render it as an unknown verdict — never as a pass. It changes nothing: no state advances, no key material is written, no KEK is provisioned on either the delegated BYOK path or the in-process managed-tier path, and the backend remains changeable afterwards. It is therefore admissible in EVERY backend state, including CHANGING — which is the state in which an operator most needs it — and it never answers 409. Not every status below is reachable for every tenant. A tenant on an ANKA-managed tier is tested IN THIS SERVICE's own process rather than delegated, so the 502 and 504 — which describe a delegation — cannot occur for it. They apply to a tenant that brought its own backend, and to a tenant that inherits the deployment's.
- [Bind the tenant's declared BYOK backend](https://apidocs.ankatech.co/reference/bindbyok.md): Advances DECLARED_PENDING_BIND -> BINDING -> ACTIVE. Required scope: admin.tenant.key-backend.byok.bind.
- [Rotate the tenant's BYOK backend credential](https://apidocs.ankatech.co/reference/updatebyokcredential.md): Reseals the Plane-2 credential without touching the backend descriptor or immutability. Required scope: admin.tenant.key-backend.byok.declare. A call that supplies a real credential cannot currently succeed: it answers 503. The one exception is a body whose primary secret is the write-only mask sentinel: that is the documented "leave the sealed credential unchanged" no-op, which runs no self-test, reseals nothing and answers 200. The reseal is gated on a self-test of the CANDIDATE credential, and the only per-tenant reachability probe this platform has is addressed by TENANT ID — it resolves the credential the tenant's row NAMES, which during a rotation is the credential being replaced. Round-tripping that one and answering PASSED would let an unverified credential commit over a working one, so the self-test refuses a rotation candidate outright instead of attesting the wrong credential. The refusal is the safe outcome; restoring a real rotation self-test needs a probe contract that can NAME the credential to authenticate with, which is an open decision with its own unsealing-oracle surface. Do not read the 503 as transient. It carries Retry-After because the status code does, and a retry will not clear it. While the backend is pre-bind (NOT_DECLARED or DECLARED_PENDING_BIND) the supported correction is to DELETE the declaration and re-declare it with the corrected credential. Once the backend is ACTIVE there is no self-service path and the condition must be escalated.
- [Get role by name (system or tenant custom)](https://apidocs.ankatech.co/reference/getrole.md)
- [Update a tenant custom role (REPLACE-semantics on scopes)](https://apidocs.ankatech.co/reference/updaterole.md)
- [Delete a tenant custom role (soft delete)](https://apidocs.ankatech.co/reference/deleterole.md)
- [List system + tenant custom roles](https://apidocs.ankatech.co/reference/listtenantroles.md)
- [Create a tenant custom role](https://apidocs.ankatech.co/reference/createrole.md)
- [Get one of a tenant's delivery providers (platform)](https://apidocs.ankatech.co/reference/getplatformtenantnotificationprovider.md): One provider row of the tenant's own chain, with its configuration and its most recent connectivity verdict. The credential is never returned.
- [Replace a tenant provider's configuration (platform)](https://apidocs.ankatech.co/reference/replaceplatformtenantnotificationproviderconfig.md): Replaces the configuration and CLEARS the connectivity verdict, because a verdict measured against the previous configuration says nothing about this one. An edit that happens to produce identical configuration keeps its verdict — the same rule stated precisely, not a second rule. chainPosition in the body is applied with insertion + renumber semantics: the provider moves to that index of this scope's chain for its channel and the others keep their relative order, renumbered contiguously from 0 in one transaction. An ABSENT chainPosition leaves the position unchanged — it is never defaulted to 0, which would silently promote an edited provider to primary.
- [Seal a tenant provider's credential (platform)](https://apidocs.ankatech.co/reference/replaceplatformtenantnotificationprovidercredential.md): Seals the secret this provider authenticates with, and returns the provider to UNVERIFIED. enable will accept it. 409: a refusal that reported "nothing to destroy" would disclose whether material is present to a caller who holds the write scope but not the read scope. DECLARED and is inert: it delivers nothing until a connectivity test passes and it is enabled. That is the same rule the tenant plane applies, because it is the same code — an unverified provider never sends. chainPosition is numeric, 0..7, and defaults to 0. Position 0 is the primary; the rest are the failover sequence in order. /api/v3/admin/platform/notification/providers.
- [Run a connectivity test against a tenant's provider (platform)](https://apidocs.ankatech.co/reference/testplatformtenantnotificationprovider.md): Reaches OUT, which is why it carries its own scope: in PROBE mode it connects, negotiates TLS and authenticates without delivering anything; in SEND mode it delivers exactly one message to the recipient the caller names. .write rather than .test, matching both shipped planes: .test exists because a test reaches OUT, and this reaches nothing. deliverable is therefore the emptiness of the ACTIVE delivery chain and nothing else; every other member exists to EXPLAIN a false and none of them may change it. GET /api/v3/admin/tenants/{tenantId}/notification/readiness) answers 403 to a SaaS platform operator, which is the whole reason this one exists — the same rule that put the tenant's provider list on this plane (PRD §166). effectiveScope reports which of the two answered — and it reads DEPLOYMENT whenever the fallback was taken, including when that fallback is itself empty. EMAIL unconditionally — account activation and password reset are email-borne — plus every other chain-borne channel for which a provider row exists at any status, in either scope. EMAIL, SMS and WHATSAPP — the three channels whose delivery resolves the provider chain this verdict is computed from. WEBHOOK is a fourth NotificationChannel value and is reported in NO state, even when a WEBHOOK provider row is declared: a webhook resolves an alert DESTINATION, whose endpoint and signing secret are per-destination and are not in this chain, so a verdict derived here would be an answer to a question webhook delivery never asks.
The vocabulary's fourth token, NO_PROVIDER_DECLARED — no row for this channel. Declare one.PROVIDER_UNVERIFIED — rows exist and none carries a PASSED verdict measured against its CURRENT configuration: never probed, last probe FAILED, or probed and then edited. Run the connectivity test.PROVIDER_NOT_ACTIVATED — a current PASSED verdict exists and no row is ACTIVE. Enable the provider.NOT_REMEDIABLE_HERE, is never emitted on this endpoint, in any state. It means contact the platform operator, and the caller here IS the platform operator, for whom that is a loop and not a remedy. The cause is therefore the specific derived token whether the rows that explain it are the tenant's or the deployment's. blockingProviderId is gated separately from the token, and on a different rule: it is present only when the blocking row lies in the scope this read is about — so only when the tenant holds rows of its own. When the tenant declared nothing and the block is on a DEPLOYMENT row, the token still rides and the identifier is omitted: admin.platform.notification.tenant.read grants THAT TENANT's provider list, not the deployment's, so naming a deployment row would hand out an identifier the scope does not otherwise carry. The deployment plane's own verdict, identifiers included, is at GET /api/v3/admin/platform/notification/readiness, under the scope that does grant it. null. A deliverable channel carries no blocking key at all, rather than a token meaning "nothing is wrong" — a rendered all-clear cannot be distinguished from a read that never happened. notice member is orthogonal to blocking: it has its own condition, never suppresses and is never suppressed by a blocking token, and both may ride one channel entry. INHERITING_DEPLOYMENT_CHAIN states that this tenant declared rows of its own for the channel and is nonetheless riding the deployment chain, because none of its own rows is currently ACTIVE. A tenant that declared nothing is inheriting by design and receives no notice. effectiveScope = DEPLOYMENT. PUT /api/v3/admin/platform/tenants/{tenantId}/key-backend/edition), which is where a licensing change belongs. resolution is required reading beside the verdict: LICENSED means the licence answered, FLOORED means it could not be read and the reply is the fail-closed floor. "Their edition does not include this" and "we could not confirm their edition" are two different conversations.
- [Returns the calling JWT's expanded scope set (resolver output)](https://apidocs.ankatech.co/reference/geteffectivescopes.md)
- [List the workload-identity-federation bindings of every tenant (platform)](https://apidocs.ankatech.co/reference/listplatformworkloadidentitytenantbindings.md): Answers which tenants trust an external identity provider of their own, and which in ONE read, over the WHOLE tenant population. This surface is a TRUST UNION, and two consequences follow that the replace-style surfaces do not have. A tenant's issuers are trusted BESIDE the deployment's, never in place of them — trust is additive by its nature, and a token minted by the deployment issuer keeps verifying after a tenant declares one of its own — so deploymentShape is still present when tenants have diverged. And deploymentStatus and every bindings[*].status are INDEPENDENT verdicts returned in the same body: a deployment issuer verified and live while a tenant's sits unverified is the ordinary case, not a contradiction. surfaceStatus is the server-composed worst-of across both planes (FR-190.21) and replaces neither of them. The response is a CLASS-level projection, on both planes. It carries the provider a tenant trusts — AUTH0 | ENTRA | OKTA | KEYCLOAK | COGNITO | GENERIC_OIDC — and a server-derived label for it, and nothing finer. It deliberately omits: the issuer url and the jwks endpoint derived from it, the permitted algorithms, the declared maximum token lifetime, the audience, the actor bindings, the last reachability probe outcome, and the operator-authored issuer display name — that column is the one a naive pass-through would leak into label, and every label here comes from a server-side table keyed on the class alone. The same rule binds the DEPLOYMENT issuer. Never entitlement-gated. This read REPORTS eligibility; it never applies it. Gating it on the workload-identity entitlement would make the notEligibleTenants band unreadable on exactly the deployment where it is not empty, and a platform operator who cannot see which tenants are Edition-blocked cannot act on the upgrade path the topology screen exists to make legible (FR-190.27). Its own admission is the scope conjunction plus the ROOT tenant boundary, and nothing else. Eligibility comes from this surface's own entitlement plane, and needs no extra scope. notEligibleCount and the tenants it names derive from the workload-identity entitlement — customer-operated deployment, or an ENTERPRISE effective Edition. That plane is gated by the same admin.platform.workload-identity.read this endpoint already requires, which is why this surface carries two conjuncts where observability-export carries three. A tenant that could not have diverged is NAMED as not eligible rather than counted as agreeing; folding it into onDefaultCount would report conformity where there was no choice. The four coverage bands partition the whole population, so onDefaultCount + divergingCount + notEligibleCount + suspendedCount == tenantCount always holds; a tenant holding a declared issuer is reported as DIVERGING even if its entitlement has since been revoked, because a downgrade never strands a configured tenant. A suspended tenant stays INSIDE the denominator, and this read never answers 404 for a tenant that has declared nothing. maxTenantBindings is null — this surface states no per-tenant ceiling. chain is [] — this is not an ordered-chain surface — while deploymentChain carries one position per DISTINCT class the deployment plane holds, as an UNORDERED set: it is [] only when the plane holds exactly one class (which deploymentShape names) or holds nothing. Both keys are PRESENT, which is what makes the six surfaces one envelope. Takes no path and no query parameter, so no caller-supplied identifier enters a query. It writes nothing: no audit row, no event, no state change. Required: the ROOT platform tenant, holding admin.platform.workload-identity.read and admin.tenant.list.
- [List the timestamping bindings of every tenant (platform)](https://apidocs.ankatech.co/reference/listplatformtimestampingtenantbindings.md): Answers which tenants have moved off the deployment RFC 3161 default, and onto what in ONE read, over the WHOLE tenant population. It never answers 404, and that is the point. The per-tenant override read uses 404 to mean this tenant inherits — defensible when you asked about one tenant, and wrong for a fleet, because an operator asking who has diverged would get an error for every tenant that has not. Here an inheriting tenant is counted in onDefaultCount and is absent from bindings; the only statuses this path can return are 200, 401, 403 and 500. The response is a CLASS-level projection, on both planes. This surface has no closed vendor vocabulary — a connection is an endpoint URL, a policy OID, a hash algorithm and a timeout — so the class is the one fact that is one: whether the plane timestamps. RFC3161_TSA names a plane pointed at an authority and TIMESTAMPING_DISABLED a plane that has switched timestamping off, which is a divergence in its own right and is never folded into the inheriting band. It deliberately omits: the TSA endpoint URL, the policy OID, the hash algorithm, the timeout, the client credential and its mask, and the per-tenant KEK reference. The same rule binds the DEPLOYMENT default — deploymentShape and deploymentLabel name its class, never a configured value, and on this surface that is not a formality: the deployment TSA URL is not under this endpoint's scope at all. The four coverage bands partition the whole population, so onDefaultCount + divergingCount + notEligibleCount + suspendedCount == tenantCount always holds. This surface has no entitlement plane, so notEligibleCount is 0 and notEligibleTenants is [] — present, never omitted. A suspended tenant stays INSIDE the denominator. deploymentStatus is the deployment's timestamping CAPABILITY, not the settings flag alone. TSA_ENABLED=false DOMINATES and is NOT_CONFIGURED whatever the trust-anchor count — a deliberate operator decision is not a fault. Only once the plane claims to timestamp does the anchor count decide: at least one ACTIVE RFC 3161 trust anchor is ACTIVE, and none is ATTENTION — configured but not operational, which is what ATTENTION means everywhere on this platform. The anchor fact is deployment-global: it never reaches a bindings[*].status. surfaceStatus is the server-composed worst-of across both planes (FR-190.21); deploymentStatus and every bindings[*].status are returned unchanged beside it. chain is [] — this is not an ordered-chain surface — and deploymentChain is [] because this plane is SINGLE_VALUED: it holds at most one class, and deploymentShape names it. The two keys are empty for DIFFERENT reasons, and after PRD §202 they must be: a surface that is not ordered-chain may still populate deploymentChain — the three multi-class surfaces do. It is the CARDINALITY, not the ordering, that empties it here. maxTenantBindings is null because it states no per-tenant ceiling. Both keys are PRESENT: a band with nothing in it emits 0, [] or null and never omits its key, which is what makes the six surfaces one envelope. Takes no path and no query parameter, so no caller-supplied identifier enters a query. It writes nothing: no audit row, no event, no state change. Required: the ROOT platform tenant, holding admin.platform.settings.timestamping.override.read, admin.tenant.list and admin.platform.tsa-trust-anchor.manage. No conjunct is decoration. The second is what makes enumerating every tenant and NAMING it not a widening, which the surface scope alone does not grant. The third is what keeps this response a strict subset of what its caller may already read directly: deploymentStatus discloses whether the deployment holds an ACTIVE RFC 3161 trust anchor, and that fact is otherwise obtainable only through GET /api/v3/admin/platform/tsa/trust-anchors, which is gated on it.
- [List the observability-export bindings of every tenant (platform)](https://apidocs.ankatech.co/reference/listplatformobservabilitytenantbindings.md): Answers which tenants stream their own telemetry out of this deployment, and to whom in ONE read, over the WHOLE tenant population. This surface is ADDITIVE, and two consequences follow that the replace-style surfaces do not have. A tenant's destinations are added BESIDE the deployment collector, never in place of it, so deploymentShape is still present when tenants have diverged. And a tenant may declare up to five destinations, so the SAME tenant may be named under more than one bindings element — counted once as a diverging tenant, named once per vendor. maxTenantBindings states that ceiling; this endpoint configures nothing, so a tenant already at the ceiling is simply reported and no 4xx is produced for it here. The response is a CLASS-level projection, on both planes. It carries the vendor a tenant exports to — DATADOG | DYNATRACE | INSTANA | GENERIC_OTLP — and a server-derived label for it, and nothing finer. It deliberately omits: the OTLP endpoint, the signal set, the export key and its mask, the credential reference, and the operator-authored destination name — that column is the one a naive pass-through would leak into label, and every label here comes from a server-side table keyed on the class alone. The same rule binds the DEPLOYMENT collector. Eligibility comes from a different plane, which is why this endpoint needs a third scope. notEligibleCount and the tenants it names derive from the observability-export entitlement — the standing plan-gate flag, gated by admin.platform.observability-backend.tenant.entitlement.manage, not by the backends scope. A tenant that could not have diverged is NAMED as not eligible rather than counted as agreeing; folding it into onDefaultCount would report conformity where there was no choice. Per-tenant export is SaaS-only, so on a customer-operated deployment no tenant is entitled and the band covers the fleet. The four coverage bands partition the whole population, so onDefaultCount + divergingCount + notEligibleCount + suspendedCount == tenantCount always holds; a tenant holding a live destination is reported as DIVERGING even if its entitlement has since been revoked, because a downgrade never strands a configured tenant and hiding a live export behind a commercial label would be a worse answer. A suspended tenant stays INSIDE the denominator, and this read never answers 404 for a tenant that has declared nothing. surfaceStatus is the server-composed worst-of across both planes (FR-190.21); deploymentStatus and every bindings[*].status are returned unchanged beside it. chain is [] — this is not an ordered-chain surface — while deploymentChain carries one position per DISTINCT class the deployment plane holds, as an UNORDERED set: it is [] only when the plane holds exactly one class (which deploymentShape names) or holds nothing. Both keys are PRESENT, which is what makes the six surfaces one envelope. Takes no path and no query parameter, so no caller-supplied identifier enters a query. It writes nothing: no audit row, no event, no state change. Required: the ROOT platform tenant, holding admin.platform.observability-backend.tenant.manage and admin.tenant.list and admin.platform.observability-backend.tenant.entitlement.manage.
- [List the mail-delivery chains of every tenant (platform)](https://apidocs.ankatech.co/reference/listplatformnotificationtenantbindings.md): Answers which tenants send mail through something other than the deployment chain, and through what, in what order in ONE read, over the WHOLE tenant population. This is the ordered-chain surface, so the binding class IS the chain. bindings[*].shape is the ordered signature — SENDGRID>SMTP — and bindings[*].chain carries the positions in order, mirroring the top-level deploymentChain. A tenant on SENDGRID → SMTP and a tenant on SMTP → SENDGRID are therefore two DIFFERENT elements: grouped by provider they would be indistinguishable, and on this surface the order is the configuration. The absence of a backup is a chain of length 1 — read from the chain itself, so no per-tenant flag exists and none is needed. The top-level deploymentShape is the SIGNATURE over deploymentChain, not one of its positions — SENDGRID>SMTP for a chain of SendGrid then SMTP. That is why a populated scalar sits beside a non-empty chain here by design: the signature names the chain as a whole and the positions name its members in order, so the two carry different facts rather than one fact stated twice. deploymentLabel is the signature's label in the same way. A chain of length 1 is the case that hides the distinction, because the signature and the single class token are then textually identical — read compositionRule on this same envelope to tell the regimes apart, never the value. Under every other composition rule the scalar names the plane's sole class instead, and deploymentChain is an unordered set. The response is a CLASS-level projection, on both planes. It carries the mechanism at each position — SENDGRID | SMTP | MS_GRAPH | GMAIL — and a server-derived label for it, and nothing finer. It deliberately omits: the relay hostname and port, the sender address, the sender display name, the SMTP username, the TLS mode, the API key and its mask, the credential-custody key, the Entra directory id, and the probe verdict text. The same rule binds the DEPLOYMENT chain: each position names its class, never a configured value. The four coverage bands partition the whole population, so onDefaultCount + divergingCount + notEligibleCount + suspendedCount == tenantCount always holds. A tenant that inherits the deployment chain is counted in onDefaultCount and is absent from bindings; it is never reported as unknown, and this read never answers 404 for it. This surface has no entitlement plane, so notEligibleCount is 0 and notEligibleTenants is [] — present, never omitted. A suspended tenant stays INSIDE the denominator. A position that is declared-but-untested or disabled is reported ATTENTION rather than dropped: it is part of the operator's failover intent, and hiding it would report a two-position chain as a single point of failure. One unhappy position makes its whole chain ATTENTION. surfaceStatus is the server-composed worst-of across both planes (FR-190.21); deploymentStatus and every bindings[*].status are returned unchanged beside it. maxTenantBindings is null because this surface states no per-tenant ceiling — the key is PRESENT, as it is on all six. Takes no path and no query parameter, so no caller-supplied identifier enters a query. It writes nothing: no audit row, no event, no state change. Required: the ROOT platform tenant, holding admin.platform.notification.tenant.read and admin.tenant.list. The second conjunct is not decoration — this response enumerates every tenant and names it, which the surface scope alone does not grant.
- [List the key-custody bindings of every tenant (platform)](https://apidocs.ankatech.co/reference/listplatformbyoktenantbindings.md): Answers which tenants have moved their key material off the deployment default, and onto what in ONE read, over the WHOLE tenant population — the question the per-tenant surfaces cannot answer, because each of them takes a tenant in the path and an operator would have to open one screen per tenant and join the results by hand. The response is a CLASS-level projection, on both planes. It carries the backend FAMILY a tenant declared — the PKCS11 | AWS_KMS | GCP_KMS | AZURE_KV provider scalar — and a server-derived label for it, and nothing finer. It deliberately omits: the endpoint, host, region, vault or partition name, key id or alias, credential reference, credential mask, operator-authored display name, and the PKCS#11 vendor — softhsm, nshield, luna and cloudhsm are declarable tokens one axis BELOW the class published here, so two tenants on different HSM appliances are one indistinguishable PKCS11 element. The same rule binds the DEPLOYMENT default: deploymentShape and deploymentLabel name its class, never a configured value. The four coverage bands partition the whole population, so onDefaultCount + divergingCount + notEligibleCount + suspendedCount == tenantCount always holds. A tenant that simply inherits is counted in onDefaultCount and is absent from bindings; it is never reported as unknown, and this read never answers 404 for it. notEligibleTenants names the tenants whose Edition excludes the capability rather than folding them into the inheriting band, and a suspended tenant stays INSIDE the denominator. deploymentStatus is declared AND operational, not merely declared. NOT_CONFIGURED means nothing is declared — and then deploymentShape is null, because both members are read from the one server-side readiness projection GET /platform/setup/status renders and cannot disagree. ACTIVE means declared, bound, provisioned and round-trip verified. Everything between those two is ATTENTION: a declared backend that is mid-bind, unreachable, or whose last self-test failed is configured but not operational, which is what ATTENTION means everywhere on this platform. surfaceStatus is the server-composed worst-of across both planes (FR-190.21); deploymentStatus and every bindings[*].status are returned unchanged beside it, so a client never recomposes a second opinion. chain is [] here — this surface is not an ordered-chain surface — and maxTenantBindings is null because it states no per-tenant ceiling. Both keys are PRESENT: a band with nothing in it emits 0, [] or null and never omits its key, which is what makes the six surfaces one envelope. Takes no path and no query parameter, so no caller-supplied identifier enters a query. It writes nothing: no audit row, no event, no state change. Required: the ROOT platform tenant, holding admin.platform.key-backend.byok.read and admin.tenant.list. The second conjunct is not decoration — this response enumerates every tenant and names it, which the surface scope alone does not grant.
- [List the security-event-forwarding bindings of every tenant (platform)](https://apidocs.ankatech.co/reference/listplatformeventforwardingtenantbindings.md): Answers which tenants send their security events somewhere other than where this deployment sends them in ONE read, over the WHOLE tenant population. On this surface a tenant can depart in TWO ways, and an exclusion is one of them. It may ADD a sink of its own, and it may OPT OUT of an inherited deployment destination. Both are divergences and each carries its own binding class — an opt-out under EXCLUDED_<sink>. Reporting an opt-out as conformity would be the quietest possible misreport: the tenant that switched off every inherited destination receives nothing, and it is precisely the tenant an operator asking who has departed most needs to see. A tenant that did both is named under both classes and counted ONCE as a diverging tenant. An exclusion class reports ACTIVE: the opt-out is in force and there is no external system whose health it could report. The deployment plane is NOT replaced — an exclusion removes one destination for ONE tenant — so deploymentShape is still present when tenants have diverged. The response is a CLASS-level projection, on both planes. It carries the sink a tenant forwards to — SYSLOG | SPLUNK_HEC | SENTINEL | WEBHOOK — or the sink it excluded, and a server-derived label, and nothing finer. It deliberately omits: the transport configuration and everything inside it (the splunk hec endpoint, the sentinel dcr id, the webhook url, the syslog host and port), the credential reference and its mask, the formatter, the minimum severity, the event-type and key-id filters, and the operator-authored destination name — that column is the one a naive pass-through would leak into label, and every label here comes from a server-side table keyed on the class alone. The same rule binds the DEPLOYMENT destinations. This surface has no entitlement plane, so notEligibleCount is 0 and notEligibleTenants is empty: forwarding is not Edition-gated and no per-tenant entitlement controller exists for it. The four coverage bands still partition the whole population, so onDefaultCount + divergingCount + notEligibleCount + suspendedCount == tenantCount always holds. A suspended tenant stays INSIDE the denominator, and this read never answers 404 for a tenant that has declared nothing. surfaceStatus is the server-composed worst-of across both planes (FR-190.21); deploymentStatus and every bindings[*].status are returned unchanged beside it. maxTenantBindings is null — this surface states no per-tenant ceiling. chain is [] — this is not an ordered-chain surface — while deploymentChain carries one position per DISTINCT class the deployment plane holds, as an UNORDERED set: it is [] only when the plane holds exactly one class (which deploymentShape names) or holds nothing. Both keys are PRESENT, which is what makes the six surfaces one envelope. Takes no path and no query parameter, so no caller-supplied identifier enters a query. It writes nothing: no audit row, no event, no state change. Required: the ROOT platform tenant, holding admin.platform.event-forwarding.manage and admin.tenant.list.
- [Select the key-protection backend type for a managed tier (platform)](https://apidocs.ankatech.co/reference/replacemanagedbackendtier.md): Sets which of the nine declarable key-protection backends a licensed tier uses. The tier transitions through BINDING and lands in DECLARED_PENDING_BIND — selecting a type declares an intent, it does not bind a backend, so the tier does NOT become ACTIVE here. A mechanism this deployment does not OFFER right now is refused 422 before anything else is evaluated (see the 422 below), and the tier listing reports that same verdict per token as offered, from the same composition, so a picker cannot present an option this operation refuses. When the selected backend provides LOWER custody assurance than the tier's name implies (for example ENTERPRISE on softhsm), the change is refused 422 unless assuranceDowngradeConfirmed is set; a same-or-higher-assurance selection needs no confirmation. A selection that would MOVE the tier's resolved key-encryption key is refused 409 while any tenant on that tier holds key material; a selection that provably does not move it — re-selecting the type the tier already holds — is permitted even then. blastRadiusAcknowledged records that the operator was shown, and accepted, how many tenants the change actually moves — that is assignedTenantCount, not the edition's total; it is recorded on the audit row and is not itself a gate, so omitting it is recorded as false and refuses nothing. Every accepted change emits a signed admin audit event carrying who, when, the tier, the from-to transition and both acknowledgements. This request carries no credential and no PIN. Required scope: admin.platform.key-backend.tier.manage.
- [Check key-protection backend coordinates before selecting them for a tier](https://apidocs.ankatech.co/reference/validateplatformkeybackendtier.md): Checks coordinates an operator is considering for one licensed tier and answers what was checked. Required scope: admin.platform.key-backend.tier.manage, plus the ROOT platform tenant, on a SaaS deployment. FAILED/UNREACHABLE. A client that renders that as a transport error loses the distinction that makes the operation useful.NOT_APPLICABLE. Nothing about authorization, entitlement or key material is known until the tier is bound.SHAPE and ENDPOINT_POLICY always run. TRANSPORT_PROBE runs only when an endpoint is supplied - so for the four PKCS#11 tokens and the two GCP tokens, which name no operator-specific host, nothing is contacted and the verdict says so with a two-entry checks list. That is what stops a green answer reading as "everything is fine".422 and not a verdict. It is a statement about the two values submitted and about nothing else: nothing was contacted.ENDPOINT_ANSWERED, because it was never asked anything to decline.Retry-After and no verdict member at all, because a bounded-out request contacted nothing.tenantSelectable reports whether the DEPLOYMENT mode is OPTIONAL, i.e. whether this tenant is the one who decides. It is carried on both views because the effective mode collapses OPTIONAL away by design: a tenant that MAY choose and has not chosen yet reads DISABLED / DEPLOYMENT_LOCKED, which is byte-identical to a tenant whose deployment forbids stamping outright. Without this flag the two are indistinguishable from the tenant plane, and the per-tenant TSA surface stays hidden in exactly the case it exists for. The deployment endpoint that carries the raw mode is platform-plane and answers 403 to a tenant caller, by plane rather than by scope, so it is not an alternative.topUsers[].username names the ACCOUNTABLE reader, and that changed the meaning of this body without changing its shape. The ranking is served from the pre-aggregated USER dimension, which is keyed by the operating human: a read performed inside an impersonation session is attributed to the operator who performed it, not to the account it ran as. Two operators impersonating one account are therefore two entries here, where they were previously one entry naming a human who performed neither read.username because that is what it is - a login identity - and no field was added or removed. A client that read this ranking as "which of my tenant's accounts were used" must read it as "which humans read this tenant's trail", which is the question it was always meant to answer.readerTenantId is this tenant: the reads this tenant's own principals performed, INCLUDING a platform operator's read performed inside an impersonation session of one of those principals. A platform operator who reads this tenant's trail WITHOUT impersonating carries their own tenant on the reader axis and this tenant only on subjectTenantId, so that read is not in this result. An empty result here is therefore NOT evidence that nobody at the provider read this trail. The subject axis has its own operation - getAllAccessLogs with subjectTenantId, on the platform plane - for the same reason getTopTenants and getTopSubjectTenants are separate operations rather than one operation with an axis parameter.readerUsername is the account the read ran as; impersonatorUsername is the platform operator who performed it, and is who this tenant would escalate about. That disclosure is deliberate: it is what lets a customer see which human at the provider read their trail in the impersonated case this operation covers, and the tenant-facing admin audit plane already discloses the same thing.impersonatorUserId is served as null on this plane, never as a value. The name carries the accountability; the operator's platform user identifier adds nothing a tenant administrator can act on and is an enumerable internal handle, so this plane serves the name alone. The KEY is present on every row - one record has one wire shape - and the platform operation (getAllAccessLogs) serves both.periodTotal counts every identity in the period; rankedTotal counts only the rows below. rowsReturned and rowLimit say whether the ranking reached its cut. A bar scaled to one of the two totals and printed beside the other is a proportion of nothing.getTopSubjectTenants. The two read alike and mean opposite things, which is why they are separate operations with separate labels rather than one operation with an axis parameter: a value a caller can omit must never decide which of two questions a ranking answered.getTopTenants, which ranks the READER tenant. An axis parameter that flipped which question a ranking answered would be the conflation this surface exists to remove, one layer up.resource filter accepts, so a click on a ranked row navigates to the contributing reads with nothing re-derived by the client.measurable: true with count: 0 means the question was asked and nothing qualified. measurable: false means the window does not hold the inputs the signal derives from — the question could not be asked. A control that cannot fire is otherwise indistinguishable from a clean result, and on a security surface that is the more dangerous of the two.HIGH_VOLUME_READ is why: its threshold is a percentile the server computed over this window, a number no client has.Two kinds of signal, and two units on the ones that have contributors
filter and no contributors. A signal whose population is a SET - (reader, subject) pairs, or readers in burst - carries an EMPTY filter and lists its contributors instead, each with the precise predicate for its own rows. An empty filter means the window alone and must never be followed as a link: doing so is how a card reading "4 found" came to open every row in the window.unit names what count counts and contributorUnit names what each contributor's count counts, and on a set-valued signal they are deliberately different: the card counts pairs or readers, each contributor counts rows. Four pairs containing sixty-three rows are two numbers, and a client that renders them under one phrase is stating something the server did not.count is not: the count is the exact population and the list is its top slice by rows, so a client renders "showing N of M" rather than reading the list's length as the answer. An empty list beside a positive count is legitimate - retention can remove the rows between the two reads - and is not an error.getAllAccessLogs's own declared parameters, so it can be replayed there verbatim.derivationKey is derived by convention from the closed signal vocabulary, derivationParams carries every value the sentence interpolates, and derivation is the English sentence itself. A client renders the key in the reader's language when it knows it and falls back to the sentence when it does not — so a threshold that moves reaches the operator in both languages with no client release, and an unknown key is never a blank line.audit.platform.integrity.read; this operation renders that answer reduced to the META chains. A second verifier over one chain could disagree with the first, and this surface would have no way to say which is right.INTACT and BROKEN there is never verified in this environment: everVerified: false with an absent verdict. On a fresh deployment that is the truth and the first state this body will be seen in — rendering it green would be an attestation produced by the absence of evidence.audit.meta.read scope defensible.numeratorPlanes is the plane the three counts are taken over — META, and only META — while denominatorPlanes is every plane a coverage denominator would have to span. They differ, which is why no ratio is served: a label reading chains covered over the numerator alone claims coverage against the wider set.
readerTenantId, readerUsername, ipAddress: who performed the read.subjectTenantId, resource: whose trail was read, and through which endpoint.subjectTenantId has THREE states. Absent places no predicate on the subject axis; a tenant UUID selects that tenant's trail; the literal NULL selects reads that addressed no single tenant, which is how a cross-tenant read is enumerated and which an absent-means-any model cannot express at all.from and to are MANDATORY and the window is bounded. audit_access_log is not partitioned, so an unbounded read is a full table scan.resource takes a NORMALIZED endpoint exactly as /stats/top-endpoints publishes it, so a ranking click carries the value it displayed. Values outside that published set are refused with 400: the braces in a normalized path are wildcards, so the accepted values are an allowlist rather than a pattern the caller composes.resource FILTER and the endpoint FIELD are deliberately not the same form, and reading one as the other is the mistake this operation must not invite. The filter is NORMALIZED (/api/v3/audit/tenants/{id}/crypto/timeline) because it names a ROUTE. The endpoint returned on every row is the RAW request URI the WORM row stores, with real identifier segments (/api/v3/audit/tenants/8d3b6c15-.../crypto/timeline), because it names ONE request and is SIGNED: rewriting it into the normalized form would invalidate the row's HMAC. The query reconciles the two by expanding each placeholder into a bounded wildcard, so neither value is rewritten.size is REFUSED, not silently reduced — a clamped page answers 200 with a result the caller did not ask for.The period export
Accept: text/csv and this same operation streams the period's whole result set as CSV instead of a page of JSON. Same filters, same fourteen columns, same order, same redaction — the export and the screen are one contract, so a column can never exist in one and not the other. A wildcard Accept is not a request for CSV: it selects the JSON page, because a client that expressed no preference must not be handed a bulk export of the whole period.page and size do NOT apply to the export and are refused with 400 if sent — the export serves the period, and silently dropping a page bound would tell a caller they received twenty rows when they received ten thousand. The export is bounded by the deployment's audit.export.max-records.=, +, -, @, TAB or CR are prefix-quoted, so a caller-authored endpoint or user agent cannot become a spreadsheet formula. The export writes exactly the fourteen wire fields: no signature, hash, sequence number or key version appears in it, for the same structural reason they appear in no JSON body.Two identities on an impersonated read
readerUsername and readerUserId name the account the read RAN AS. When it ran inside an impersonation session, impersonatorUsername and impersonatorUserId name the operating human — and it is that human who is ACCOUNTABLE for the read. Both are absent on an ordinary read, and their absence is served explicitly rather than by omitting the keys, so "this read was not impersonated" and "this surface does not report impersonation" are never the same observation.readerUsername as a FILTER selects the accountable reader — the operator on an impersonated row, the account on every other one. It is the one filter on this operation that is not a plain column match, and it is what makes a signal contributor's link land on exactly the rows that contributor was counted for. Filtering by the impersonated account instead would merge two operators who share one account into a single reader who performed neither read.GET /api/v3/audit/vocabulary; do not author one.audit.platform.* scope receives the PLATFORM projection; every other caller receives the TENANT projection, which is scoped on the governance disclosure set rather than on the wider tenant input-validation set — so platform-owned families such as PLATFORMSETTING are never offered to a tenant auditor even though a tenant-plane query would accept them.operationTypes to crypto; entityTypes, actions and integrations to admin; statuses is shared by all three. A dimension the caller may not read comes back as an empty array, and admittedSources names the sources it was projected for.slug is matched against a closed server-side set and is never interpolated into a query. The entity types it resolves to are the server's own, from the one integration vocabulary; the caller cannot widen them.summary.timeRange is all and the population is bounded only by the audit retention window. summary.snapshotComputedAt is the instant that population was read. This endpoint has no fixed reporting period — for a bounded window call GET /api/v3/audit/platform/stats with startTime and endTime, which echoes the applied bounds back in the same field.
- [Get platform crypto time-series data](https://apidocs.ankatech.co/reference/getplatformcryptotimeseries.md): Returns aggregated crypto operations by day for dashboard charts. Supports optional tenant filtering. Returns pre-aggregated data instead of individual events for optimal performance.
- [List authentication audit events across the platform](https://apidocs.ankatech.co/reference/getplatformauthevents.md): Returns paginated authentication events from every tenant, intended for platform security analysts investigating cross-tenant brute-force or credential-stuffing patterns. When the {@code tenantIds} filter is omitted, the query INCLUDES pre-authentication events that never resolved to a tenant (tenant_id IS NULL) — these are the most valuable rows for attack detection. When the filter is provided, null-tenant rows are excluded by design. The applied window and the maximum this feed accepts are stated on every accepted response in `appliedRange`; this description asserts neither.
- [Get platform-wide authentication audit statistics](https://apidocs.ankatech.co/reference/getplatformauthstats.md): Returns aggregated auth statistics across every tenant (or the subset specified via tenantIds). The top-N lists are the headline signal for cross-tenant brute-force detection. Aggregated over the window the request resolved to. This description asserts no maximum: the rule is one constant, applied by every audit feed.
- [Get platform-wide admin operations timeline](https://apidocs.ankatech.co/reference/getplatformadmintimeline.md): Returns administrative operations across all tenants (or filtered by tenant list). Optionally filter by time range using startTime and endTime parameters. If not provided, returns all events. Useful for compliance audits and security investigations.
- [Get platform-wide admin statistics](https://apidocs.ankatech.co/reference/getplatformadminstats.md): Returns aggregate statistics for administrative operations across all tenants (or filtered by tenant list). The counters cover every retained admin audit event in scope: the aggregation applies no time predicate, so summary.timeRange is all and the population is bounded only by the audit retention window. This endpoint has no fixed reporting period — for a bounded window call GET /api/v3/audit/platform/stats with startTime and endTime, which echoes the applied bounds back in the same field.
- [Get platform admin time-series data](https://apidocs.ankatech.co/reference/getplatformadmintimeseries.md): Returns aggregated admin operations by day for dashboard charts. Supports optional tenant filtering. Returns pre-aggregated data instead of individual events for optimal performance.