Skip to content

Unprocessable Entity

URI: https://docs.ankatech.co/errors/unprocessable-entity
HTTP Status: 422 Unprocessable Entity

When you see this

The server understood your request and the syntax was correct, but it could not process the content because of semantic or integrity issues.

Common Causes

  • Malformed JWE header
    The flattened JWE JSON is invalid or missing required fields (protected, iv, recipients, or encrypted_key for a key-wrapping algorithm; a dir recipient carries no encrypted_key, per RFC 7516 §7.2.1), or it carries a crit member outside protected (RFC 7516 §4.1.13) or an unrecognised crit extension.

  • Malformed Compact JWE
    The incoming JWE string cannot be parsed (e.g. does not conform to RFC 7516).

  • Malformed Compact JWS
    The incoming JWS string cannot be parsed (e.g. does not conform to RFC 7515).

  • Detached JWS header validation failure
    The detached-JWS JSON is missing its protected section or kid field, or isn’t valid Base64-encoded JSON.

  • Ciphertext integrity failure
    AES-GCM/CCM tag mismatch or asymmetric unwrap padding error (OAEP/PKCS#1) during decrypt/unwrapping.

  • A PKCS#7 signer whose CMS signature does not verify
    On the PKCS#7 conversion, the detail names the index of the first failing signer.

  • Streaming encryption or signing refused
    On /api/v3/crypto/stream/encrypt, /stream/sign and /stream/sign-encrypt, the key's cipher or the signing engine refused the operation. These routes read no ciphertext, so this is never an integrity failure. The body is written with zero payload bytes while the response is not yet committed; after commit the stream is truncated instead.

  • JWS time claim refused
    The signature's exp has passed, its iat is in the future or older than the deployment's maximum age, its nbf is not yet reached, or its exp is not after its iat: on /api/v3/crypto/verify, on the inner JWS of /decrypt-verify and on the source of /resign and /stream/resign. On /stream/verify a missing iat (when the deployment requires one) and a malformed claim are also 422, where the buffered routes answer 400. The rule and the status are the same for SIMPLE and composite signatures; detail names the failed check and echoes no claim value or server time. See Timestamp Integrity*.

Core API conditions named by extensions.errorCode

Some Core API refusals of this type carry an extensions.errorCode, so a client can branch without parsing the detail. The codes for a token addressed to or signed by several keys are key-neutral: no kid, no algorithm and no count appears in the code or in its detail.

errorCode Where Condition
AMBIGUOUS_ACTOR_KEY PKCS#7 conversion, both endpoints A signer's or recipient's certificate is held under more than one tenant key admissible for its role (for example after importing the same keystore twice), so the actor cannot be resolved to a single key. The detail names the actor's index, issuer and serial, never a kid. Remove the duplicate import and retry.
JWE_RECIPIENT_KID_MISSING Crypto operations that read a JWE, both planes A recipient of a JWE addressed to several keys carries no kid in its header: every recipient of such a JWE names its key (RFC 7516 §7.2.1). Decided from the headers, before any authorization or lifecycle check.
JWE_SHARED_KID_NOT_COMPOSITE Crypto operations that read a JWE, both planes The JWE names a shared key in its protected header, but its recipients are not the components of a composite key it can be read with — decided on the resolved key, never on a header member.
JOSE_ACTOR_KID_REPEATED Crypto operations, both planes One key is named by more than one entry of the token (two recipients, or two signatures): every entry of a multi-key token names a distinct key. Decided from the headers.
JOSE_SYMMETRIC_ACTOR Crypto operations, both planes An entry of a multi-key token uses a symmetric or direct algorithm (HS*, A*KW, A*GCMKW, PBES2-*, dir, ECDH-ES), or an entry, newKid or a kept recipient resolves to a symmetric key (refused after its own gate): a token addressed to or signed by several keys admits asymmetric key management and signatures only.
JOSE_ACTOR_LIMIT_EXCEEDED Crypto operations, both planes The token names more keys than the crypto plane admits in one token (ankasecure.crypto.jose.max-actors, default 16). Decided from the headers.
JWE_COMPOSITE_RECIPIENT Crypto operations that read a JWE, both planes A recipient of a JWE addressed to several keys is a composite key — the one a decryption reads with, or the replaced recipient, newKid or a kept recipient of a re-encryption: a composite key is read from and written to its own composite JWE, never as one recipient among several. Decided after that key's own authorization and lifecycle gates.
JWS_ROOT_MEMBER_FORBIDDEN Crypto operations that read a JWS, both planes The JWS carries a member at its root besides payload and signatures (on the detached plane, besides signatures): the General JSON syntax carries each signature's headers and value inside signatures (RFC 7515 §7.2.1). A root protected or signature is refused on any number of signatures. Decided where the body is bound.
JWE_ROOT_MEMBER_FORBIDDEN Crypto operations that read a JWE, both planes The JWE carries a member at its root besides protected, recipients, iv, ciphertext and tag — a JSON aad member, a shared unprotected header, or any name the platform does not define. On a streaming route the detached header admits anka_composite_mode beside them, refuses a ciphertext value (the ciphertext travels in the body; the member is absent or null) and accepts a tag. RFC 7516 §7.2.1 allows aad and unprotected; this platform reads neither, so a member it would not read is refused rather than dropped. Decided on every route and every shape (one key, a composite key, several keys), before any grant or lifecycle decision and before any key is used.
JWE_RECIPIENT_MEMBER_FORBIDDEN Crypto operations that read a JWE, both planes A recipient of the JWE carries a member besides header and encrypted_key (RFC 7516 §7.2.1). Decided like JWE_ROOT_MEMBER_FORBIDDEN: on every route and every shape, before any grant or key use.
JWE_RECIPIENT_SET_UNPROTECTED /api/v3/crypto/reencrypt, /stream/reencrypt The recipients of a JWE addressed to several keys are not exactly the recipient set its protected header records (ank_kvs). A recipient entry the protected header does not name is covered by no tag, so the content is never re-encrypted to it; a JWE whose protected header records no set is refused the same way. Decided before any key is gated. On the streaming route a tampered ank_kvs itself fails the source AES-GCM tag at end-of-stream: the output is left without its tag and the verdict is INVALID.
JWS_SIGNATURE_MISSING Crypto operations that read a JWS The JWS carries no signature at all: an empty signatures array, or no or an empty signature. No key is resolved.
JWS_SINGLE_SIGNATURE_REQUIRED /api/v3/interoperability/verify The JWS carries several signatures, and this operation is given ONE caller-supplied key: it verifies one signature. Refused before any signature is verified.
JWE_ALG_KEY_MISMATCH /api/v3/crypto/decrypt, /reencrypt, /decrypt-verify and their /stream/ counterparts A one-recipient JWE whose alg sits only in its unprotected recipient header (the General layout of RFC 7516 §7.2.1, which the PKCS#7 conversion emits for serialization JSON) names an alg other than the key-management algorithm the platform uses for the key its kid resolves to (RSA-OAEP-512 for RSA, ECDH-ES+A256KW for EC and for X25519/X448, the family's own name for a KEM key). Decided after that key's grant and lifecycle decisions; on the streaming routes, before any body byte.
JWE_KEY_TRANSPORT_RETIRED Crypto operations that read a JWE, both planes A recipient on a simple X25519 or X448 key uses alg X25519 or X448. Such a key is read only as ECDH-ES+A256KW with an OKP epk (RFC 8037). A composite key's component recipient keeps its component alg and is not affected. Decided from the header, before any key agreement.
JWE_ALG_NOT_PROTECTED /api/v3/crypto/decrypt, /reencrypt, /decrypt-verify and their /stream/ counterparts That same layout addressed to a symmetric or composite key, whose alg must be in the protected header.

Response Example

{
  "type": "https://docs.ankatech.co/errors/unprocessable-entity",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "The ciphertext failed its AES-GCM integrity check.",
  "instance": "/api/v3/admin/tenants/2f1c9d84-6b2e-4d3a-9f57-0a1b2c3d4e5f/keys/my-mlkem-key",
  "timestamp": 1751500800,
  "extensions": {
    "requestId": "550e8400-e29b-41d4-a716-446655440000"
  }
}

Content-Type: application/problem+json — the response follows RFC 9457 Problem Details. The example above is an Admin API body, where the correlation identity is extensions.requestId; the Core API and the PQC Handshake API instead emit a top-level correlationId. Read the member your service emits — see Error Index & Overview.

Per-entry failures are a named member, never text inside detail

A keystore import in which every entry fails publishes those entries as the failedEntries extension member, so a client reads structured data rather than parsing a sentence. detail remains a single sentence, and it names how many entries this response carries and where to read them:

{
  "type": "https://docs.ankatech.co/errors/upstream-rejected-request",
  "title": "Upstream Rejected Request",
  "status": 422,
  "detail": "The key-management service rejected the request. This response carries 2 failed keystore entries under extensions.failedEntries.",
  "instance": "/api/v3/admin/tenants/2f1c9d84-6b2e-4d3a-9f57-0a1b2c3d4e5f/keys/import-keystore",
  "timestamp": 1751500800,
  "extensions": {
    "requestId": "550e8400-e29b-41d4-a716-446655440000",
    "failedEntries": [
      {
        "kid": "legacy-signing-key",
        "sourceAlias": "firma",
        "entryType": "PRIVATE_KEY",
        "failureReason": "CERTIFICATE_VALIDATION_FAILED",
        "errorMessage": "Certificate validation failed for kid 'legacy-signing-key'"
      },
      {
        "kid": "legacy-encryption-key",
        "sourceAlias": "cifrado",
        "entryType": "PRIVATE_KEY",
        "failureReason": "UNEXPECTED_ERROR",
        "errorMessage": "The entry could not be imported due to an internal error. The cause is recorded in the server log."
      }
    ]
  }
}

Each entry carries only the fields listed above (kid, entryType, sourceAlias, failureReason, errorMessage); the list is capped at 50 entries so the response stays bounded. Read it at response.extensions.failedEntries.

errorMessage is never a raw internal diagnostic. Where the failure is a typed domain outcome — a certificate that did not validate, for example — the sentence describes that outcome and names only identifiers you supplied. Where it is an unexpected internal fault, the sentence is a fixed curated one (shown verbatim in the second entry above) and the diagnosable cause is recorded server-side instead; correlate it through extensions.requestId or the X-Request-Id header when contacting support.

The count in detail describes this response, not the upstream rejection: it is the number of entries published under extensions.failedEntries, which the 50-entry cap may have bounded. The sentence reads entry in the singular when a single entry is published.

Why this example carries the upstream-rejected-request type

The keystore-import 422 originates in the internal key-management service, and that service's 422 body is a keystore-import result document rather than a problem document — it carries the per-entry failures array the failedEntries member is projected from, but no type and no detail. So type and title are the responding service's own, and detail is an admin-authored sentence counting the entries this response publishes. The "returned no usable problem description" wording of the degenerate branch is reserved for a rejection from which no entry could be extracted — using it here would deny the structure the same response carries. The rule that matters to a client is unchanged — read type to classify, read extensions.failedEntries for the per-entry data, and never parse detail.

How to Resolve

  1. Validate token syntax
    • Ensure your JWE/JWS strings exactly match the expected RFC formats.
  2. Check header JSON
    • Decode the Base64 header and confirm all required fields (kid, alg, iv, etc.) are present.
  3. Confirm cryptographic parameters
    • IV length must be 12 bytes for AES-GCM; tags must be 16 bytes.
  4. Use correct key material
    • Make sure you’re using the same kid and algorithm that encrypted or signed the data.
  5. Re-submit
    • After correcting any header or token issues, retry your request.

If you continue to receive a 422 error after these steps, please consult the full Developer Hub Reference.

* restricted content — see higher-tier documentation