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, orencrypted_keyfor a key-wrapping algorithm; adirrecipient carries noencrypted_key, per RFC 7516 §7.2.1), or it carries acritmember outsideprotected(RFC 7516 §4.1.13) or an unrecognisedcritextension. -
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 itsprotectedsection orkidfield, 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, thedetailnames the index of the first failing signer. -
Streaming encryption or signing refused
On/api/v3/crypto/stream/encrypt,/stream/signand/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'sexphas passed, itsiatis in the future or older than the deployment's maximum age, itsnbfis not yet reached, or itsexpis not after itsiat: on/api/v3/crypto/verify, on the inner JWS of/decrypt-verifyand on the source of/resignand/stream/resign. On/stream/verifya missingiat(when the deployment requires one) and a malformed claim are also422, where the buffered routes answer400. The rule and the status are the same for SIMPLE and composite signatures;detailnames 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
- Validate token syntax
- Ensure your JWE/JWS strings exactly match the expected RFC formats.
- Check header JSON
- Decode the Base64 header and confirm all required fields (
kid,alg,iv, etc.) are present.
- Decode the Base64 header and confirm all required fields (
- Confirm cryptographic parameters
- IV length must be 12 bytes for AES-GCM; tags must be 16 bytes.
- Use correct key material
- Make sure you’re using the same
kidand algorithm that encrypted or signed the data.
- Make sure you’re using the same
- 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.