# ankasecure Documentation > Documentation for ankasecure Append .md to any documentation page URL to get its markdown version. ## Guides - [Welcome to ANKASecure](https://apidocs.ankatech.co/docs/welcome.md): Crypto Agility Orchestration Platform — one Key ID, one API call, any algorithm. - [Quickstart](https://apidocs.ankatech.co/docs/quickstart.md): From zero to encrypted round-trip in 5 minutes — copy, paste, run. Uses OAuth 2.0 client-credentials against an existing key. - [Authentication](https://apidocs.ankatech.co/docs/authentication.md): OAuth 2.0 (RFC 6749) — client_credentials for workloads, password for admin users, token exchange for delegation. One Bearer JWT for all 5 APIs. - [Environments & Credentials](https://apidocs.ankatech.co/docs/environments.md): Staging vs Production — base URLs, how to request client_id + client_secret, and what changes between environments. - [AES-256 Encrypt / Decrypt (basic)](https://apidocs.ankatech.co/docs/aes-256-basic.md): Provision an AES-256 symmetric key (as admin), then encrypt / decrypt with an application token. Full end-to-end round-trip using both auth flows. - [ML-KEM-1024 Encrypt / Decrypt (post-quantum)](https://apidocs.ankatech.co/docs/ml-kem-1024-basic.md): Same round-trip as AES, this time with ML-KEM-1024 (FIPS 203, NIST Level 5). Provisioned by an admin, exercised by an application — identical app code. - [RSA-2048 → ML-KEM-768 Immediate Rotation](https://apidocs.ankatech.co/docs/rsa-to-ml-kem-rotation.md): Rotate a classical RSA-2048 key into a post-quantum ML-KEM-768 successor. Application code keeps referencing the original kid — the platform silently redirects to the PQC successor. - [ML-KEM-512 Encrypt / Decrypt (compact, PQC Level 1)](https://apidocs.ankatech.co/docs/ml-kem-512-basic.md): ML-KEM-512 — the compact NIST Level 1 post-quantum KEM. Same flow as ML-KEM-1024, smaller ciphertexts, useful for high-throughput scenarios. - [ML-DSA-87 Sign / Verify (post-quantum signatures)](https://apidocs.ankatech.co/docs/ml-dsa-87-sign-verify.md): Post-quantum digital signatures with ML-DSA-87 (FIPS 204, formerly Dilithium-5). Sign a payload, verify the JWS, all quantum-safe. - [Runtime Algorithm Discovery & PQC Smoke-Test](https://apidocs.ankatech.co/docs/runtime-algorithm-discovery.md): Query the catalogue of supported algorithms at runtime, filter PQC-only, and smoke-test the first result end-to-end. Useful for feature flags and tenant provisioning tooling. - [RSA-3072 Sign, then Rapid Revocation](https://apidocs.ankatech.co/docs/rsa-3072-sign-with-rapid-revocation.md): Provision an RSA-3072 signing key, sign a payload, then revoke the key immediately. Verify that the revoked key can no longer be used. - [Re-Sign RSA-2048 → ML-DSA-65 (PQC signature migration)](https://apidocs.ankatech.co/docs/re-sign-rsa-to-ml-dsa.md): Migrate an existing JWS signed with RSA-2048 to a fresh ML-DSA-65 signature — payload bytes preserved. The parallel of key rotation but for signatures, not envelopes. - [Key Revocation Validation (ML-KEM-768)](https://apidocs.ankatech.co/docs/key-revocation-validation.md): Prove that a revoked key is unusable for new operations, but that existing ciphertexts remain decryptable while the key material is retained. - [P-521 → ML-KEM-768 Re-Encrypt](https://apidocs.ankatech.co/docs/p-521-to-ml-kem-768-reencrypt.md): Re-encrypt an existing JWE from P-521 (ECC) to ML-KEM-768 (PQC), without the plaintext ever leaving the platform. - [RSA-2048 → ML-KEM-1024 Token Rotation (highest PQC level)](https://apidocs.ankatech.co/docs/rsa-2048-to-ml-kem-1024-rotation.md): Rotate an RSA-2048 key into ML-KEM-1024 (NIST Level 5 PQC). Same transparent redirect as the ML-KEM-768 variant, but targeting the highest PQC tier. - [Key Lifecycle Management (ACTIVE → SUSPENDED → REVOKED → ARCHIVED)](https://apidocs.ankatech.co/docs/key-lifecycle-management.md): Full walkthrough of every state transition: create → suspend → reactivate → revoke → archive. Understand which operations are allowed in each state. - [Sign-then-Encrypt (nested JWE(JWS))](https://apidocs.ankatech.co/docs/sign-then-encrypt-nested.md): Produce a single token that is both signed (authenticity) and encrypted (confidentiality) — nested JWE(JWS) in one API call. - [Composite Hybrid Keys (X25519+ML-KEM-768 · Ed25519+ML-DSA-44)](https://apidocs.ankatech.co/docs/composite-hybrid-keys.md): Use a single kid that carries BOTH a classical and a post-quantum primitive — the platform combines them per composite JOSE draft. - [In-Place Key Metadata Update (JSON Merge-Patch)](https://apidocs.ankatech.co/docs/in-place-key-metadata-update.md): Extend a key's lifetime, raise its usage limits, and add arbitrary metadata — all via a single RFC 7396 JSON Merge-Patch. - [In-Memory JWE / JWS Quick-Start (throwaway keys)](https://apidocs.ankatech.co/docs/in-memory-jwe-jws-quickstart.md): Short-lived provisional key pattern — create, use for a burst, and let it archive itself when the session ends. Useful for ephemeral tokens. - [External Key Interoperability (encrypt & verify without import)](https://apidocs.ankatech.co/docs/external-key-interoperability.md): Use a partner's public key directly on encrypt/verify, without provisioning it as a tenant key. Ideal for one-off exchanges. - [B2B Public Key Distribution (ML-KEM-768 + ML-DSA-87)](https://apidocs.ankatech.co/docs/b2b-public-key-distribution.md): Publish YOUR public keys so partners can encrypt for you or verify your signatures — ML-KEM-768 (KEM) + ML-DSA-87 (Sign) PQC pair. - [PKCS#12 Import & RSA-2048 → ML-KEM-768 Migration](https://apidocs.ankatech.co/docs/pkcs12-import-and-migration.md): Import a legacy PKCS#12 keystore holding an RSA-2048 key, migrate to ML-KEM-768 via rotation. Legacy → PQC in three steps. - [PKCS#7 → JOSE Migration (RSA-2048 → ML-KEM-768)](https://apidocs.ankatech.co/docs/pkcs7-jose-migration.md): Convert a legacy PKCS#7 / CMS payload to a modern JOSE (JWE / JWS) token, then re-encrypt into PQC. Legacy container to modern format, in two calls. ## API Reference - [Discover the crypto plaintext limit](https://apidocs.ankatech.co/reference/getcryptolimits.md): Returns the maximum plaintext byte length the producer crypto endpoints (encrypt / sign / sign-encrypt) accept for the calling tenant. Resolved from the same single source of truth, for the same tenant, that the encrypt gate enforces, so the advertised value never drifts from the enforced one. The operator sets it on the deployment plane and it applies without a restart. - [List keys (paginated, no secret data).](https://apidocs.ankatech.co/reference/listallkeys.md): Retrieves ONE PAGE of the tenant's keys. Secret fields (e.g. privateKey) are omitted. Filters (origin/restricted/kty/status) apply server-side BEFORE pagination: totalElements is the filtered count. size is capped at 200 (400 on violation, never clamped). A page beyond the last page yields an empty content array with the true totals (HTTP 200). - [Retrieve key metadata (no cryptographic material)](https://apidocs.ankatech.co/reference/getkeymetadata.md): Returns comprehensive metadata for a single key including: - Lifecycle information (status, created/expires timestamps) - Usage tracking (usage count, limits) - Algorithm details (kty, alg) - Capability flags (exportable, origin, restricted, keyOps) This endpoint is suitable for administrative dashboards, key inventory systems, and compliance audits where cryptographic material exposure is unnecessary. **Note**: To retrieve the public key material, use the admin-api export endpoint GET /api/v3/admin/tenants/{tenantId}/keys/{kid}/export which enforces exportable flag validation. - [Verify a JWS signature (auto-detecting, JAdES-aware)](https://apidocs.ankatech.co/reference/verifysignature.md): Verifies a JWS and reports the cryptographic verdict plus key metadata. Serialization is AUTO-DETECTED — both RFC 7515 §7.1 Compact (three dot-separated segments) and §7.2 JWS JSON Serialization inputs are accepted with no `serialization` parameter. The wire shape is uniform: single-signature for SIMPLE keys, multi-signature for COMPOSITE hybrid keys. **Qualified timestamp (JAdES B-T, informational).** When the verified JWS carries an embedded RFC 3161 qualified timestamp (JAdES `sigTst`), the response surfaces an informational `qualifiedTimestamp` block — `present`, `trustStatus` (VALID/UNTRUSTED/REVOKED/EXPIRED/TIME_ANOMALY), `genTime`, `serialNumber`, `policyOid`. This block is purely informational and NEVER flips the boolean `valid`: a cryptographically valid signature with an absent, untrusted, or revoked timestamp still returns `valid = true`. The block is omitted for a compact or an unstamped JWS. - [Sign data (JWS).](https://apidocs.ankatech.co/reference/signdata.md): Signs Base64-encoded `data` with the private key associated with `kid` and returns the produced JWS inside a signing-metadata envelope. Supported algorithms: ML-DSA, Falcon, RSA-PSS (PS256), ECDSA (ES256/384/512), and COMPOSITE hybrid keys. **Serialization selection (PRD §60).** The optional `serialization` field expresses the client's chosen JWS representation and is orthogonal to the backend-authoritative RFC 3161 time-stamping policy (§49). Two honest compliance tiers exist: **RFC 7515 JWS** (`COMPACT` = §7.1 three-segment compact, or `JSON` = §7.2 JWS JSON Serialization) and **JAdES B-T** (JWS JSON Serialization only, carrying an RFC 3161 qualified timestamp `sigTst` in the JWS Unprotected Header per ETSI TS 119 182-1 — a `COMPACT` token structurally cannot carry it). **Behavior truth table** (`serialization` × effective stamping → output form): | `serialization` | stamping | output | 400? | |---|---|---|---| | omitted | OFF | compact JWS | no | | omitted | REQUIRED | JWS-JSON + `sigTst` (auto-upgrade, JAdES B-T) | no | | `COMPACT` | OFF | compact JWS | no | | `COMPACT` | REQUIRED | — | **400** `serialization-incompatible-with-timestamp` | | `JSON` | OFF | JWS-JSON, no `sigTst` | no | | `JSON` | REQUIRED | JWS-JSON + `sigTst` (JAdES B-T) | no | An explicit `COMPACT` is never silently upgraded and a mandated stamp is never dropped — the compact+REQUIRED row is the only rejection. The `output` column names the logical serialization tier; see "Output to parse" for the exact wire shape. **Output to parse.** On the HTTP wire the produced JWS is ALWAYS carried in the typed `jwsToken` (RFC 7515 §7.2 JWS JSON Serialization) envelope. A COMPACT / unstamped result is a single-signature object with no `sigTst` (losslessly convertible to the 3-segment RFC 7515 §7.1 compact form, as the SDK renders it); a JAdES B-T result carries the RFC 3161 `sigTst` in the unprotected header (`signatures[].header.etsiU[].sigTst`). The surrounding metadata envelope (`keyRequested`, `materialVersion`, `algorithmUsed`, `warnings`) is ALWAYS present regardless of serialization. - [Sign then encrypt data (nested JWE(JWS))](https://apidocs.ankatech.co/reference/signthenencrypt.md): Performs sign-then-encrypt operation: signs data with signKid, then encrypts the resulting JWS with encryptKid to produce a nested JWE(JWS) structure. Both keys must be homogeneous (both symmetric or both asymmetric). Both keys must be different for security. **Serialization selection (PRD §60).** The optional `serialization` field selects the representation of the INNER signed JWS (before it is encrypted into the outer JWE) and is orthogonal to the backend-authoritative RFC 3161 time-stamping policy (§49). Two honest compliance tiers apply to the inner JWS: **RFC 7515 JWS** (`COMPACT` = §7.1 three-segment compact, or `JSON` = §7.2 JWS JSON Serialization) and **JAdES B-T** (JWS JSON Serialization only, carrying an RFC 3161 qualified timestamp `sigTst` in the Unprotected Header per ETSI TS 119 182-1 — a `COMPACT` inner token structurally cannot carry it). Truth table (`serialization` × effective stamping → inner form): omitted+OFF → compact; omitted+REQUIRED → JWS-JSON + `sigTst` (auto-upgrade, JAdES B-T); `COMPACT`+OFF → compact; `COMPACT`+REQUIRED → **400** `serialization-incompatible-with-timestamp` (rejected BEFORE any signing or encryption); `JSON`+OFF → JWS-JSON, no `sigTst`; `JSON`+REQUIRED → JWS-JSON + `sigTst`. It never adds or drops a timestamp and does not change the outer JWE. The `jweToken` field and the separated sign/encrypt metadata envelope are ALWAYS present regardless of serialization. Uses JWE General JSON Serialization (RFC 7516 §7.2) and JWS General JSON Serialization (RFC 7515 §7.2) uniformly across SIMPLE and COMPOSITE keys. COMPOSITE keys enable hybrid post-quantum cryptography with dual signatures (COMPOSITE_SIGNATURE) and combined key encapsulation (COMPOSITE_KEM_COMBINE). Key type combinations: - Symmetric + Symmetric (e.g., AES-GCM + HMAC-SHA256) - Asymmetric + Asymmetric (e.g., ML-KEM + ML-DSA, RSA + ECDSA) - COMPOSITE + COMPOSITE (e.g., COMPOSITE_KEM_COMBINE + COMPOSITE_SIGNATURE) - COMPOSITE + SIMPLE (any homogeneous combination) The JWE outer header includes cty="JWS" to indicate nested content. - [Re-sign a JWS (Compact or General JSON) with a new key](https://apidocs.ankatech.co/reference/resigndata.md): Verifies the incoming JWS (Compact or General JSON Serialization) under its embedded `oldKid` (or an explicit override) and, if valid, produces a replacement signature with `newKid`. Useful for algorithm migrations without touching payload bytes. **Serialization selection (PRD §60).** The optional `serialization` field selects the representation of the RE-SIGNED output (the incoming form does not govern it) and is orthogonal to the backend-authoritative RFC 3161 time-stamping policy (§49). Two honest compliance tiers: **RFC 7515 JWS** (`COMPACT` = §7.1 three-segment compact, or `JSON` = §7.2 JWS JSON Serialization) and **JAdES B-T** (JWS JSON Serialization only, carrying an RFC 3161 qualified timestamp `sigTst` in the Unprotected Header per ETSI TS 119 182-1 — a `COMPACT` token structurally cannot carry it). **Behavior truth table** (`serialization` × effective stamping → output form): | `serialization` | stamping | output | 400? | |---|---|---|---| | omitted | OFF | compact JWS | no | | omitted | REQUIRED | JWS-JSON + `sigTst` (auto-upgrade, JAdES B-T) | no | | `COMPACT` | OFF | compact JWS | no | | `COMPACT` | REQUIRED | — | **400** `serialization-incompatible-with-timestamp` | | `JSON` | OFF | JWS-JSON, no `sigTst` | no | | `JSON` | REQUIRED | JWS-JSON + `sigTst` (JAdES B-T) | no | An explicit `COMPACT` is never silently upgraded and a mandated stamp is never dropped; the `output` column names the logical serialization tier. On the HTTP wire the re-signed JWS is ALWAYS carried in the typed `jwsToken` (RFC 7515 §7.2 JWS JSON Serialization) envelope: a COMPACT / unstamped result is a single-signature object with no `sigTst` (losslessly convertible to the 3-segment compact form), a JAdES B-T result carries the RFC 3161 `sigTst` in the unprotected header. The re-sign metadata envelope (old/new key requested, per-leg material versions, algorithms, warnings) is ALWAYS present regardless of serialization. - [Re-encrypt a JWE General JSON Serialization payload](https://apidocs.ankatech.co/reference/reencryptdata.md): Decrypts the incoming JWE under the 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, publicKeyfile – 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- Content-Type: application/jose+json {"protected":"...","recipients":[...],"iv":"..."} ------ankatech- Content-Type: application/octet-stream ------ankatech--- ``` **Important:** - No size limit (processes files of any size) - Response does NOT include metadata (signers, recipients, algorithm details) - For metadata: Use POST /api/v3/migration/stream/analyze-pkcs7 first - Output is directly compatible with POST /api/v3/crypto/stream/reencrypt or /resign **Prerequisites:** Import signer/recipient keys via POST /api/v3/admin/tenants/{tenantId}/keys/import-keystore **Input:** multipart/form-data with 'metadata' (JSON) and 'file' (binary PKCS#7) - [Analyze PKCS#7 structure (streaming, >5MB files)](https://apidocs.ankatech.co/reference/analyzepkcs7stream.md): Introspects PKCS#7/CMS binary data of ANY size and returns structured metadata, parsing incrementally. Returns metadata including signers, recipients, algorithms, certificates, and conversion feasibility. **Supports:** - SignedData (1 signer) - EnvelopedData (1 recipient) - SignedAndEnvelopedData (1 signer + 1 recipient) **Current support**: Single signer and single recipient only **Multi-actor support**: Planned for future release **Input:** multipart/form-data with a `metadata` part followed by a `file` part (binary PKCS#7). **Part order is enforced**, as on every other streaming endpoint: `metadata` MUST precede `file`. The parse is sequential and single-pass, so the payload cannot be reached without consuming what precedes it and there is nothing to go back to; a `file`-first body is refused with `400 .../errors/multipart-part-order` before any payload byte is read. - [Convert PKCS#7/CMS to JOSE format](https://apidocs.ankatech.co/reference/convertpkcs7tojose.md): Converts legacy PKCS#7/CMS files to modern JOSE (JWE/JWS) 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 Compact - EnvelopedData (1 recipient) → JWE Compact - SignedAndEnvelopedData (1 signer + 1 recipient) → JWE(JWS) Compact **Future enhancement**: Multi-signer/multi-recipient support with JSON serialization **Process:** 1. Parse PKCS#7 structure 2. Verify signatures (optional) 3. Decrypt if encrypted (using imported key) 4. Match signers/recipients with imported keys 5. Re-sign/re-encrypt with JOSE format **Prerequisites:** Import private keys via POST /api/v3/admin/tenants/{tenantId}/keys/import-keystore **Next steps:** For PQC algorithms, use POST /api/crypto/reencrypt or POST /api/crypto/resign - [Analyze PKCS#7/CMS file structure](https://apidocs.ankatech.co/reference/analyzepkcs7.md): Introspects PKCS#7/CMS binary data and returns structured metadata including signers, recipients, algorithms, certificates, and conversion feasibility. Supports SignedData, EnvelopedData, and SignedAndEnvelopedData. Provides actionable guidance for migration to JOSE (JWE/JWS). **Current support**: Single signer and single recipient only **Multi-actor support**: Planned for future release (multi-signer/multi-recipient with JSON serialization) - [Get license validation metrics](https://apidocs.ankatech.co/reference/getmetrics.md): Returns real-time metrics about license validation behavior, including the percentage of requests validated strictly vs allowed without validation. Useful for monitoring service availability vs enforcement tradeoff. - [Verify a detached-JWS (streaming).](https://apidocs.ankatech.co/reference/verifysignaturestream.md): Two 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.

Supports both SIMPLE keys (compact JWS with single signature) and COMPOSITE keys (General JSON with multiple signatures and anka_composite_kid field). - [Sign data (streaming).](https://apidocs.ankatech.co/reference/signdatastream.md): Consumes two multipart/form-data fields:
1. metadata â€" JSON body SignStreamRequest
2. file â€" binary data to sign

Produces a detached-JWS in JSON format streamed as application/octet-stream. Output structure depends on key type:
• SIMPLE keys: Single signature in compact detached format
• COMPOSITE keys: Multiple signatures (classical + PQC) with anka_composite_kid field - [Sign-then-encrypt data (streaming, nested JWE(JWS)).](https://apidocs.ankatech.co/reference/signencryptstream.md): Consumes a 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.

Supports all key type combinations:
• SIMPLE → SIMPLE (single signature → single signature)
• SIMPLE → COMPOSITE (single signature → multiple signatures)
• COMPOSITE → COMPOSITE (multiple signatures → multiple signatures)
• COMPOSITE → SIMPLE (multiple signatures → single signature) - [Re-encrypt data (streaming).](https://apidocs.ankatech.co/reference/reencryptdatastream.md): Decrypts the incoming ciphertext using 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.

Supports all key type combinations:
• SIMPLE → SIMPLE (single recipient → single recipient)
• SIMPLE → COMPOSITE (single recipient → multiple recipients)
• COMPOSITE → COMPOSITE (multiple recipients → multiple recipients)
• COMPOSITE → SIMPLE (multiple recipients → single recipient) - [Encrypt data (streaming).](https://apidocs.ankatech.co/reference/encryptstream.md): Consumes two multipart/form-data fields:
1. metadata – JSON body EncryptStreamRequest
2. file – binary plaintext

Produces a multipart/mixed response where:
• Part 1 (application/jose+json) is a JWE General-JSON header with detached ciphertext.
• Part 2 (application/octet-stream) is the proprietary envelope [wkLen]‖wrappedKey‖[ivLen]‖iv‖ciphertext‖tag streamed on-the-fly.

Output format depends on key type:
• SIMPLE keys: JWE General JSON with single recipient
• COMPOSITE keys: JWE General JSON with multiple recipients (one per component) - [Decrypt data (streaming).](https://apidocs.ankatech.co/reference/decryptstream.md): The request is multipart/mixed with two parts:
  1. application/jose+json — JWE General-JSON header (detached ciphertext).
  2. application/octet-stream — proprietary binary envelope followed by the ciphertext and GCM tag.
The response is multipart/mixed (PRD §64): PART 1 (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.

Supports both SIMPLE keys (single recipient) and COMPOSITE keys (multiple recipients). For COMPOSITE keys, decryption uses all recipients to derive the final content encryption key. - [Decrypt-then-verify data (streaming, two verdicts).](https://apidocs.ankatech.co/reference/decryptverifystream.md): Consumes the nested ciphertext as 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 `Page` envelope with `content` (the array of keys for the current page), `totalElements`, `totalPages`, `number` (zero-based page index), `size`, and the standard Spring pagination flags (`first`, `last`, `empty`, `numberOfElements`). **Pagination**: server-side via Spring `Pageable`. Default `size=25`, default sort `createdAt,desc`. Max `size=200` (rejected with 400 above). Sort fields allow-list: `createdAt`, `expiresAt`, `alg`, `kty`, `status`, `kid`. Any other sort field returns 400. Sorting by `kty` orders over ONE vocabulary: every composite key carries the single coarse token `COMPOSITE`, so the ordering is over the published key types and not over a column whose values follow two conventions. **Query parameters**: - `status` (optional): filter by exact lifecycle state, using the SAME token a key's `status` field carries in the response — `active`, `pending_rotation`, `rotated`, `expired`, `revoked`, `archived`, `deleted_logical`, `suspended`, `pending_destruction`. Absent or empty means no filter. - `kty` (optional): filter by exact key type, using the SAME token a key's `kty` field carries in the response — `ML-KEM`, `ML-DSA`, `SLH-DSA`, `FALCON`, `XMSS`, `LMS`, `HQC`, `FRODO`, `CMCE`, `BIKE`, `SABER`, `NTRU`, `NTRUPRIME`, `RSA`, `EC`, `OKP`, `oct`, `SM2`, `GOST-EC`, and `COMPOSITE` for every hybrid pairing. A composite key's construction is carried by `compositeMode` and its pairing by `alg`, never by `kty`. Absent or empty means no filter. - `origin` (optional): filter by exact key origin, using the SAME token a key's `origin` field carries in the response — `GENERATED` or `IMPORTED`. Absent or empty means no filter. Any other value is rejected with 400, never answered with an empty page. - `page` (optional, default 0): zero-based page index. - `size` (optional, default 25, max 200): page size. - `sort` (optional, default `createdAt,desc`): sort field + direction. **Authorization**: requires scope `admin.keys.read`. Cross-tenant listing is rejected with 403 unless the caller is a platform administrator (`tenantId == ROOT_TENANT_ID`). - [Suspend a cryptographic key](https://apidocs.ankatech.co/reference/suspendkey.md): Temporarily suspends a key for security incident investigation. Unlike revocation, suspension is **reversible**: the key can be reactivated once the investigation is cleared (POST .../reactivate), or permanently revoked if compromise is confirmed (POST .../revoke). During suspension: encrypt/sign operations are DENIED; decrypt/verify remain ALLOWED. Valid source states: ACTIVE, PENDING_ROTATION. State transition: ACTIVE|PENDING_ROTATION → SUSPENDED. **Authorization**: requires scope `admin.keys.suspend`. - [Rotate a cryptographic key (advance its material version)](https://apidocs.ankatech.co/reference/rotatekey.md): Advances the Stable KID {kid} to a NEW PRIMARY material version. The kid is FIXED — rotation does not mint a successor key; it generates a new material version (new algorithm/spec) under the same key id and retires the previous one. Workflow: 1. Validates tenant access from JWT 2. Proxies the rotation request to Core API (S2S) 3. Logs administrative action in admin_audit_log 4. Returns the key metadata at its advanced material version (same kid) The new material MUST be able to serve the Stable KID's invariant purpose (ENCRYPT_DECRYPT or SIGN_VERIFY); a cross-purpose rotation is rejected with 422 `purpose-mismatch`. Within the same purpose, the new material can use the same or a different algorithm (policy permitting). **Authorization**: requires scope `admin.keys.rotate`. - [Revoke a cryptographic key](https://apidocs.ankatech.co/reference/revokekey.md): Marks a key as REVOKED. This operation is irreversible. A request body is REQUIRED and must carry a non-blank `reason` (PRD §231): the operator's justification is recorded at canonical position 21 of the signed audit event, so a revocation can be answered later. `cause` remains optional and defaults to ADMINISTRATIVE when omitted. State transition: Any non-deleted, non-revoked state → REVOKED. **Authorization**: requires scope `admin.keys.revoke`. - [Reactivate a suspended cryptographic key](https://apidocs.ankatech.co/reference/reactivatekey.md): Reactivates a key that was previously suspended, restoring it to its pre-suspension status (ACTIVE or PENDING_ROTATION). Use this endpoint when the security investigation is cleared and no compromise was found. If compromise was confirmed, use POST .../revoke instead. Valid source state: SUSPENDED only. State transition: SUSPENDED → pre-suspension status (ACTIVE or PENDING_ROTATION). **Authorization**: requires scope `admin.keys.reactivate`. - [Mark an archived key for destruction](https://apidocs.ankatech.co/reference/markkeyfordestruction.md): Transitions an ARCHIVED key to PENDING_DESTRUCTION state, establishing a mandatory cooling-off period before logical deletion is permitted. The response includes `destructionEligibleFrom` — the earliest timestamp at which DELETE will succeed. DELETE requests before that date return 412 Precondition Failed. **No scheduler is required**: destruction remains manual but is gated by the temporal constraint. When a scheduler microservice is added, it will call the same DELETE endpoint automatically. Valid source state: ARCHIVED only. State transition: ARCHIVED → PENDING_DESTRUCTION. **Authorization**: requires scope `admin.keys.mark-for-destruction`. - [Cancel a pending key destruction](https://apidocs.ankatech.co/reference/cancelkeydestruction.md): Cancels a scheduled destruction, reverting the key from PENDING_DESTRUCTION back to ARCHIVED state for continued compliance retention. Use this when the destruction was scheduled prematurely or the decision has been reversed by a compliance or security review. Valid source state: PENDING_DESTRUCTION only. State transition: PENDING_DESTRUCTION → ARCHIVED. **Authorization**: requires scope `admin.keys.cancel-destruction`. - [Archive a cryptographic key](https://apidocs.ankatech.co/reference/archivekey-1.md): Moves a revoked key to archived state for compliance/audit retention. Archived keys are read-only and cannot be used for cryptographic operations. Workflow: 1. Validates tenant access from JWT 2. Validates key is in REVOKED state 3. Updates key status to ARCHIVED in PostgreSQL 4. Publishes audit event to Kafka Use cases: - Long-term compliance retention after revocation - Audit trail preservation - Historical data retention requirements State transition: REVOKED → ARCHIVED (other states rejected with 409). **Authorization**: requires scope `admin.keys.archive`. - [Import a keystore (PKCS#12 / JKS) into the tenant keystore](https://apidocs.ankatech.co/reference/importkeystore.md): Imports private keys and trusted certificates from a PKCS#12 or JKS keystore into the tenant keystore, by S2S-proxying to Core API. Sent as `multipart/form-data` with three parts: `metadata` (JSON), `keystore` (binary file), and an optional `password` (binary). The password is forwarded to Core API as raw bytes — it is never logged, stringified, or echoed in any error or audit payload. **Authorization**: requires scope `migration.importKeystore`. - [Analyze a keystore (PKCS#12 / JKS) WITHOUT importing it (read-only preview)](https://apidocs.ankatech.co/reference/analyzekeystore.md): Inspects a PKCS#12 or JKS keystore and reports, per alias, the detected algorithm, the projected Stable KID, and the cryptographic purpose derived from the certificate's X.509 KeyUsage (ENCRYPT_DECRYPT, SIGN_VERIFY, or AMBIGUOUS), by S2S-proxying to Core API. Sent as `multipart/form-data` with the same three parts as the import endpoint: `metadata` (JSON), `keystore` (binary file), and an optional `password` (binary). This is a pure preview: NOTHING is imported, wrapped, audited, or persisted, and an ambiguous alias is reported (200) rather than rejected. The password is forwarded to Core API as raw bytes — never logged, stringified, or echoed. **Authorization**: requires scope `migration.importKeystore` (shared with the import endpoint; analysis is gated on the same scope). - [Import a PUBLIC key (simple or composite) into the tenant keystore](https://apidocs.ankatech.co/reference/importkey.md): Imports public key material for a simple or composite (hybrid) key, by S2S-proxying to Core API. This endpoint imports PUBLIC keys only — private key material is never accepted. **Vocabulary constraints (both enforced upstream, both surfaced as `400`)** - `kty` MUST be a published key type. A stored value outside the vocabulary can neither be filtered on nor round-tripped, so it is refused rather than stored. - For a composite key, `alg` MUST name a pairing this deployment's composite catalogue carries. The construction — whether the component secrets are combined into one or signed independently — is resolved FROM the pairing rather than restated by the caller, so an uncatalogued pairing has no resolvable construction and cannot be served. Call `GET /api/v3/admin/tenants/{tenantId}/supported-algorithms?kty=COMPOSITE` for the pairings this deployment accepts — the tenant plane, reachable with the same tenant-scoped authorization this import already requires. **Authorization**: requires scope `migration.importKey`. - [Get key metadata](https://apidocs.ankatech.co/reference/getkeymetadata-2.md): Retrieves operational metadata for a single cryptographic key. The response is an `AdminKeyResponse` containing identity, algorithm, lifecycle state, usage counters, expiration timestamps, suspension / revocation / destruction metadata (when applicable), and caller-editable fields (`displayName`, `description`, `exportable`). Persistence-internal fields (surrogate id, version, deletedAt, lazy composite components) are never exposed. **Authorization**: requires scope `admin.keys.read`. Cross-tenant reads are rejected with 403 unless the caller is a platform administrator (`tenantId == ROOT_TENANT_ID`), in which case any tenant's key is readable. - [Remove (delete) a cryptographic key](https://apidocs.ankatech.co/reference/removekey.md): Logically deletes a key (soft delete). Sets status to DELETED_LOGICAL. **⚠ BREAKING CHANGE**: This endpoint now enforces a two-gate policy: **Gate 1 — State**: The key MUST be in PENDING_DESTRUCTION state. Direct deletion from ACTIVE, REVOKED, ARCHIVED or any other state returns 409, and the `detail` of that 409 names the steps that remain **for the state the key is actually in** — three from ACTIVE, two from REVOKED, one from ARCHIVED. It is derived from the same transition table the gates enforce, so it cannot name a step the server would refuse. This description deliberately publishes no fixed sequence: the one it used to publish (`archive` first) was wrong from ACTIVE, because `archive` admits only REVOKED keys. **Gate 2 — Temporal**: The cooling-off period established by mark-for-destruction MUST have elapsed. Deletion before `destructionEligibleFrom` returns 412 Precondition Failed with the eligible date in the response body. This design (Model B) enforces compliance retention without requiring a background scheduler. When a scheduler is added, it calls this same endpoint. **Authorization**: requires scope `admin.keys.remove`. - [Update key metadata (JSON Merge-Patch)](https://apidocs.ankatech.co/reference/patchkeymetadata.md): Applies RFC 7396 JSON Merge-Patch to update key metadata fields. Workflow: 1. Validates tenant access from JWT. 2. Updates metadata fields in PostgreSQL. 3. Publishes audit event to Kafka. Patchable fields (see `AdminKeyMergePatchRequest` schema for full details): - `displayName`: Human-readable key name (max 255 chars). - `description`: Key description (max 1000 chars). - `exportable`: Boolean flag for key export permission. - `expiresAt`: Hard expiration UTC timestamp. - `softLimitExpiration`: Soft expiration UTC timestamp (warning threshold). - `maxUsageLimit`: Hard usage counter limit. - `softUsageLimit`: Soft usage counter limit (must be less than `maxUsageLimit`). Validation rules: - `displayName` length <= 255 characters. - `description` length <= 1000 characters. - `softUsageLimit` must be strictly less than `maxUsageLimit` if both are set. - Explicit `null` in the patch deletes the field (standard RFC 7396 semantics). - Absent fields leave the current value unchanged. **Authorization**: requires scope `admin.keys.patch`. - [Preview rotation admissibility for a source key (console-facing)](https://apidocs.ankatech.co/reference/rotationadmissibility.md): Returns the server-authoritative rotation admissibility verdict for the source key {kid}: for every algorithm in the tenant catalogue, whether the key may rotate to it and why. The verdict is computed by core-api (the single rule authority) and relayed 1:1 — admin-api recomputes no rule and reorders no candidate. The endpoint performs ZERO writes (no DB, HSM, Redis, Kafka, or audit). The console MUST use this admin-api endpoint — it must never call core-api directly (the core-api compute is S2S-only behind an Admin→Core issuer gate). **Authorization**: requires scope `admin.keys.read`. The tenant boundary is enforced from the path `tenantId` (platform-admin-on-ROOT or `tenantId == jwtTenantId`). - [Export public key material and metadata for a key (public-only)](https://apidocs.ankatech.co/reference/exportkey.md): Returns the PUBLIC key material and full operational metadata for a key. This is the S2S source Core API proxies to from its migration export endpoint. **Security — public material only**: private key material is never returned. - Simple asymmetric key: `publicKey` carries the Base64 SubjectPublicKeyInfo. - Symmetric key (`kty=oct`): `publicKey` is null (no secret material exposed). - Composite key: only component PUBLIC keys are returned via `components[]`; `publicKey` is null and no component private material is exposed. **Non-exportable keys** (`exportable=false`) are refused with `400 Bad Request`, mirroring core-api's export behavior; no key material is present in the refusal. **Tenant isolation**: a `kid` that does not exist for the authenticated tenant — including a key owned by another tenant — yields `404 Not Found` with a generic detail that does not reveal the key's existence elsewhere. **Authorization**: requires scope `admin.keys.read`. - [Read this deployment's key-protection backend declaration](https://apidocs.ankatech.co/reference/getdeploymentbackendconfiguration.md): Returns the declared backend token, its non-secret per-provider coordinates, whether a credential is sealed, the derived activationState 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.

- [Declare the key-protection backend for first-run provisioning](https://apidocs.ankatech.co/reference/declarebackend.md): Declares (or re-declares) this deployment's key-protection backend for first-run provisioning. The body carries only the backend slug — never a PIN and never a credential. Changeable until the backend is bound; once it is, the declaration is refused 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).

- [Withdraw the declared key-protection backend](https://apidocs.ankatech.co/reference/deletedeploymentbackenddeclaration.md): Withdraws this deployment's key-protection backend declaration and returns the platform to the un-declared first-run state, so a different backend can be declared after a failed or abandoned activation.

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;
  • the DEPLOYMENT-scope key-backend descriptor row, holding the saved coordinates;
  • the sealed deployment credential, its retained .previous and any stranded rotation .candidate;
  • the bootstrap tenants' active key-encryption key rows, whose orphaned references are named in the audit trail.

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.

- [Activate this deployment's configured key-protection backend](https://apidocs.ankatech.co/reference/activatedeploymentbackend.md): Puts the configured deployment key-protection backend into force. This is the only step of the three that reaches the backend at all: 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).

- [Write the deployment key-protection backend's credential](https://apidocs.ankatech.co/reference/updatedeploymentbackendcredential.md): Establishes the deployment key-protection backend's first credential, rotates it to a supplied candidate, or restores the credential the last rotation superseded. Exactly one of 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.

- [Get first-run platform provisioning status](https://apidocs.ankatech.co/reference/getplatformsetupstatus.md): Returns the complete first-run setup readiness state: whether provisioning has completed, the declared backend token, whether the key-material backend is bound, whether this platform can actually perform key operations, the single server-computed 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, and resolution — 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:

  • the tenant's 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;
  • the session JTI — a live revocation key, which never leaves admin-api on any surface;
  • the declaring administrator — a named individual at a customer, serving no decision this list supports.

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:

  1. NO_PROVIDER_DECLARED — no row for this channel. Declare one.
  2. 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.
  3. PROVIDER_NOT_ACTIVATED — a current PASSED verdict exists and no row is ACTIVE. Enable the provider.
  4. 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.
The substitution follows whose rows must be ACTED ON, not whose chain would have carried the message. A tenant that declared providers of its own receives the specific token and 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_ and stores its non-secret connection config (endpoint, policy OID, hash algorithm, timeout). An omitted credential keeps the existing seal (non-secret fields still update). Required scope: admin.platform.settings.timestamping.override.write. - [Revert a tenant's TSA connection override to inherited (platform)](https://apidocs.ankatech.co/reference/deleteplatformtimestampingoverride.md): Deletes the tenant's TSA connection value row + sealed credential so the deployment default is inherited live. Required scope: admin.platform.settings.timestamping.override.write. - [List all role identifiers](https://apidocs.ankatech.co/reference/listroles.md): Returns the complete catalog of role identifiers (scopes) that can be assigned to users and applications. Roles are organized by category and determine what operations a principal can perform within the platform. **Role Naming Convention:** `..` (e.g., `admin.tenant.user.create`) - [List role translations by locale](https://apidocs.ankatech.co/reference/listtranslations.md): Returns the catalog of roles with their localized descriptions for the requested locale. Each role includes a human-readable description explaining what operations it grants. Locale must match the RFC 5646-like pattern: two lowercase letters optionally followed by '-' and an uppercase country code (e.g., 'es', 'en', 'es-MX'). - [Get an observability backend (key masked)](https://apidocs.ankatech.co/reference/getplatformbackend.md) - [Update an observability backend](https://apidocs.ankatech.co/reference/replaceplatformbackend.md) - [Delete an observability backend](https://apidocs.ankatech.co/reference/deleteplatformbackend.md) - [List observability backends (keys masked)](https://apidocs.ankatech.co/reference/listplatformbackends.md) - [Create an observability backend](https://apidocs.ankatech.co/reference/createplatformbackend.md) - [Enable an observability backend](https://apidocs.ankatech.co/reference/enableplatformbackend.md) - [Disable an observability backend](https://apidocs.ankatech.co/reference/disableplatformbackend.md) - [Get one of a tenant's observability destinations (platform; key masked)](https://apidocs.ankatech.co/reference/getplatformobservabilitybackend.md) - [Update a tenant observability destination (platform)](https://apidocs.ankatech.co/reference/replaceplatformobservabilitybackend.md) - [Delete a tenant observability destination (platform)](https://apidocs.ankatech.co/reference/deleteplatformobservabilitybackend.md) - [List a tenant's observability destinations (platform; keys masked)](https://apidocs.ankatech.co/reference/listplatformobservabilitybackends.md) - [Blind-provision a tenant observability destination (platform)](https://apidocs.ankatech.co/reference/createplatformobservabilitybackend.md) - [Rotate a tenant observability destination's export key (platform; credential-only)](https://apidocs.ankatech.co/reference/updateplatformobservabilitybackendcredential.md) - [Suspend tenant (ACTIVE -> SUSPENDED)](https://apidocs.ankatech.co/reference/suspendtenant.md): Temporarily disables crypto and login capabilities for the tenant. Admin read access is preserved. Publishes a signed status envelope to Redis and emits an admin audit event.

A 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.

Omitting the reason, or sending it blank or whitespace-only, is refused with 400 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.

- [Reactivate tenant (SUSPENDED -> ACTIVE)](https://apidocs.ankatech.co/reference/reactivatetenant.md): Re-enables a suspended tenant. Publishes a signed status envelope to Redis and emits an admin audit event. - [Activate tenant (PROVISIONING -> ACTIVE)](https://apidocs.ankatech.co/reference/activatetenant.md): Transitions a tenant from PROVISIONING to ACTIVE once KEK, policies, and the first administrator are ready. Publishes a signed status envelope to Redis and emits an admin audit event. - [Get a platform-global counterparty type by id](https://apidocs.ankatech.co/reference/getplatformcounterpartytype.md): Returns 404 if id is tenant-local. Required scope: admin.platform.counterparty-type.read. - [Update a platform-global counterparty type's display_name + description](https://apidocs.ankatech.co/reference/replaceplatformcounterpartytype.md): code is IMMUTABLE post-creation. 403 on tenant-local rows. Required scope: admin.platform.counterparty-type.update. - [Delete a platform-global counterparty type](https://apidocs.ankatech.co/reference/deleteplatformcounterpartytype.md): 409 if any tenant's exchange_context references the type. The 409 problem-details body includes the cross-tenant referencing context list (intentional disclosure to platform admin). Required scope: admin.platform.counterparty-type.delete. - [List platform-global counterparty types](https://apidocs.ankatech.co/reference/listplatformcounterpartytypes.md): Returns ONLY rows with tenant_id IS NULL. Tenant-local rows are filtered out at the SQL layer. Required scope: admin.platform.counterparty-type.list. - [Create a platform-global counterparty type](https://apidocs.ankatech.co/reference/createplatformcounterpartytype.md): Persists with tenant_id = NULL (visible to all tenants once committed). 409 if code collides with any tenant-local row. Required scope: admin.platform.counterparty-type.create. - [List actors under a specific Application](https://apidocs.ankatech.co/reference/listapplicationactors.md): Returns every non-deleted Cryptographic Actor owned by the supplied Application. Useful for the Application detail page in the Admin Console. - [Create a manual actor under an Application](https://apidocs.ankatech.co/reference/createactor.md): Creates a non-default Cryptographic Actor. Default Actors are auto-provisioned with their parent Application and cannot be created through this endpoint. - [List roles assigned to an Actor](https://apidocs.ankatech.co/reference/listactorroles.md): Returns the set of roles (OAuth2 scopes) currently bound to the supplied Cryptographic Actor. Ordered by role name for stable UX rendering. - [Replace the Actor's role set (REPLACE-semantics, idempotent)](https://apidocs.ankatech.co/reference/assignactorroles.md): Replaces every role currently bound to the Actor with the supplied set. The replacement is computed as a DIFF: only the roles being added are inserted and only the roles being dropped are deleted, so POST is idempotent — re-applying the same set is a DB no-op, and a role present in both sets keeps its original assignment timestamp and produces no audit event. **To ADD a role, POST the union of the current set and the new role**; to drop one, POST the set without it (or use `DELETE /actors/{actorId}/roles/{roleName}`). A payload that shares roles with the current set is the normal case, not a conflict. Validation: every role name must exist in the active catalog and must NOT carry the s2sOnly flag (defense-in-depth — S2S-internal scopes are rejected here even if a tenant-admin somehow obtains the assign scope). **When a replacement drops a role, the withdrawal takes effect at the Actor's next token mint — not immediately.** An access token already issued to this Actor carries the withdrawn scope literally and stays valid until its natural expiry, bounded by `JWT_EXPIRATION` (default 900s). The `client_credentials` grant issues no refresh token (RFC 6749 §4.4.3), so the token cannot be extended — only re-minted from credentials, and a re-mint reads the role set fresh. Worst-case staleness is therefore one token lifetime; raising `JWT_EXPIRATION` widens that window linearly. To cut a machine identity off without waiting for expiry, suspend the Actor instead — that works through the effective-status gate, which is consulted on the Actor's next request rather than at its next mint. - [Get actor details](https://apidocs.ankatech.co/reference/getactor.md) - [Soft-delete an actor](https://apidocs.ankatech.co/reference/deleteactor.md): Sets deleted_at on the Actor. Default Actors cannot be deleted independently — delete the parent Application instead. - [Update actor metadata (PATCH semantics)](https://apidocs.ankatech.co/reference/updateactor.md) - [List actors in tenant](https://apidocs.ankatech.co/reference/listtenantactors.md): Returns every non-deleted Cryptographic Actor across every Application in the supplied tenant. Ordered by name. - [Remove a single role from the Actor (idempotent)](https://apidocs.ankatech.co/reference/unassignactorrole.md): Returns HTTP 204 unconditionally — the same response shape is returned whether or not the role was previously assigned. This avoids the 404 oracle on assignment state. **The removal takes effect at the Actor's next token mint — not immediately.** An access token already issued to this Actor carries the withdrawn scope literally and stays valid until its natural expiry, bounded by `JWT_EXPIRATION` (default 900s). The `client_credentials` grant issues no refresh token (RFC 6749 §4.4.3), so the token cannot be extended — only re-minted from credentials, and a re-mint reads the role set fresh. Worst-case staleness is therefore one token lifetime; raising `JWT_EXPIRATION` widens that window linearly. To cut a machine identity off without waiting for expiry, suspend the Actor instead — that works through the effective-status gate, which is consulted on the Actor's next request rather than at its next mint. - [Get a role by name with full scopes[]](https://apidocs.ankatech.co/reference/getroledetail.md): Returns the role with the full expanded scope list. Required scope: admin.role.list. - [Update the scopes[] of a system role](https://apidocs.ankatech.co/reference/updaterolescopes.md): Replaces the expanded scope set of a system role (is_system=true). Body shape: { "scopes": [...] }. Jackson rejects unknown properties. Required scope: admin.platform.role.update. Required JWT tenantId: ROOT_TENANT_ID (defense-in-depth). Cache invalidation: on commit, publishes to the role-scope Pub/Sub channel. Effective on NEXT JWT issuance — active JWTs retain their original scope set until expiration (architect Slice 5 P0 authorization rollback semantics). - [List all role identifiers](https://apidocs.ankatech.co/reference/listplatformroles.md): Returns the complete catalog of role identifiers (scopes) that can be assigned to users and applications. Roles are organized by category and determine what operations a principal can perform within the platform. **Role Naming Convention:** `..` (e.g., `admin.tenant.user.create`) This is an alias of /api/v3/admin/roles for platform administration. - [List role translations by locale](https://apidocs.ankatech.co/reference/listplatformtranslations.md): Returns the catalog of roles with their localized descriptions for the requested locale. Each role includes a human-readable description explaining what operations it grants. Locale must match the RFC 5646-like pattern: two lowercase letters optionally followed by '-' and an uppercase country code (e.g., 'es', 'en', 'es-MX'). This is an alias of /api/v3/admin/roles/translations for platform administration. - [Re-derive an Exchange Context's governance fields (S2S only)](https://apidocs.ankatech.co/reference/resolveexchangecontext.md): Reads the exchange context (tenant-scoped) from the database and returns its counterparty/direction/status. Requires the ankasecure-core S2S issuer and the admin.s2s.exchange-context.resolve scope. - [Get key usage limits report](https://apidocs.ankatech.co/reference/getusagelimitsreport.md): Returns keys approaching or exceeding a usage limit they are actually subject to: - Keys above soft usage limit (advisory warning threshold) - Keys above 75% of hard limit - Keys above 90% of hard limit (critical) - Usage percentage per key (usageCount / maxUsageLimit * 100) Only keys with a POSITIVE `maxUsageLimit` are included. Under the platform sentinel both `0` and `null` mean unlimited, and an unlimited key is stored as the literal `0` — so "has a value configured" is not the population, and a report built on that reading returns the entire inventory. Results are sorted by usage percentage descending (most critical first). **Authorization:** • Tenant admins: view own tenant's analytics only • Platform admins: view any tenant's analytics **Path Parameter Validation:** The tenantId in the URL must match the JWT tenantId claim (unless authenticated as platform admin with PLATFORM.ROOT scope). - [Export the key usage-limits report as CSV](https://apidocs.ankatech.co/reference/exportusagelimitsreport.md): Returns the SAME report `GET /usage-limits` serves, rendered as an RFC 4180 CSV attachment for an audit package, a spreadsheet or a third party. **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` the caller read under; the `population` the figures are taken over (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` — every key in the population is present, so the artifact does not depend on how far a screen was scrolled); 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. A `kid`, `alg` or `status` opening with a character a spreadsheet reads as a formula is prefixed with an apostrophe at the writer. **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 — `admin.tenant.analytics.read`. Exporting a report is reading it, so it grants nothing the screen does not. - [Get tenant cryptographic-posture summary](https://apidocs.ankatech.co/reference/gettenantposturesummary.md): Returns the tenant's cryptographic-posture summary — the four scalar CAPA-pillar aggregates (agility, modernization, governance, conformance), evaluated strictly within the tenant boundary. 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. **Authorization:** • Tenant admins: view own tenant's posture only • Platform admins: view any tenant's posture **Path Parameter Validation:** The tenantId in the URL must match the JWT tenantId claim (unless authenticated as platform admin with PLATFORM.ROOT scope). - [Get tenant cryptographic-posture history](https://apidocs.ankatech.co/reference/gettenantposturehistory.md): Returns the tenant's 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, from the tenant's own append-only snapshot chain. Scalars and facts only — no 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. **Authorization:** • Tenant admins: view own tenant's history only • Platform admins: view any tenant's history (non-SaaS deployments) **Path Parameter Validation:** The tenantId in the URL must match the JWT tenantId claim (unless authenticated as platform admin with PLATFORM.ROOT scope). - [Get most used keys report](https://apidocs.ankatech.co/reference/getmostusedkeysreport.md): Returns the top N most frequently used keys by usage count: - Key identification and algorithm - Usage count and limits - Usage percentage of max limit `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. The health-summary endpoint publishes a figure under the same label; both now state their own span, so a reader can tell whether they are comparing two measurements or reading one twice. Use this for capacity planning and identifying critical dependencies. **Authorization:** • Tenant admins: view own tenant's analytics only • Platform admins: view any tenant's analytics **Path Parameter Validation:** The tenantId in the URL must match the JWT tenantId claim (unless authenticated as platform admin with PLATFORM.ROOT scope). - [Export the most-used-keys report as CSV](https://apidocs.ankatech.co/reference/exportmostusedkeysreport.md): Returns the SAME report `GET /most-used-keys` serves, rendered as an RFC 4180 CSV attachment, for 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` the caller read under, the `population` (the top `limit` keys of the whole inventory in scope, ranked by `usageCount` descending — and a note that `totalUsageCount` covers the WHOLE inventory, not only the rows listed), the `completeness` (`BOUNDED`: this file is 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 — `admin.tenant.analytics.read`. - [Get PQC migration candidates report](https://apidocs.ankatech.co/reference/getmigrationcandidatesreport.md): Returns a report of active keys using classical algorithms that should be migrated to post-quantum algorithms: - Keys using RSA, EC, EdDSA, or ECDH algorithms - Recommended PQC replacement algorithm - Migration priority based on usage and age Use this for planning the transition to quantum-resistant cryptography. **Authorization:** • Tenant admins: view own tenant's analytics only • Platform admins: view any tenant's analytics **Path Parameter Validation:** The tenantId in the URL must match the JWT tenantId claim (unless authenticated as platform admin with PLATFORM.ROOT scope). - [Find keys by component algorithm](https://apidocs.ankatech.co/reference/getkeysbycomponentalgorithm.md): Searches for all composite keys that contain a specific algorithm as a component: - 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 Use this for impact analysis when planning algorithm deprecation, rotation, or security audits targeting specific algorithms. **Authorization:** • Tenant admins: view own tenant's analytics only • Platform admins: view any tenant's analytics **Path Parameter Validation:** The tenantId in the URL must match the JWT tenantId claim (unless authenticated as platform admin with PLATFORM.ROOT scope). - [Get key inventory health summary](https://apidocs.ankatech.co/reference/getkeyhealthsummary.md): Returns high-level metrics about the cryptographic key inventory: - Total keys by status (ACTIVE, ROTATED, EXPIRED, REVOKED) - Keys in warning period (soft expiration reached) - Keys approaching usage limits - Total usage count across all keys, served with the `usageWindow` it is measured over (`ALL_TIME`) and the i18n key naming it, because a second screen renders a figure under the same label and a total that does not say which span it covers cannot be compared with anything This endpoint is designed for CISO dashboards and security monitoring. **Authorization:** • Tenant admins: view own tenant's analytics only • Platform admins: view any tenant's analytics **Path Parameter Validation:** The tenantId in the URL must match the JWT tenantId claim (unless authenticated as platform admin with PLATFORM.ROOT scope). - [Get key expiration report](https://apidocs.ankatech.co/reference/getkeyexpirationreport.md): Returns a report of keys approaching expiration or in warning period: - `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 and a change to that rule needs no console release - Keys that have reached soft expiration (warning period started) - Keys that will expire within the specified threshold Use this for proactive key renewal planning and compliance. **Authorization:** • Tenant admins: view own tenant's analytics only • Platform admins: view any tenant's analytics **Path Parameter Validation:** The tenantId in the URL must match the JWT tenantId claim (unless authenticated as platform admin with PLATFORM.ROOT scope). - [Get component algorithm distribution](https://apidocs.ankatech.co/reference/getcomponentalgorithmdistribution.md): Returns granular algorithm distribution within composite key components: - Total composite keys and component counts - Classical vs PQC component breakdown - Distribution by algorithm within components - Distribution by role (classical/pqc) Use this for analyzing hybrid key composition patterns and component-level PQC adoption within composite keys. **Authorization:** • Tenant admins: view own tenant's analytics only • Platform admins: view any tenant's analytics **Path Parameter Validation:** The tenantId in the URL must match the JWT tenantId claim (unless authenticated as platform admin with PLATFORM.ROOT scope). - [Get algorithm usage report](https://apidocs.ankatech.co/reference/getalgorithmusagereport.md): Returns algorithm distribution across the key inventory: - Count of PQC, classical, and hybrid keys - PQC adoption percentage - Detailed breakdown by algorithm Use this for capacity planning and security posture assessment. **Authorization:** • Tenant admins: view own tenant's analytics only • Platform admins: view any tenant's analytics **Path Parameter Validation:** The tenantId in the URL must match the JWT tenantId claim (unless authenticated as platform admin with PLATFORM.ROOT scope). - [List internal crypto use cases for the tenant](https://apidocs.ankatech.co/reference/listinternalcryptousecases.md): Paginated list of non-deleted use cases. Empty tenant returns 200 with empty page (NEVER 404). Scope: admin.tenant.internal-use-case.read - [Provision an internal crypto use case](https://apidocs.ankatech.co/reference/createinternalcryptousecase.md): Composes an authorization overlay over keys that already exist, for the calling application's self-use scenario. Atomically creates the control-plane entities — Exchange Context (INTERNAL) + Actor (INTERNAL) + Credential + Asset references + Constraint Policy (permissive) + 1..N Capability Grants. This endpoint creates no key. 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.

Nothing is revoked. The Capability Grants, the Actor Credential, the owned Exchange Context and every constraint counter survive the transition untouched, so PATCH /resume restores the same workload rather than provisioning a new one.

The cascade runs inside this request's transaction: if the actor transition is refused, the aggregate is not written. An actor already 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.

Idempotent on an already-SUSPENDED aggregate → 422. A concurrent transition answers 422 as well, never 409: the aggregate row is taken under 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.

A body is now REQUIRED, and it carries a mandatory justification (PRD §231, operation 1). 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.

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 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.suspend

- [Resume a SUSPENDED use case](https://apidocs.ankatech.co/reference/resumeinternalcryptousecase.md): Transitions the aggregate SUSPENDED → ACTIVE and cascades to the owned Cryptographic Actor, which returns to ACTIVE and has its signed effective-status envelope republished, so crypto operations and new token requests both succeed again.

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 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.resume

- [Get internal crypto use case detail](https://apidocs.ankatech.co/reference/getinternalcryptousecase.md): Returns the aggregate with all 7 child entity references. Cross-tenant probe → 404 oracle-safe (NOT 403). credential.clientSecret is OMITTED from the JSON response (only present on 201 create). Scope: admin.tenant.internal-use-case.read - [Revoke a use case (terminal)](https://apidocs.ankatech.co/reference/deleteinternalcryptousecase.md): Transitions ACTIVE/SUSPENDED → REVOKED. Idempotent on already-REVOKED → 204 (NOT 404, NOT 422) — matches Actor Roles unassign convention. Soft-deletes the aggregate; audit history preserved. Scope: admin.tenant.internal-use-case.revoke - [Read a tenant's key-protection precedence chain (platform)](https://apidocs.ankatech.co/reference/getplatformtenantkeybackendprecedence.md): Answers which backend protects this tenant's keys, and why in one read, so an operator does not have to open the deployment default, the Edition tier and the tenant's own declaration and infer the chain from three screens. 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 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.

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 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.

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 enable will accept it.

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 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 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. 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.

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.

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.

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. 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.

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.

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 plane in more than one condition yields the earliest, which is always the first action the operator must take:

  1. NO_PROVIDER_DECLARED — no row for this channel. Declare one.
  2. 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.
  3. 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.

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 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.

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` envelope with `content`, `totalElements`, `totalPages`, `number` (zero-based), `size`, and the standard Spring pagination flags. **Pagination**: server-side via Spring `Pageable`. Default `size=25`, default sort `name,asc`. Max `size=200` (rejected with 400 above). Sort fields allow-list: `createdAt`, `name`, `status`. Any other sort field returns 400. **Authorization**: requires scope `admin.tenant.application.list`. - [Create application](https://apidocs.ankatech.co/reference/createapplication.md): Creates a new application client for cryptographic operations. **IMPORTANT**: Applications can ONLY be created in regular tenants. The platform tenant (ID: 00000000-0000-0000-0000-000000000001) is reserved for platform administration and does not support application clients. Applications receive crypto operation scopes: - key_management.* (key lifecycle operations) - secure.* (encrypt, decrypt, sign, verify, reencrypt, resign) - stream.* (streaming crypto for large files) The application secret will be securely hashed before storage. - [Get application details](https://apidocs.ankatech.co/reference/getapplication.md): Retrieves the details of a specific application. - [Delete application](https://apidocs.ankatech.co/reference/deleteapplication.md): Removes the specified application. This operation is irreversible. - [Update application](https://apidocs.ankatech.co/reference/updateapplication.md): Updates an existing application using JSON Merge Patch (RFC 7396). Only provided (non-null) fields will be updated. Omitted fields remain unchanged. Updatable fields: - applicationName: Unique name within tenant - status: ACTIVE, SUSPENDED, or DISABLED - displayName: Human-readable name for UI - description: Purpose and scope description - environment: PRODUCTION, STAGING, DEVELOPMENT, or SANDBOX - businessUnit: Organizational unit - costCenter: Cost center for chargeback - ownerName: Business owner name - ownerEmail: Business owner email - technicalContactName: Technical contact name - technicalContactEmail: Technical contact email - dataClassification: PUBLIC, INTERNAL, CONFIDENTIAL, or RESTRICTED - riskLevel: LOW, MEDIUM, HIGH, or CRITICAL - regulatoryScope: Array of regulations (e.g., PCI-DSS, SOX, GDPR) - tags: Free-form tags (full replacement) - externalRefs: External system references (full replacement) - [Per-Application Exchange aggregate stats](https://apidocs.ankatech.co/reference/getapplicationexchangestats.md): Returns aggregate counts derived from `cryptographic_exchange` rows scoped to `(tenantId, applicationId)` with `status = ACTIVE` and `deletedAt IS NULL`: - **activeExchangesCount** — `COUNT(*)` of Cryptographic Exchanges in `ACTIVE` state for the application. - **distinctActiveKeysCount** — `COUNT(DISTINCT asset_kid)` over those same ACTIVE Exchanges. May be smaller than `activeExchangesCount` when multiple Exchanges target the same key. **Empty-result contract**: a tenant with no ACTIVE Exchanges for the application returns HTTP 200 with `{0, 0}` — NEVER 404 (the 404 case is reserved for unknown `applicationId` within the tenant). **Tenant boundary**: enforced FIRST via `validateTenantAccess`. A user from Tenant A querying Tenant B's application receives 403 BEFORE any application-existence lookup or scope decision (per the workspace's tenant-boundary-first invariant). **Authorization**: requires scope `admin.tenant.exchange.read`. - [List retired applications](https://apidocs.ankatech.co/reference/listretiredapplications.md): Read-only list of terminally-deleted (retired) applications for the tenant. - [Retired application drill-down](https://apidocs.ankatech.co/reference/getretiredaggregate.md): Read-only view of a retired application's terminal children (forensic). - [List users](https://apidocs.ankatech.co/reference/listhumanusers.md): Returns all users registered under the specified tenant. This includes active, suspended, and disabled users. - [Create user](https://apidocs.ankatech.co/reference/createhumanuser.md): Creates a new user under the specified tenant. **Two provisioning modes:** • **Secure mode (recommended)**: Set requirePasswordChange=true, omit password. User receives activation link and sets their own password (zero-knowledge provisioning). • **Quick mode (testing)**: Set requirePasswordChange=false, include password. User can login immediately. **Response includes:** • User details (always) • Activation information (if requirePasswordChange=true): token, expiration, activation URL The password is securely hashed before storage. If requirePasswordChange=true and no password is provided, a secure random password is generated internally (user never sees it). - [List user roles](https://apidocs.ankatech.co/reference/listuserroles.md): Returns all roles assigned to the specified user. - [Assign roles to user](https://apidocs.ankatech.co/reference/assignuserroles.md): Assigns a set of roles to a human user. The allowed roles depend on the tenant type: **Platform Tenant Users** (tenantId = 00000000-0000-0000-0000-000000000001): - platform.* (3 permissions: bootstrap, pending-activations.read, regenerate-activation-token) - admin.policy.deployment.* (9 permissions: deployment-level policies) - admin.policy.tenant.* (9 permissions: cross-tenant policy intervention) - admin.tenant.* (tenant management, including list/delete) - admin.policy.* (6 permissions: global policy operations including sync, cache management) - admin.role.* (2 permissions: role catalog) - licenseManagement.* (2 permissions: usage tracking) **Regular Tenant Users** (all other tenants): - admin.tenant.* (tenant resource management, EXCEPT admin.tenant.list and admin.tenant.delete) - admin.policy.tenant.* (9 permissions: own tenant policies only) - admin.role.* (2 permissions: role catalog) - admin.policy.templates.read (policy template reference) - admin.policy.effective.read (effective policy queries) **Forbidden for ALL human users**: - key_management.* (crypto operations reserved for applications) - secure.* (crypto operations reserved for applications) - stream.* (streaming crypto reserved for applications) **Validation**: All-or-nothing - if ANY role is invalid for the tenant type, the entire request is rejected with HTTP 400. Existing role assignments will be replaced. - [Regenerate activation token](https://apidocs.ankatech.co/reference/regenerateactivationtoken.md): Generates a new single-use activation token for a user whose previous token expired or was lost. The user must have requirePasswordChange=true (pending activation status). This endpoint provides self-service recovery for expired activation tokens, preventing permanent user lockout. - [Declare a federated link intent](https://apidocs.ankatech.co/reference/declarefederatedlinkintent.md): Authorizes the next matching federated assertion to bind this pre-existing local account to the asserting identity, within a bounded window. The administrator supplies the provider and the email **claim** they expect that provider to assert — never the provider's internal subject identifier, which they have no way to know. The platform learns the subject at the first matching assertion and keys the binding on it from then on. The window defaults to 72 hours and is capped at 168. A request above the cap is **refused, never clamped**: a silent clamp would tell the administrator they received a window they did not receive, and the out-of-band coordination this flow depends on would be planned around a deadline that has already passed. - [Revoke a federated link intent](https://apidocs.ankatech.co/reference/revokefederatedlinkintent.md): Withdraws a declared intent so no assertion can consume it. **Idempotent by construction, not by a guard.** Consumption and revocation are the same row DELETE, so revoking an intent that is not there deletes zero rows and still answers 204. A 404 would make this endpoint an existence probe for an intent, which is a statement about the account. - [Get user details](https://apidocs.ankatech.co/reference/gethumanuser.md): Retrieves the details of a specific user. - [Delete user](https://apidocs.ankatech.co/reference/deletehumanuser.md): Removes the specified user. This operation is irreversible. **SECURITY RESTRICTION**: Users cannot delete their own account. Contact another administrator to perform this operation if you need to delete your account. - [Update user](https://apidocs.ankatech.co/reference/updatehumanuser.md): Updates user information (display name, password, and/or status). Only provided fields will be updated (JSON Merge Patch). **SECURITY RESTRICTION**: Users cannot modify their own account status. - Users CAN update their own password and displayName - Users CANNOT change their own status (suspension, deactivation) Contact another administrator if you need to change your account status. - [Get pending activations](https://apidocs.ankatech.co/reference/getpendingactivations.md): Returns list of users in the tenant who are pending account activation (requirePasswordChange=true). Includes activation status (PENDING or EXPIRED) and time remaining until token expiration. Useful for identifying users who need activation token regeneration. - [Unlink a federated identity](https://apidocs.ankatech.co/reference/unlinkfederatedidentity.md): Returns a bound account to local-only authentication and ends its live sessions. **This — not a password reset — is the remedy for a mistaken binding.** A password reset changes a credential the federated path does not use, so the wrong person keeps signing in through the identity provider while the operator believes the account is secured. **The account does NOT regain a prior password.** Binding replaced the local credential with an unusable random value and none is restored, so unlink also sets `requirePasswordChange` and the account re-enters the platform's ordinary activation / password-reset path — the state a newly-provisioned user is in. - [Get a tenant override forwarding destination (secret masked)](https://apidocs.ankatech.co/reference/getdestination.md): Returns one of this tenant's override destinations with its sink credential masked and its pinned certificate authorities described. Required scope: admin.tenant.event-forwarding.override. - [Update a tenant override forwarding destination](https://apidocs.ankatech.co/reference/replacedestination.md): Replaces the destination wholesale. 🔴 An OMITTED transport.trustAnchorsPem REMOVES the anchor, like any other omitted member of a replaced record — the read returns the canonical PEM, so an edit form opens populated and omitting the field is a deliberate un-pinning rather than an accident. That transition widens what the forwarder will trust, so it is recorded under its own audit action rather than being left to be inferred from a diff. A submitted anchor is bounded, CA-checked and canonically re-encoded before anything is written. - [Delete a tenant override forwarding destination](https://apidocs.ankatech.co/reference/deletedestination.md) - [List the tenant's exclusions of inherited deployment destinations](https://apidocs.ankatech.co/reference/listexclusions.md) - [Exclude an inherited deployment destination from the tenant's effective set](https://apidocs.ankatech.co/reference/createexclusion.md) - [List the tenant's override forwarding destinations (secrets masked)](https://apidocs.ankatech.co/reference/listdestinations.md): Lists this tenant's OWN override destinations — not the inherited deployment-default set, which is resolved by listEffectiveDestinations. Each carries its sink credential masked and its pinned certificate authorities described. Required scope: admin.tenant.event-forwarding.override. - [Create a tenant override forwarding destination](https://apidocs.ankatech.co/reference/createdestination.md): Creates a TENANT-level override destination for this tenant. The optional transport.trustAnchorsPem member pins the certificate authorities this destination's TLS session will accept; omit it to use the deployment trust store, which is the right configuration for a publicly-trusted collector. The anchor is read, bounded and CA-checked here — before anything is persisted — and only the canonical re-encoding derived from the accepted DER is stored, so two spellings of the same certificate store identically. The response describes each anchor and returns the canonical PEM unmasked: a certificate authority is public material. The sink credential is never returned. - [Resolve the tenant's effective forwarding destinations (deployment-defaults ∪ overrides − exclusions)](https://apidocs.ankatech.co/reference/listeffectivedestinations.md): Resolves what this tenant's forwarder actually delivers to. Each destination carries its sink credential masked and its pinned certificate authorities described from the canonical PEM, so the anchor an inherited deployment default pins is visible to the tenant that inherits it. Required scope: admin.tenant.event-forwarding.override. - [Remove an exclusion so the tenant re-inherits the deployment destination](https://apidocs.ankatech.co/reference/deleteexclusion.md) - [Get a counterparty type by id](https://apidocs.ankatech.co/reference/getcounterpartytype.md): Visible scopes: own tenant-local + any platform-global. Cross-tenant probes return 404 (oracle-safe). Required scope: admin.tenant.counterparty-type.read. - [Update a tenant-local counterparty type's display_name + description](https://apidocs.ankatech.co/reference/replacecounterpartytype.md): code is IMMUTABLE post-creation: attempts to send 'code' in the body return 400 (via Jackson FAIL_ON_UNKNOWN_PROPERTIES at the global config). Cross-tenant attempts return 404. Attempts on platform-global rows return 403. Required scope: admin.tenant.counterparty-type.update. - [Delete a tenant-local counterparty type](https://apidocs.ankatech.co/reference/deletecounterpartytype.md): Returns 409 if any exchange context in THIS tenant references the type; the body lists the referencing context ids under 'extensions.referencingContextIds'. Cannot delete platform-global from tenant context (returns 403). Required scope: admin.tenant.counterparty-type.delete. - [List counterparty types visible to this tenant](https://apidocs.ankatech.co/reference/listcounterpartytypes.md): Returns the UNION of platform-global rows (tenant_id IS NULL) and the caller tenant's local rows. Each row carries scope=PLATFORM|TENANT for badge rendering. Required scope: admin.tenant.counterparty-type.list. - [Create a tenant-local counterparty type](https://apidocs.ankatech.co/reference/createcounterpartytype.md): Persists with tenant_id = {tenantId}. Returns 409 on global code collision. Required scope: admin.tenant.counterparty-type.create. - [Force policy synchronization (unified sync/async)](https://apidocs.ankatech.co/reference/syncpolicies.md): Forces synchronization of policies to Redis cache and Core API. **Synchronization Modes:** 1. **Synchronous Mode** (async=false, default): - Waits for synchronization to complete - Returns HTTP 200 OK with detailed results - Response includes: synchronizedCount, failedCount, status, message - Supports clearCache parameter - Recommended for: Admin operations, testing, troubleshooting 2. **Asynchronous Mode** (async=true): - Returns immediately with HTTP 202 ACCEPTED - Synchronization happens in background - Response includes: status=INITIATED, timestamp, estimatedCompletion - Recommended for: UI operations, bulk updates, non-blocking workflows **Use Cases:** • After bulk policy updates • Before critical operations requiring latest policies • Troubleshooting policy inconsistencies • Emergency policy rollout • Scheduled maintenance operations - [List all available policy templates](https://apidocs.ankatech.co/reference/getalltemplates.md): Retrieves all available policy templates that can be used as base configurations for tenant policies. Available template types and their seeded templates. Each description below is the template's own stored description; the authoritative set is the seeded `policy_templates` catalogue returned by this endpoint. • ALGORITHM_AVAILABILITY (29 templates): Controls which cryptographic algorithms are allowed - DEFAULT: Default algorithm availability - all algorithms allowed (CLASSICAL + POST_QUANTUM + HYBRID) - CLASSICAL: Classical algorithms only. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - POST_QUANTUM: Post-quantum algorithms only. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - NIST_APPROVED: NIST approved algorithms (FIPS + PQC finalists). Composite (hybrid) algorithms bypass these restrictions and are admitted at create - HIGH_SECURITY: High security algorithms (Level 3+). Composite (hybrid) algorithms bypass these restrictions and are admitted at create - EU_COMPLIANT: European Union compliance (ENISA recommended algorithms) - GDPR, NIS2, eIDAS 2.0. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - CHINA_GMT_COMPLIANT: Chinese GM/T cryptographic standards (SM2, SM3, SM4) - Mandatory for China operations - JAPAN_CRYPTREC: Japan CRYPTREC e-Government Recommended Ciphers List (May 2024 revision). Composite (hybrid) algorithms bypass these restrictions and are admitted at create - GLOBAL_ISO_COMPLIANT: ISO/IEC international standards for global interoperability and compliance. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - SAUDI_NCA_COMPLIANT: Saudi Arabia NCA-NCS-1:2020 approved algorithms for critical infrastructure. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - MALAYSIA_MYSEAL: Malaysia MySEAL 2.0 Approved Algorithms for government and critical infrastructure. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - KOREA_KCMVP: South Korea KCMVP (KISA) plus ISO/IEC-approved algorithms for government systems. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - US_NSA_CNSA: NSA CNSA 2.0 (Sep 2022) - explicit allow-list, currently AES-GCM-256 and AES-CCM-256 only - PQC_TRANSITION: Classical and post-quantum for the quantum transition period - algorithms approved by NIST or ENISA, Level 3 minimum. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - FINANCE_COMPLIANT: Financial services compliance (PCI-DSS, SOC2, ISO 27001) - algorithms approved by NIST or ISO, Level 3 minimum. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - BSI_COMPLIANT: German BSI TR-02102-1 approved algorithms for federal contracts. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - ETSI_QSC: ETSI TS 103 744 Quantum-Safe Cryptography for telecommunications. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - GOST_COMPLIANT: Russian GOST algorithms plus ISO/IEC-approved international ciphers for state information systems. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - ANSSI_COMPLIANT: French ANSSI cryptographic recommendations for defense and critical infrastructure. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - IETF_RFC: IETF RFC standardized algorithms for Internet protocols (TLS, VPN, SSH). Composite (hybrid) algorithms bypass these restrictions and are admitted at create - NCSC_UK_PQC: UK NCSC PQC recommendations - NIST and ENISA aligned for government and critical infrastructure. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - CCN_CERT_ENS: Spanish CCN-CERT / ENS compliance - ENISA and NIST aligned for critical infrastructure. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - ACN_ITALY_PQC: Italian ACN quantum-safe cryptography - ENISA, NIST, and ISO aligned for national security. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - BSI_HYBRID_ENFORCED: German BSI TR-02102-1 with mandatory hybrid enforcement - Level 3 minimum. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - ANSSI_HYBRID_ENFORCED: French ANSSI with mandatory hybridization - Level 3 minimum, HKDF-SHA256 only. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - ETSI_HYBRID_ENFORCED: ETSI TS 103 744 Quantum-Safe Hybrid Key Exchanges with CatKDF/CasKDF. Composite (hybrid) algorithms bypass these restrictions and the declared composite mode, and are admitted at create - EU_UNIFIED_HYBRID: Broadest EU template - admits algorithms approved by BSI, ANSSI or ETSI (any one), Level 3 minimum, hybrid enforced. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - NIST_HYBRID_OPTIONAL: NIST SP 800-227 hybrid recommended but not required. Composite (hybrid) algorithms bypass these restrictions and are admitted at create - ENISA_RISK_BASED: ENISA risk-based approach - Hybrid recommended for high-risk systems. Composite (hybrid) algorithms bypass these restrictions and are admitted at create • ALGORITHM_ROTATION (6 templates): Rules for algorithm transitions during key rotation - DEFAULT: Default rotation policy - secure transitions - NO_PQC_TO_CLASSICAL: Prevent downgrade from PQC to classical - STRICT_SECURITY: Strict no-downgrade with same family requirement - FLEXIBLE: Flexible transitions allowing all changes - NIST_COMPLIANT: Requires NIST compliance throughout rotation - SAME_FAMILY_ONLY: Only allows transitions within same algorithm family Optionally filter by template type: ALGORITHM_AVAILABILITY or ALGORITHM_ROTATION. Total available: 35 policy templates (29 availability + 6 rotation) - [Get effective policies for a tenant](https://apidocs.ankatech.co/reference/geteffectiveplatformpolicies.md): Returns the resolved effective policies for a specific tenant, considering policy inheritance from deployment level. This endpoint merges: • Deployment-level policies (base configuration) • Tenant-specific policies (overrides) • Template-based policies (if applicable) The result shows what policies are actually in effect for the tenant's cryptographic operations. - [Get policy cache status](https://apidocs.ankatech.co/reference/getcachestatus.md): Returns information about the current state of the policy cache in Redis. This endpoint provides: • Cache availability status (true if Redis is configured) • Number of policies currently cached in Redis • Cache TTL configuration (seconds) The `cacheSize`, `hitRate`, `policiesByType` and `status` members are declared by the schema but are not populated on this route, so they are emitted at their zero/null values; `totalPolicies` is a per-tenant figure and reads 0 on the global route. **Use Cases:** • Monitor cache health and efficiency • Troubleshoot policy synchronization issues • Verify cache configuration • Performance analysis and optimization Optionally specify a tenant ID to check cache status for a specific tenant. - [Clear policy cache globally](https://apidocs.ankatech.co/reference/clearcache.md): Clears the Redis policy cache and invalidates local caches in ALL Core-API instances. **Propagation Mechanism:** 1. Deletes policy keys from Redis (L2 shared cache) 2. Publishes Redis Pub/Sub event to "policy-cache-updates" channel 3. All Core-API instances receive event and invalidate their L1 caches (Caffeine) 4. Next cryptographic operation will reload fresh policies from Redis or database **Behavior:** • Without tenantId parameter: Clears entire policy cache (all tenants + deployment) • With tenantId parameter: Clears only that specific tenant's policies **Use Cases:** • Emergency cache invalidation across entire cluster • Policies updated directly in database (bypassing Admin API) • Cache corruption suspected • Testing policy changes in production • Troubleshooting policy inconsistencies • Forcing immediate policy refresh cluster-wide **Performance Impact:** • Propagation latency: ~10-15ms via Redis Pub/Sub • Next operations will experience cache miss (slower initial request) • Automatic cache repopulation on demand **Note:** This operation only clears cache. To reload policies from database, use the POST /sync endpoint after clearing cache. - [Get platform observability health](https://apidocs.ankatech.co/reference/getplatformhealth.md): Returns a platform-scoped health aggregation for the Admin Console: - `services[]`: per-container availability (AVAILABLE/UNAVAILABLE/UNMEASURED, derived from each service's liveness probe) + serving (READY/DEGRADED) + CPU/heap/disk/uptime, fanned out S2S to each service's actuator over the overlay (Tier 1). - `node`: host CPU/memory/disk aggregate from the collector's internal hostmetrics listener (Tier 2); `null` when the listener is not reachable. Availability reports what was OBSERVED, and the third literal is what keeps it honest. A response RECEIVED at any status is evidence about the probed service: it yields AVAILABLE when the service reports itself live, and UNAVAILABLE when it answers and reports itself not live. A timeout, a refused connection, or the fan-out's own deadline is evidence about the PROBER, not about the service — nothing about the service was observed, so the row is UNMEASURED. UNMEASURED is not an outage and must not be read as one. Degrade-in-place: no probe outcome fails the request. A service that could not be measured degrades in place to `availability=UNMEASURED` with null resource fields, and no internal detail is leaked — `detail` is cause-neutral and never names a host, port, status or cause. The endpoint still returns 200; only an aggregator-internal fault yields 500. The `node` block degrades to `null` independently. - `evaluatedAt`: when the evaluation that produced this document ran. Snapshot-served (PRD 161). This document is NOT probed per request: the platform alert evaluator performs one fan-out per evaluation interval (60s by default), stores it, and both this endpoint and the alert state read that stored evaluation. The document may therefore be up to one interval old, and `evaluatedAt` says exactly how old. The reason is not caching: the panel and the alerter must be incapable of reporting different verdicts, which a per-request fan-out cannot guarantee because each caller measures separately. - `stale`: whether the served evaluation is older than the platform can vouch for. Decided by the server from `evaluatedAt` and the evaluator's armed interval, so every replica answering at the same instant answers the same way and no client needs a clock of its own. A stale document is served WITH its last measured rows and its measurement time: it is a last-known reading, not current fact. Before the first evaluation completes, `services[]` is empty, `evaluatedAt` is absent and `stale` is false. That is reported as absence rather than as a healthy platform, and absence is not staleness — there is nothing yet whose age could be judged. Authorization: `admin.platform.observability.read` (platform/ROOT admins only). - [List Capability Grants in tenant](https://apidocs.ankatech.co/reference/listcapabilitygrants.md): Returns every active Capability Grant in the tenant, optionally filtered by `actorId`, `assetKid`, and/or `exchangeContextId` (all AND-composed). Use `actorId` to render the Actor detail page, `assetKid` to render the per-key Access tab, and `exchangeContextId` for the exchange-context blast-radius view. The `assetKid` filter matches JSONB array MEMBERSHIP on `assetKids` — cross-kid REENCRYPT/RESIGN grants match whether the key is the source or the target. The `exchangeContextId` filter matches the scalar exchange-context FK on the grant. An unknown kid or exchange context (or one belonging to another tenant) returns an EMPTY page with HTTP 200 — never 403/404 — so resource existence is not disclosed. `assetKid`-filtered results are ordered by creation time descending (custom sort parameters are not applied on that path). - [Create Capability Grant](https://apidocs.ankatech.co/reference/createcapabilitygrant.md): Wildcard grants (assetId null) require allowWildcardAsset=true per G-4. Every FK (actorId, exchangeContextId, constraintPolicyId) is validated to belong to the path tenant — cross-tenant probing is rejected. - [Get Capability Grant](https://apidocs.ankatech.co/reference/getcapabilitygrant.md) - [Revoke Capability Grant (soft delete — re-grantable at same dimensions later)](https://apidocs.ankatech.co/reference/deletecapabilitygrant.md) - [Find which bindable declaration a presented issuer names, for this tenant](https://apidocs.ankatech.co/reference/resolveplatformbindableissuer.md): The platform twin of the tenant plane's resolve, for an operator composing a binding on a customer's behalf. It answers which of the issuers a binding in THIS tenant may name is the one a presented token's iss identifies, or none.

🔴 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 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.

🔴 It is a 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.

🔴 It carries the READ scope, not a write one, and it does not acquire a write's gates: 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.

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-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.

🔴 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 — and a matched row is identical to the row that list publishes for it, inherited displayName withheld and all.

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 ../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.

The 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.

The 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.

🔴 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 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.

🔴 It is the set the bind call accepts, not the tenant's own registry. The registry read at ../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.

No 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.

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 ../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.

A wrong code is not an error. This endpoint answers the question "is this code correct?", and 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.

This matches how the industry treats enrollment verification on an already-authenticated session. Twilio Verify migrated this exact case from 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.

Retry limits and lockout are enforced by the enrollment service, not by this status.

- [Setup MFA (TOTP)](https://apidocs.ankatech.co/reference/setupmfa.md): Initialize TOTP-based MFA for the authenticated user. Returns the TOTP secret, otpauth:// QR-code data, and one-time backup codes for enrollment. The TOTP secret is envelope-encrypted under the tenant HSM KEK (AES-GCM) before persistence. - [Get deployment TSA usage policy](https://apidocs.ankatech.co/reference/getdeploymenttsausagepolicy.md): Returns the effective deployment TSA usage default: the persisted active deployment policy when present, otherwise the code DEFAULT (DISABLED). Platform-admin only. - [Replace deployment TSA usage policy](https://apidocs.ankatech.co/reference/replacedeploymenttsausagepolicy.md): Full replacement (PUT) of the deployment TSA usage default. Platform-admin only. - [Reset deployment TSA usage policy to DEFAULT](https://apidocs.ankatech.co/reference/resetdeploymenttsausagepolicy.md): Soft-deletes the persisted deployment TSA usage policy so the default falls back to the code DEFAULT (DISABLED). Platform-admin only. - [Preview the disable blast radius](https://apidocs.ankatech.co/reference/disableimpact.md): Returns the count of tenants that are CURRENTLY stamping (effective mode REQUIRED) and would therefore STOP stamping if the deployment TSA usage policy were set to DISABLED (PRD §49, OQ-1). The Admin Console calls this before confirming a disable. Scalar count only — NO tenant identifiers are disclosed. Platform-admin only. - [Get a deployment forwarding destination (secret masked)](https://apidocs.ankatech.co/reference/getplatformdestination.md): Returns one deployment-default destination with its sink credential masked and its pinned certificate authorities described. Required scope: admin.platform.event-forwarding.manage. - [Update a deployment forwarding destination](https://apidocs.ankatech.co/reference/replaceplatformdestination.md): Replaces the destination wholesale. 🔴 An OMITTED transport.trustAnchorsPem REMOVES the anchor, like any other omitted member of a replaced record — the read returns the canonical PEM, so an edit form opens populated and omitting the field is a deliberate un-pinning rather than an accident. That transition widens what the forwarder will trust, so it is recorded under its own audit action rather than being left to be inferred from a diff. A submitted anchor is bounded, CA-checked and canonically re-encoded before anything is written. Required scope: admin.platform.event-forwarding.manage. - [Delete a deployment forwarding destination](https://apidocs.ankatech.co/reference/deleteplatformdestination.md) - [List deployment forwarding destinations (secrets masked)](https://apidocs.ankatech.co/reference/listplatformdestinations.md): Lists every deployment-default destination, each with its sink credential masked and its pinned certificate authorities described. Required scope: admin.platform.event-forwarding.manage. - [Create a deployment forwarding destination](https://apidocs.ankatech.co/reference/createplatformdestination.md): Creates a deployment-default destination every tenant inherits. The optional transport.trustAnchorsPem member pins the certificate authorities this destination's TLS session will accept; omit it to use the deployment trust store, which is the right configuration for a publicly-trusted collector. The anchor is read, bounded and CA-checked here — before anything is persisted — and only the canonical re-encoding derived from the accepted DER is stored, so two spellings of the same certificate store identically. The response describes each anchor and returns the canonical PEM unmasked: a certificate authority is public material. The sink credential is never returned. Required scope: admin.platform.event-forwarding.manage. - [List the tenants that have excluded a deployment forwarding destination](https://apidocs.ankatech.co/reference/listplatformdestinationexclusions.md) - [Get a tenant's BYOK key-material backend status (platform)](https://apidocs.ankatech.co/reference/listplatformbyok.md): Required scope: admin.platform.key-backend.byok.read. This read NEVER answers 404 — neither for a tenant that has declared nothing, which is reported as state NOT_DECLARED with source INHERITED, nor for an unknown tenantId, which is reported the same way: the read consults only the key-backend row and asserts no tenant existence, so it is not an existence oracle. - [Declare a tenant's BYOK key-material backend (platform)](https://apidocs.ankatech.co/reference/replaceplatformbyok.md): Seals the backend access credential under the tenant's Plane-2 KEK and records the non-secret descriptor in DECLARED_PENDING_BIND. Required scope: admin.platform.key-backend.byok.declare. - [Revert a tenant's pre-bind BYOK declaration (platform)](https://apidocs.ankatech.co/reference/deleteplatformbyok.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.platform.key-backend.byok.declare. - [Check a tenant's BYOK backend coordinates before declaring them (platform)](https://apidocs.ankatech.co/reference/validateplatformtenantbyokbackend.md): Checks coordinates a platform operator is considering for one tenant's own (BYOK) key-material backend, and answers what was checked. Required scope: admin.platform.key-backend.byok.declare, plus the ROOT platform tenant.

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 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 - and wraps nothing, so a pass is not something it is able to observe. The green outcome is NOT_APPLICABLE. Nothing about authorization, entitlement to the named key, or key material is known until the backend is declared and bound.

Which checks run depends on the selected family and on what was submitted, and the verdict names them. 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".

An endpoint the declared backend does not accept is a 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.

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.

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.

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.

- [Test that a tenant's key-material backend can be reached (platform)](https://apidocs.ankatech.co/reference/testplatformbyok.md): Reaches a tenant's effective key-material backend and reports whether it answers, so a misconfigured backend can be told apart from an unreachable one BEFORE a crypto operation fails. Required scope: 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.

Each row carries four independent facts, and their independence is the point. A mechanism can be 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.

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 EXPERIMENTAL mechanism whose connection test passes is a correct state.

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 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.

- [List all TSA trust anchors (ACTIVE + RETIRED)](https://apidocs.ankatech.co/reference/listplatformtrustanchors.md): Returns metadata for every stored trust anchor. The certificate DER bytes are never included — only public metadata. - [Upload (commit) selected TSA trust anchor(s)](https://apidocs.ankatech.co/reference/createplatformtrustanchor.md): Parses the uploaded certificate/keystore, commits the operator-selected fingerprint(s) as ACTIVE anchors, projects the ACTIVE set to Redis on commit, and emits an audit event (uploaded for the first anchor, rotated when an ACTIVE anchor already exists). N ACTIVE anchors are allowed to support a CA rotation overlap. - [Retire an ACTIVE TSA trust anchor](https://apidocs.ankatech.co/reference/retireplatformtrustanchor.md): Moves an ACTIVE anchor to RETIRED (no longer projected to Redis / trusted by core-api), re-projects the ACTIVE set on commit, and emits a retired audit event. A RETIRED anchor is kept for audit and can subsequently be deleted (two-step). If this is the LAST ACTIVE anchor and one or more tenants are currently stamping, the retire is rejected with 409 unless the caller sets override=true. - [Analyze an uploaded certificate/keystore (preview, no persist)](https://apidocs.ankatech.co/reference/analyzeplatformtrustanchor.md): Parses an uploaded X.509 certificate (PEM/DER) or PKCS12/JKS keystore and returns its CA-eligible candidates with an 'alreadyPresent' flag. NO persist, NO audit, NO projection — a pure read-only preview so the operator can select which fingerprint(s) to commit. - [Delete a RETIRED TSA trust anchor](https://apidocs.ankatech.co/reference/deleteplatformtrustanchor.md): Hard-deletes a RETIRED anchor row and emits a deleted audit event. Deletion is a two-step operation: an ACTIVE anchor must be retired first — deleting an ACTIVE anchor is rejected. Because a RETIRED anchor is not in the ACTIVE projection, no re-projection is performed and the last-active-anchor guard does not apply. - [Authorise ANKA support to act as this tenant](https://apidocs.ankatech.co/reference/createsupportaccessgrant.md): Declares a time-boxed, revocable authorisation for the 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.

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 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.

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.

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.

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 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.

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 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.

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 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.

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.

It never fails with a 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.

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). - [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_BYOKthe 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.

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 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 - and wraps nothing, so a pass is not something it is able to observe. The green outcome is NOT_APPLICABLE. Nothing about authorization, entitlement to the named key, or key material is known until the backend is declared and bound.

Which checks run depends on the selected family and on what was submitted, and the verdict names them. 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".

An endpoint the declared backend does not accept is a 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.

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.

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.

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.

- [Test that the tenant's key-material backend can be reached](https://apidocs.ankatech.co/reference/testbyok.md): Reaches the tenant's effective key-material backend and reports whether it answers, so a misconfigured backend can be told apart from an unreachable one BEFORE a crypto operation fails. Required scope: 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.

An ACTIVE provider whose configuration is replaced becomes unverified again and stops delivering until it is retested and re-enabled.

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 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.

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 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 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 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.

The ROOT platform tenant is not a valid target: its chain IS the deployment plane, reached at /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.

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 .write rather than .test, matching both shipped planes: .test exists because a test reaches OUT, and this reaches nothing.

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. 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.

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 (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).

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. 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 that must be taken:

  1. NO_PROVIDER_DECLARED — no row for this channel. Declare one.
  2. 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.
  3. PROVIDER_NOT_ACTIVATED — a current PASSED verdict exists and no row is ACTIVE. Enable the provider.
The vocabulary's fourth token, 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.

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.

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 effectiveScope = DEPLOYMENT.

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 (PUT /api/v3/admin/platform/tenants/{tenantId}/key-backend/edition), which is where a licensing change belongs.

The tenant-plane twin of this endpoint answers 403 for a SaaS operator, which is the whole reason this one exists.

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 ATTENTIONconfigured 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 positionsSENDGRID>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 vendorsofthsm, 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.

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 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 - and wraps nothing, so a pass is not something it is able to observe. The green outcome is NOT_APPLICABLE. Nothing about authorization, entitlement or key material is known until the tier is bound.

Which checks run depends on the selected family and the verdict names them. 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".

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 422 and not a verdict. It is a statement about the two values submitted and about nothing else: nothing was contacted.

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.

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.

- [Bind a managed tier's declared key-protection backend (platform)](https://apidocs.ankatech.co/reference/bindmanagedbackendtier.md): Proves that the key-protection backend a tier declared actually answers with the configuration it carries, and records that it does. An ephemeral, non-tenant key is wrapped and unwrapped against the tier's own backend; only when that round trip completes does the tier advance from DECLARED_PENDING_BIND through BINDING to ACTIVE and record when it bound. Nothing about a tenant is touched: the probe key carries a distinct marker, is never persisted, and can never become tenant key material. A tier that has declared no backend type is refused 409 — select a type first. A tier that is already ACTIVE is an idempotent no-op returning its current view. A tier that is already transitioning is refused 409. If the round trip does not complete the tier is refused 422, is NOT bound, and is left in DECLARED_PENDING_BIND so that correcting the configuration and binding again is the next action. This request carries no credential and no PIN. Required scope: admin.platform.key-backend.tier.manage. - [Rotate or restore a managed tier's backend credential (platform)](https://apidocs.ankatech.co/reference/updatemanagedbackendtiercredential.md): Updates the credential an ANKA-managed tier uses to authenticate to its key-protection backend. Exactly one of two actions per request: supply a candidate credential to ROTATE, or set restorePrevious to put back the credential the last rotation superseded. A rotation is proved before it lands by a CROSS-CREDENTIAL round trip: an ephemeral key is wrapped under the credential currently in effect and unwrapped under the candidate, so only a candidate that reaches the SAME key-encryption key is accepted. A candidate that is individually valid but reaches a different key is refused 422 with the previous credential unchanged and still in effect — which is what stops a credential swap silently relocating custody and making existing material unreadable. The superseded credential stays restorable for a bounded, audited retention window; past it the retained value is purged and a restore is refused. A tier whose backend takes no console-managed credential is refused 422 before anything is verified or written. The credential is never returned, logged or audited. Required scope: admin.platform.key-backend.tier.manage. - [List the ANKA-managed key-protection backend tiers (platform)](https://apidocs.ankatech.co/reference/listmanagedbackendtiers.md): Returns the deployment licence context plus one entry per licensed tier (STANDARD, PROFESSIONAL, ENTERPRISE): the selected backend type, its non-secret instance configuration, its lifecycle state, and THREE separate tenant COUNTS. The three are never merged, because they are three different facts about custody: assignedTenantCount is the population this tier's backend actually wraps and the one a reconfiguration is refused over; inheritingTenantCount reaches this Edition by inheriting the deployment Edition and rides the DEPLOYMENT backend instead; byokOverrideTenantCount declared a backend of its OWN, which outranks the tier, so this tier wraps none of its material. A tier is the edition's default, not a guarantee about every tenant carrying that edition. A tier that has not been configured is reported with the fixed Edition-to-backend default and state NOT_DECLARED rather than omitted. An unresolvable licence does NOT fail this read: the response is 200 with licenseState = UNRESOLVED, which is distinct from NOT_IMPORTED (the licence server positively reporting that no licence is installed). No tenant identifier and no credential, PIN or PIN path is ever returned. Required scope: admin.platform.key-backend.tier.manage. - [Re-drive the platform's Redis projections from the authoritative database](https://apidocs.ankatech.co/reference/republishplatformprojection.md): Operator lever that re-publishes the capability-grant, tenant-status, role-scope, KEK-label, policy-cache and settings projections. The re-drive runs off the request thread (202); a re-drive already in progress coalesces (409); a Redis outage returns 503. - [Get tenant TSA usage policy](https://apidocs.ankatech.co/reference/gettenanttsausagepolicy.md): Returns the tenant's TSA usage policy. With includeInherited=true (default) the EFFECTIVE policy is resolved under the deployment ceiling/floor (deployment DISABLED/REQUIRED pins it; deployment OPTIONAL yields REQUIRED iff the tenant selected REQUIRED, else DISABLED). With includeInherited=false only the tenant's own remembered selection is returned (or DISABLED when none is assigned), ignoring the deployment default.

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.

- [Set tenant TSA usage policy override](https://apidocs.ankatech.co/reference/replacetenanttsausagepolicy.md): Full replacement (PUT) of the tenant's TSA usage override. Only the usage mode is read. The tenant tier accepts REQUIRED or DISABLED only: OPTIONAL is a deployment-tier concept and is rejected with 400. An unknown mode is rejected with 400. The response carries the EFFECTIVE mode after the deployment ceiling/floor is applied. - [Remove tenant TSA usage override](https://apidocs.ankatech.co/reference/resettenanttsausagepolicy.md): Removes the tenant's TSA usage override so it reverts to the inherited deployment default (or the code DEFAULT DISABLED). - [Grant a tenant's observability-export plan-gate entitlement (platform)](https://apidocs.ankatech.co/reference/createplatformobservabilityentitlement.md): Sets enabled=true on the tenant's observability-export entitlement row (upserting when absent). SaaS-only; the ROOT tenant is not a valid target. Required scope: admin.platform.observability-backend.tenant.entitlement.manage. - [Revoke a tenant's observability-export plan-gate entitlement (platform)](https://apidocs.ankatech.co/reference/deleteplatformobservabilityentitlement.md): Clears enabled on the tenant's observability-export entitlement row, blocking new declares. Does NOT delete or disable an existing destination (don't-strand). SaaS-only; the ROOT tenant is not a valid target. Required scope: admin.platform.observability-backend.tenant.entitlement.manage. - [Suspend a Cryptographic Actor](https://apidocs.ankatech.co/reference/suspendactor.md): Transitions an ACTIVE actor to SUSPENDED (reversible). Blocks the actor's M2M authentication, crypto operations, and scope-gated reads. Requires scope admin.tenant.actor.lifecycle.suspend. - [Reactivate a suspended Cryptographic Actor](https://apidocs.ankatech.co/reference/reactivateactor.md): Transitions a SUSPENDED actor back to ACTIVE without resetting any grant/constraint counter. Requires scope admin.tenant.actor.lifecycle.reactivate. - [Initialize PQC handshake](https://apidocs.ankatech.co/reference/initializehandshake.md): Receives the client's Base64-encoded PQC public key and the desired KEM algorithm name. The server will: 1) Generate a random AES-256 key. 2) Encapsulate it using the client's public key with the specified algorithm. 3) Store the AES key under a sessionId. 4) Return ciphertextBase64 and sessionId in the response. - [List supported KEM algorithms](https://apidocs.ankatech.co/reference/getsupportedalgorithms-2.md): Returns the list of KEM algorithms available for PQC handshake on this deployment. Experimental algorithms are only included when pqc.handshake.allow-experimental-kem is true. - [Timeline of operations under a specific Exchange Context](https://apidocs.ankatech.co/reference/listexchangecontextcryptoevents.md): Returns every crypto operation scoped to the supplied exchange context, ordered by event timestamp DESC. Backed by `idx_crypto_audit_exchange_context_id`. - [Timeline of operations involving a specific counterparty](https://apidocs.ankatech.co/reference/listcounterpartycryptoevents.md): Partner-centric view: returns every operation whose counterparty field matches the supplied name exactly. Backed by `idx_crypto_audit_counterparty`. Useful for partner billing reconciliation and counterparty-specific SLA reporting. - [Denied operations attributable to a constraint-policy violation](https://apidocs.ankatech.co/reference/listconstraintviolations.md): Returns every event with `status=DENIED` and a non-null `constraint_policy_id`. Use for compliance / forensic review of constraint-policy enforcement (max_usage exceeded, rate-limit tripped, single-use re-invocation, out-of-validity-window). - [Timeline of operations by a specific Cryptographic Actor](https://apidocs.ankatech.co/reference/listactorcryptoevents.md): Returns every crypto operation attributed to the supplied actor, ordered by event timestamp DESC. Uses the partial index `idx_crypto_audit_actor_id` plus HASH-partition pruning by tenant. - [Get meta-audit statistics for a specific tenant](https://apidocs.ankatech.co/reference/gettenantmetastats.md): Returns statistics about meta-audit access for a tenant: total access count, top users, top endpoints. Tenant admins can only view their own tenant; platform admins can view any tenant.

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.

It is still called 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.

- [Get meta-audit logs for a specific tenant](https://apidocs.ankatech.co/reference/gettenantaccesslogs.md): Returns meta-audit logs for a tenant: the reads performed BY this tenant's principals, when, and how much came back. Tenant admins can only view their own tenant; platform admins can view any tenant.

This operation selects the READER axis, not the subject axis. It returns the rows whose 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.

An impersonated read names the operating human. 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.

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.

- [Rank the login identities that read the audit trail most in a period](https://apidocs.ankatech.co/reference/gettopusers.md): Ranks the READER axis: which login identities queried the audit trail most within the period.

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. 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.

- [Rank the READER tenants that queried the audit trail most in a period](https://apidocs.ankatech.co/reference/gettoptenants.md): Ranks the READER axis by tenant: which tenants' principals performed the most audit reads within the period.

This does NOT answer "whose audit trail was read". That is the subject axis, and it has its own operation — 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.

The operationId and the ranking itself are unchanged — only the label is. The defect was the label.

- [Rank the tenants whose audit trail was read most in a period](https://apidocs.ankatech.co/reference/gettopsubjecttenants.md): Ranks the SUBJECT axis: whose audit trail was read, within the period. It is the aggregate form of the question the meta plane exists to answer, and until now no surface could ask it.

A deliberately separate operation from 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.

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.

- [Rank the audit endpoints queried most in a period](https://apidocs.ankatech.co/reference/gettopendpoints.md): Ranks the normalized audit endpoints by how often they were queried within the period.

The endpoint values are exactly the vocabulary the explorer's resource filter accepts, so a click on a ranked row navigates to the contributing reads with nothing re-derived by the client.

- [Derive the four monitoring signals for a window](https://apidocs.ankatech.co/reference/getmetaauditsignals.md): Returns the four derived signals for the window, each stating separately whether it could be measured at all and what it found.

The two are independent, and that is the point. 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.

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. 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

A signal whose population IS one row predicate carries that predicate in 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.

The contributor list is BOUNDED and 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.

Every contributor filter is expressed in getAllAccessLogs's own declared parameters, so it can be replayed there verbatim.

Every derivation travels as three fields, always all three. 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.

- [Attest a period of the meta-audit trail](https://apidocs.ankatech.co/reference/getmetaauditattestation.md): Returns the period, how many meta-audit rows it holds, and the standing chain verdict for the META plane.

The verdict is PROJECTED, never recomputed. The integrity subsystem already verifies this plane's WORM chain and publishes the result under 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.

Three states, not two. Besides 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.

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 audit.meta.read scope defensible.

The verdict names its populations. 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.

- [Investigate audit access along the reader and subject axes](https://apidocs.ankatech.co/reference/getallaccesslogs.md): Returns meta-audit entries — who read the audit trail, when, from where, whose trail they read, and what came back. Platform admins only.

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:

  • Reader axisreaderTenantId, readerUsername, ipAddress: who performed the read.
  • Subject axissubjectTenantId, 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.

The 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.

An oversized size is REFUSED, not silently reduced — a clamped page answers 200 with a result the caller did not ask for.

The period export

Send 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.

Cells beginning =, +, -, @, 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 all events by correlation ID](https://apidocs.ankatech.co/reference/getalleventsbycorrelation.md): Returns the crypto, admin and auth events associated with a specific correlation ID. SCOPE OF THE RESULT SET: a caller holding audit.platform.all.read - the single platform scope this endpoint's own gate names, the narrowest of the three correlation gates - receives every tenant's rows on all three arrays. Every other caller, including a holder of audit.platform.crypto.read or audit.platform.admin.read, receives the rows belonging to its OWN tenant only, on all three arrays (PRD 230). Provides a complete view of a distributed transaction across all microservices. PERFORMANCE WARNING: Scans all database partitions. - [Get crypto events by correlation ID](https://apidocs.ankatech.co/reference/getcryptoeventsbycorrelation.md): Returns the cryptographic operations associated with a specific correlation ID. SCOPE OF THE RESULT SET: a caller holding audit.platform.crypto.read or audit.platform.all.read - the platform scopes this endpoint's own gate names - receives every tenant's rows for the id, which is the cross-tenant timeline this endpoint exists for. Every other caller, including a holder of audit.correlation.read or of some other audit.platform.* scope, receives the rows belonging to its OWN tenant only (PRD 230). Useful for tracing a distributed transaction across multiple services. PERFORMANCE WARNING: Scans all database partitions. - [Get admin events by correlation ID](https://apidocs.ankatech.co/reference/getadmineventsbycorrelation.md): Returns the administrative operations associated with a specific correlation ID. SCOPE OF THE RESULT SET: a caller holding audit.platform.admin.read or audit.platform.all.read - the platform scopes this endpoint's own gate names - receives every tenant's rows for the id. Every other caller, including a holder of audit.correlation.read or of some other audit.platform.* scope, receives the rows belonging to its OWN tenant only, with platform-plane rows excluded (PRD 230). Useful for tracing user actions across multiple microservices. - [Support-access sessions held against this tenant](https://apidocs.ankatech.co/reference/listsupportaccesssessions.md): Returns every ANKATech support-access (impersonation) session whose start boundary falls inside the requested window, most recent first — with the operator, the window, why the session ended, and the FULL list of actions taken inside it. **Derived entirely from the signed audit events**, never from the grant or session rows admin-api holds. The session row carries `ON DELETE CASCADE` on the grant, revoke deletes the grant and the reconciler purges lapsed ones — so a row-based view would be empty at exactly the two moments this view exists to serve. The signed `support-access.session.started` / `.session.ended` events in `admin_audit_log` survive both, so this history remains readable after the grant is revoked and purged. A session is one `.session.started` paired with the next `.session.ended` for the same operator. An unpaired start is an **open** session and is rendered as such (`open: true`, no `endedAt`, no `endCause`) — it is not an omission. An end boundary whose start lies before the requested window is omitted; widen `from` to include it. Two operators inside one grant are two entries, because the grouping key includes the operator. The session JTI is never projected — it is a live revocation key and is not a value this service can reach. The window is always bounded: omitting `from` / `to` applies a 90-day lookback ending now rather than scanning all history. The maximum span is 365 days. The action list is bounded too, and independently: the time window caps how far back the view reaches, not how many rows sit inside it, and one scripted or bulk operation performed during a session produces arbitrarily many. Each session therefore projects at most **500** actions, oldest first, and sets `actionsTruncated: true` when it held more. The truncation is never silent — reading a cut list as though it were complete is the one misreading this view cannot afford. - [Get unified statistics](https://apidocs.ankatech.co/reference/getunifiedstats.md): Returns combined statistics for both crypto and admin operations. - [List this tenant's administrative activity for one integration](https://apidocs.ankatech.co/reference/listtenantintegrationauditevents.md): Returns every administrative row this tenant's use of an integration wrote, newest first.

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 GET /api/v3/audit/vocabulary; do not author one.

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.

- [List governance actions taken on this tenant](https://apidocs.ankatech.co/reference/listtenantgovernanceevents.md): Returns the administrative actions taken ON this tenant — the platform-operator changes a tenant could not previously see, filed under the tenant they act on. **Which planes are visible is the deployment's decision, not the caller's.** On a customer-operated deployment (private cloud, on premise) the operator is the customer's own staff, so both planes are returned. On SaaS, PLATFORM-plane rows are withheld. The optional `actorPlane` parameter can only NARROW that set — it can never widen it, so requesting `PLATFORM` on SaaS returns an empty page rather than an error or a disclosure. - [Unified tenant audit events (crypto + admin + auth, server-merged)](https://apidocs.ankatech.co/reference/getunifiedevents.md): Returns one keyset page of the crypto, admin and authentication audit events for a tenant, ordered server-side by (event_timestamp DESC, event_source, log_id DESC). Replaces client-side merge of independent streams. The stream is narrowed to the event sources the caller's scopes admit, and the set it was narrowed to is returned in `admittedSources`: `audit.*.crypto.read` admits crypto, `audit.*.admin.read` admits admin, `audit.*.auth.read` admits auth, and `audit.*.all.read` admits all three. A caller admitted to no source receives 403, never an empty page. The stream can be narrowed further, WITHIN what the scopes admit: `source` selects one trail, `operationType` narrows the crypto branch and `entityType` / `action` narrow the admin branch. A filter never widens the admitted set — a `source` the caller may not read is answered 403, exactly as an empty admitted set is. A dimension no admitted branch carries yields an EMPTY page rather than an error or an unfiltered one, and a value outside the projected vocabulary is answered 400 rather than silently ignored. Pagination is by opaque cursor: pass back `nextCursor` to continue, and read `hasMore` to learn whether more rows exist. There is no offset ceiling and no sort parameter — a cursor is valid only for the ordering it encodes. A cursor replayed against a different window or filter set is rejected with 400. The applied window and the maximum this feed accepts are stated in `appliedRange`. Crypto rows carry the key's `materialVersion` (generation) and resolve the actor as COALESCE(user_id, actor_id) so actor-driven operations surface their identity; admin and auth rows have a null materialVersion. - [Export the unified tenant audit stream to CSV](https://apidocs.ankatech.co/reference/exporttenantevents.md): Streams the rows GET /api/v3/audit/tenants/{tenantId}/events would serve for the same parameters - the same window, the same filters, the same admitted sources and the same (event_timestamp DESC, event_source, log_id DESC) ordering - as one RFC 4180 CSV. startTime and endTime are REQUIRED: an export is a bulk read and it is bounded by a window the caller states. It takes the SAME source and dimension filters as the feed, and applies them the same way: a file exported from a tab holds that tab's rows, because "the export emits exactly the feed's rows" is only true when both are built from one parameter set by one query builder. There is no cursor parameter. An export is not paged, and silently ignoring a cursor would emit a different row set than the caller believes they asked for, so passing one is answered 400. The response carries X-Export-Row-Ceiling before the first row, and X-Export-Rows and X-Export-Truncated after the last. An ABSENT header means UNKNOWN, never complete: a container discards a header set on a committed response. The file's own last record is the durable answer. The `details` column of the feed is deliberately NOT exported. - [Get crypto operations timeline for tenant](https://apidocs.ankatech.co/reference/getcryptotimeline.md): Returns all cryptographic operations for the tenant in reverse chronological order. Includes encryption, decryption, signing, verification, key generation, and key wrapping operations. Optionally filter by time range using startTime and endTime parameters. If not provided, returns all events for the tenant. - [Get crypto statistics](https://apidocs.ankatech.co/reference/getcryptostats.md): Returns aggregate statistics for cryptographic operations (total events, failed operations, etc.). Optional ISO-8601 startTime/endTime bound the aggregate to a closed [startTime, endTime] window; omitting both yields the full historic aggregate. - [Get tenant crypto time-series data](https://apidocs.ankatech.co/reference/gettenantcryptotimeseries.md): Returns aggregated crypto operations by day for tenant dashboard charts. - [Daily PQC vs Classical crypto stats for tenant](https://apidocs.ankatech.co/reference/getdailycryptostats.md): Returns per-day aggregated cryptographic operation counts split by algorithm family (PQC vs Classical) for the requested date range. Sparse output: days with zero events are omitted. The range cap is 365 days. PQC families: ML-KEM, ML-DSA, SLH-DSA, FALCON. All other algorithms are classified as Classical. Reads from `crypto_audit_log` (HASH-partitioned by tenant_id) — always supply `tenantId` so PostgreSQL can prune to the correct partition. - [Filter by operation type](https://apidocs.ankatech.co/reference/getoperationsbytype.md): Returns all operations of a specific type (ENCRYPT, DECRYPT, SIGN, VERIFY, etc.). Useful for analyzing specific cryptographic operation patterns. - [Get Stable KID lineage history](https://apidocs.ankatech.co/reference/getkeyhistory.md): Returns cryptographic operations performed under a specific Stable KID (`keyId` = identity-layer alias). By default the response aggregates the FULL material lineage — every rotation generation of the alias — most-recent-first, with each entry carrying its `materialVersion` (the `ank_kv` of the key material in force at operation time). Pass the optional `materialVersion` query parameter to narrow the history to a single rotation generation. Useful for key lifecycle tracking, rotation forensics, and compliance audits. - [Query crypto events](https://apidocs.ankatech.co/reference/getcryptoevents.md): Returns cryptographic operations filtered by time range. Timestamps must be in ISO 8601 format (e.g., 2025-01-15T10:00:00Z). If no time range is provided, returns all crypto events for the tenant. - [Get client operations history](https://apidocs.ankatech.co/reference/getclienthistory.md): Returns all cryptographic operations performed by a specific client application. Useful for debugging client issues and security investigations. - [List authentication audit events for tenant](https://apidocs.ankatech.co/reference/gettenantauthevents.md): Returns paginated authentication audit events for the tenant with optional filtering by principal, status, error code, source IP, operation type, event type pattern and time range. The applied window and the maximum this feed accepts are stated on every accepted response in `appliedRange`; this description asserts neither. - [Get a single authentication audit event by ID](https://apidocs.ankatech.co/reference/gettenantauthevent.md): Returns the authentication audit event with the given {eventId} as long as it belongs to the caller's tenant. Returns 404 when the event does not exist within that tenant. - [Get authentication audit statistics for tenant](https://apidocs.ankatech.co/reference/gettenantauthstats.md): Returns aggregated auth statistics for the tenant: success/failure counts, failure rate, top 10 failed principals and top 10 source IPs, over the window the request resolved to. This description asserts no maximum: the rule is one constant, applied by every audit feed. - [Get administrative operations timeline](https://apidocs.ankatech.co/reference/getadmintimeline.md): Returns all administrative operations for the tenant in reverse chronological order. Includes user management, policy changes, tenant configuration, etc. Optionally filter by time range using startTime and endTime parameters. If not provided, returns all events for the tenant. - [Get admin statistics](https://apidocs.ankatech.co/reference/getadminstats.md): Returns aggregate statistics for administrative operations (total events, events by actor, etc.). - [Get tenant admin time-series data](https://apidocs.ankatech.co/reference/gettenantadmintimeseries.md): Returns aggregated admin operations by day for tenant dashboard charts. - [Query admin events](https://apidocs.ankatech.co/reference/getadminevents.md): Returns administrative events filtered by event type pattern or time range. Use eventTypePattern for filtering (e.g., 'admin.humanuser.%' for all user events). Use startTime/endTime for time range queries. - [Get entity change history](https://apidocs.ankatech.co/reference/getentityhistory.md): Returns all administrative operations affecting a specific entity (e.g., all changes to a tenant, user, or policy). Useful for auditing resource lifecycle. - [Get actor history](https://apidocs.ankatech.co/reference/getactorhistory.md): Returns all administrative operations performed by a specific user or application. Useful for compliance audits and security investigations. - [Get the audit filter vocabulary for this caller's plane](https://apidocs.ankatech.co/reference/getauditfiltervocabulary.md): Returns the entity types, actions, statuses, crypto operation types and integration surfaces the audit queries on THIS caller's plane accept, so a client never authors a filter value the server would reject or answer with a permanently empty page.

The projection is plane-scoped. A caller holding any 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.

It is also source-scoped. Each dimension belongs to an event source and is projected only when the caller's scopes admit that source: 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.

Every offered value is accepted by this plane's queries; the reverse does not hold, and deliberately so.

- [Verify one tenant audit chain at rest](https://apidocs.ankatech.co/reference/verifytenantauditintegrity.md): Walks the tenant's hash chain on one audit plane and reports what it finds. Every row is re-checked from its own persisted columns: the envelope HMAC still reproduces, the sequence has no gap and no repeat, each row links to its predecessor, and the keyed row hash recomputes. **The verdict is derived on every call.** No result is cached — a stale "verified OK" is precisely the assurance this endpoint exists to replace. The walk is paged and budgeted. A run that stops at its budget reports `complete: false` with the window it did cover; call again to continue from where it stopped. Pass `fromGenesis=true` to re-prove the chain from its origin regardless of any recorded checkpoint. A resumed run re-reads the row its predecessor stopped at **before** it skips anything, so a row altered after it was verified is still caught. - [Start a platform-wide audit-integrity verification](https://apidocs.ankatech.co/reference/verifyplatformauditintegrity.md): Starts a budgeted walk over every discovered `(tenant, plane)` chain and returns immediately with a run to poll. **It is asynchronous because the alternative is an outage.** A synchronous platform-wide walk is a scan of every partition of an unbounded WORM table held open by an HTTP request. **It is single-flight.** A request made while a platform run is already walking joins that run and returns its identifier rather than starting a second one, so the endpoint cannot be used as a load-generation primitive by anyone holding the scope. Progress is readable at `GET /platform/integrity/runs/{runId}`, whose row counter advances monotonically — a stalled run reads as a frozen counter rather than as silence. - [Read a tenant's audit-chain trust state, across its planes or on one of them](https://apidocs.ankatech.co/reference/gettenantauditintegrityverdict.md): Returns what the most recent run over this tenant's chains found, together with how far it reached. **`plane` is optional.** Omit it for the aggregate across all five planes; supply it to address exactly one. Both forms answer the same shape: the same verdict, the same coverage fields, and a `byScope[]` naming exactly the planes the verdict speaks for. The single-plane form additionally carries that chain's full run record in `lastRun`. **The aggregate is decided here, and it never upgrades a verdict.** One BROKEN plane makes the aggregate BROKEN, however many intact planes sit beside it — an aggregate that could be outvoted gets quieter the more chains a deployment has. A plane with **no run record** shows on the coverage axis and never on the verdict axis: a tenant with no PQC rows has no PQC run, and reporting that as "not yet verified" forever is a false alarm, not a finding. **Coverage is `scopesTotal` of `chainsKnown`.** `chainsKnown` is how many chains this tenant actually HAS — measured against the audit tables, not the number of planes that could exist — so a tenant with no PQC handshake rows has four chains and reads "4 of 4" when all four are walked, never a "4 of 5" it can never close. "3 of 4" says one existing chain has never been walked, and `byScope` names the three the verdict speaks for. There is no completeness flag: the one that used to sit here compared the answered set with itself and was therefore affirmative unconditionally. Unlike the platform surface, this denominator is exact and current — it is measured by the read itself — so it carries no measurement timestamp. **Coverage is reported, not folded away.** `retentionTruncated` is true when ANY addressed chain's prefix was lawfully aged out under a valid SIGNED watermark, and `coverageFrom` is the earliest instant from which EVERY addressed chain is actually covered — the latest of the per-chain watermarks, because any earlier value would claim a window some chain no longer holds. **This is history, not a verdict about now.** A chain that has never been verified answers `everVerified: false` with no run attached — deliberately not `INTACT`, because "nobody has looked" and "we looked and it was sound" are different facts and only one of them is assurance. The tenant plane is read-only by design: starting a run requires a verify scope. - [List verification runs, newest first](https://apidocs.ankatech.co/reference/listplatformauditintegrityruns.md): The paged history of every verification run. It is a functional requirement rather than a convenience: it is the only way to see that scheduled runs have stopped arriving. A job that is silently disabled raises no alarm of its own, and a by-id read cannot show an absence. - [Read one verification run](https://apidocs.ankatech.co/reference/getplatformauditintegrityrun.md): Returns one recorded run: its status, its verdict, the window it covered and how many rows it has walked so far. `rowsVerified` advances monotonically while a run is in flight, which is what makes a stalled run visible. A control that fails silently is worse than no control, because its absence is indistinguishable from its success. - [Read the platform-wide trust state](https://apidocs.ankatech.co/reference/getplatformauditintegrityverdict.md): Returns the aggregate verdict across every chain that has a recorded run, plus the per-chain verdicts it is computed from. **One read serves both.** Splitting the headline from its breakdown would put them on two clocks, so a console could render "all chains sound" beside a row saying otherwise with no way to tell which was stale. **Coverage is aggregated, never dropped.** `retentionTruncated` is true when ANY contributing chain's prefix was lawfully aged out under a valid SIGNED watermark, and `coverageFrom` is the earliest instant from which EVERY contributing chain is covered — the latest of the per-chain watermarks. Without them a platform whose coverage had been truncated would read as plainly intact, which is the false assurance this feature exists to remove. **Coverage is a MEASURE, and there is deliberately no completeness flag.** `scopesTotal` is how many chains this verdict speaks for, `chainsKnown` is how many exist as counted over the last two enumeration traversals, and `chainsKnownAt` is when that count was taken — read them together ("37 of 41, measured 6 hours ago"). They are different populations measured at different times: `scopesTotal` counts every chain with any recorded run and grows continuously, so `scopesTotal >= chainsKnown` is satisfied by growth as readily as by coverage and is NOT a completeness proof. A flag derived from that comparison shipped once and failed open; it is gone rather than re-tuned. `chainsKnown` is ABSENT — never 0 — when no trustworthy count is available: nothing has enumerated the estate, the enumeration spans more passes than the probe reads, or the run history this read reduced over no longer reaches back across it. Absent means "no coverage claim is available". `everVerified: false` is a first-class state and is NOT a pass. It is also the only way a silently disabled scheduled job becomes visible. - [Force a platform crypto stats snapshot refresh](https://apidocs.ankatech.co/reference/refreshplatformcryptostats.md): Triggers an immediate recomputation of the platform crypto statistics snapshot. Guarded by a 60-second cooldown per the latest snapshot age. The refreshed snapshot is returned in the response. - [Get unified platform statistics](https://apidocs.ankatech.co/reference/getunifiedplatformstats.md): Returns combined statistics for crypto, admin, and auth operations across all tenants (or filtered by tenant list). Includes KPI fields for the Global Audit Log header: todayEvents, securityAlerts, failedOps, uniqueActors, and per-subsystem breakdowns. Auth events (including pre-authentication null-tenant rows) are included in totalEvents, securityAlerts, and todayEvents when tenantIds is omitted. Updated to include auth subsystem: 2026-04-12. - [List the administrative activity of one integration](https://apidocs.ankatech.co/reference/listplatformintegrationauditevents.md): Returns every administrative row an integration's administration wrote, across tenants, newest first.

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 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.

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.

- [Unified platform audit events (crypto + admin + auth, server-merged across tenants)](https://apidocs.ankatech.co/reference/getunifiedplatformevents.md): Returns one keyset page of the merged crypto, admin and authentication audit stream across every tenant, ordered server-side by (event_timestamp DESC, event_source, log_id DESC) and including platform-plane rows. Each row carries its tenant identifier. The stream is narrowed to the event sources the caller's scopes admit, and the set it was narrowed to is returned in `admittedSources`: `audit.platform.crypto.read` admits crypto, `audit.platform.admin.read` admits admin, `audit.platform.auth.read` admits auth, and `audit.platform.all.read` admits all three. A caller admitted to no source receives 403, never an empty page. Platform-tenant (ROOT) authentication is required in addition to the scope. Pagination is by opaque cursor: pass back `nextCursor` to continue, and read `hasMore` to learn whether more rows exist. There is no offset ceiling and no sort parameter - a cursor is valid only for the ordering it encodes. `tenantIds` filters within what the caller may already read; omitting it means every tenant. The applied window and the maximum this feed accepts are stated in `appliedRange`, and the scope-bar figures for that same window, filter set and source set in `scopeBar`. - [Export the unified platform audit stream to CSV](https://apidocs.ankatech.co/reference/exportplatformevents.md): Streams the rows GET /api/v3/audit/platform/events would serve for the same parameters - the same window, the same filters, the same admitted sources and the same (event_timestamp DESC, event_source, log_id DESC) ordering - as one RFC 4180 CSV. startTime and endTime are REQUIRED: an export is a bulk read and it is bounded by a window the caller states. It takes the SAME source and dimension filters as the feed, and applies them the same way: a file exported from a tab holds that tab's rows, because "the export emits exactly the feed's rows" is only true when both are built from one parameter set by one query builder. There is no cursor parameter. An export is not paged, and silently ignoring a cursor would emit a different row set than the caller believes they asked for, so passing one is answered 400. The response carries X-Export-Row-Ceiling before the first row, and X-Export-Rows and X-Export-Truncated after the last. An ABSENT header means UNKNOWN, never complete: a container discards a header set on a committed response. The file's own last record is the durable answer. The `details` column of the feed is deliberately NOT exported. - [Get platform-wide crypto operations timeline](https://apidocs.ankatech.co/reference/getplatformcryptotimeline.md): Returns cryptographic 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. PERFORMANCE WARNING: Scans multiple database partitions, use with care. - [Get platform-wide crypto statistics](https://apidocs.ankatech.co/reference/getplatformcryptostats.md): Returns aggregate statistics served from a pre-computed snapshot (refreshed every 5 minutes). Use POST /crypto/stats/refresh to force an immediate refresh. The response header X-Stats-Snapshot-Age-Seconds indicates how many seconds ago the snapshot was computed. The counters cover every retained crypto audit event: the aggregation applies no time predicate, so 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.