Skip to main content

KEM API

The KEM (Key Encapsulation Mechanism) API provides post-quantum encryption and decryption using the ML-KEM-768 algorithm (FIPS 203; the API wire value is Kyber768). Qpher uses a hybrid KEM-DEM scheme: ML-KEM-768 encapsulates a shared secret, which is then used with HKDF-SHA256 and AES-256-GCM to encrypt your data.

Private keys never leave Qpher

All decryption operations happen inside the Qpher secure enclave. Your private keys are never exposed or exported.

X-Wing hybrid KEM (Starter+)

Pass "algorithm": "X-Wing" in encrypt/decrypt requests to use hybrid KEM (X25519 + ML-KEM-768). If omitted, the default is "Kyber768" (PQC-only, backward-compatible).

X-Wing provides defense-in-depth by combining the post-quantum security of ML-KEM-768 with the battle-tested classical security of X25519. This construction follows the IETF CFRG X-Wing draft (draft-connolly-cfrg-xwing-kem).

See Security Architecture for details.

ML-KEM-1024 — NIST Category 5 / CNSA 2.0 (Enterprise+)

Pass "algorithm": "ML-KEM-1024" to use the NIST Category 5 (highest-strength) ML-KEM parameter set — the KEM named in the NSA CNSA 2.0 suite. Most callers select it via the SDK compliance_profile="cnsa-2.0" selector (which also sends the X-Compliance-Profile: cnsa-2.0 header) rather than per call. This is algorithm alignment with CNSA 2.0, not a compliance certification.

key_version is optional on encrypt — pinning it requires the active version

Encrypt now lets you omit key_version to use your tenant's active key (the response returns the version that was used). If you pin a specific key_version, it must reference an active key — a new tenant's active key is usually higher than 1, so copying 1 verbatim returns ERR_NOT_FOUND_001. Find your active version with GET /api/v1/kms/keys/active?algorithm=Kyber768 (use algorithm=Dilithium3 for signing). Decrypt, encapsulate/decapsulate, and key wrap/unwrap still require an explicit key_version.


Encrypt​

POST/api/v1/kem/encryptEncrypt plaintext using ML-KEM-768 KEM-DEM hybrid encryption.

Encrypts the provided plaintext using the KEM-DEM scheme with the specified key version. The key must be in active status.

Request body​

FieldTypeRequiredDescription
plaintextstring (base64)YesBase64-encoded data to encrypt. Maximum size: 1 MB.
key_versionintegerNoThe version of the ML-KEM-768 key to use. Omit to use your tenant's active key (the response returns the version used). If provided, it must reference an active key.
algorithmstringNo"Kyber768" (default) or "X-Wing" (hybrid, Starter+). If omitted, defaults to ML-KEM-768.
modestringNoEncryption mode: "standard" (default) or "deterministic".
saltstring (base64)ConditionalBase64-encoded salt. Required when mode is "deterministic".
Deterministic mode

Deterministic encryption (mode: "deterministic") produces identical ciphertext for the same plaintext and salt. Use it only when you need equality checks on encrypted data. Standard mode is recommended for all other use cases.

Request size

plaintext is capped at ~1 MB (1,400,000 base64 characters). For larger data, don't send it through /kem/encrypt — use key-wrap (/kem/key/wrap) or the client-side encryption pattern (encapsulate + local AES-256-GCM) so only a small wrapped key / shared secret transits the API, not your data.

Response (200 OK)​

FieldTypeDescription
data.ciphertextstring (base64)The encrypted data, base64-encoded.
data.key_versionintegerThe key version used for encryption.
data.algorithmstring"Kyber768" or "X-Wing", matching the algorithm used for the operation.

Example​

RequestPOST/api/v1/kem/encrypt
X-API-Key: qph_your_key_here
Content-Type: application/json
{
  "plaintext": "SGVsbG8sIFdvcmxkIQ=="
}
Response200
{
  "data": {
    "ciphertext": "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5...",
    "key_version": 1,
    "algorithm": "Kyber768"
  },
  "request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2026-01-15T10:30:00.000Z"
}
Encrypt example
curl -X POST https://api.qpher.ai/api/v1/kem/encrypt \
  -H "X-API-Key: qph_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "plaintext": "SGVsbG8sIFdvcmxkIQ=="
  }'
key_version is optional

key_version is now optional on encrypt and sign — omit it to use your tenant's active key (the response returns the version that was used). To pin a specific version, pass key_version (it must reference an active key).

Decrypt and verify still require the exact key_version that was used to encrypt.

Errors​

HTTP StatusError CodeDescription
400ERR_KEM_001Invalid encryption request — missing plaintext, invalid key_version, or bad encoding.
401ERR_AUTH_001Missing or invalid API key.
404ERR_NOT_FOUND_001Key version not found or key is not in active status.
429ERR_RATE_LIMIT_001Rate limit exceeded for your plan tier.

Decrypt​

POST/api/v1/kem/decryptDecrypt ciphertext using ML-KEM-768 KEM-DEM hybrid decryption.

Decrypts ciphertext that was previously encrypted with the KEM encrypt endpoint. The referenced key must be in active or retired status.

Request body​

FieldTypeRequiredDescription
ciphertextstring (base64)YesBase64-encoded ciphertext returned by the encrypt endpoint.
key_versionintegerYesThe key version that was used to encrypt this data.
algorithmstringNo"Kyber768" (default) or "X-Wing" (hybrid, Starter+). If omitted, defaults to ML-KEM-768.

Response (200 OK)​

FieldTypeDescription
data.plaintextstring (base64)The decrypted data, base64-encoded.
data.key_versionintegerThe key version used for decryption.
data.algorithmstring"Kyber768" or "X-Wing", matching the algorithm used for the operation.

Example​

RequestPOST/api/v1/kem/decrypt
X-API-Key: qph_your_key_here
Content-Type: application/json
{
  "ciphertext": "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5...",
  "key_version": 1
}
Response200
{
  "data": {
    "plaintext": "SGVsbG8sIFdvcmxkIQ==",
    "key_version": 1,
    "algorithm": "Kyber768"
  },
  "request_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "timestamp": "2026-01-15T10:30:01.000Z"
}
Decrypt example
curl -X POST https://api.qpher.ai/api/v1/kem/decrypt \
  -H "X-API-Key: qph_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "ciphertext": "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5...",
    "key_version": 1
  }'

Errors​

HTTP StatusError CodeDescription
400ERR_KEM_002Invalid decryption request — corrupt ciphertext or wrong key_version.
401ERR_AUTH_001Missing or invalid API key.
404ERR_NOT_FOUND_001Key version not found or key is in archived status.
429ERR_RATE_LIMIT_001Rate limit exceeded for your plan tier.

Encapsulate​

POST/api/v1/kem/encapsulateEncapsulate a shared secret using ML-KEM-768 KEM — for client-side (zero-trust) encryption.

Returns a KEM ciphertext and a 32-byte shared secret. The plaintext never leaves your environment. Use the shared secret as an AES-256-GCM key to encrypt data locally, then store the KEM ciphertext for later decapsulation.

Zero-trust encryption

With encapsulate/decapsulate, Qpher never sees your plaintext. You perform AES-256-GCM encryption locally using the shared secret. This is the most privacy-preserving mode.

Request body​

FieldTypeRequiredDescription
key_versionintegerYesThe version of the KEM key to use. Must reference an active key.
algorithmstringNo"Kyber768" (default) or "X-Wing" (hybrid, Starter+).

Response (200 OK)​

FieldTypeDescription
data.kem_ciphertextstring (base64)The KEM ciphertext. Store this — you need it for decapsulate.
data.shared_secretstring (base64)32-byte shared secret (base64). Use as your AES-256-GCM key. Ephemeral — Qpher does not store this.
data.key_versionintegerThe key version used.
data.algorithmstring"Kyber768" or "X-Wing".

Example​

Encapsulate example
curl -X POST https://api.qpher.ai/api/v1/kem/encapsulate \
  -H "X-API-Key: qph_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "key_version": 1
  }'

Errors​

HTTP StatusError CodeDescription
400ERR_KEM_001Invalid encapsulate request — invalid key_version.
401ERR_AUTH_001Missing or invalid API key.
404ERR_NOT_FOUND_001Key version not found or key is not in active status.
429ERR_RATE_LIMIT_001Rate limit exceeded for your plan tier.

Decapsulate​

POST/api/v1/kem/decapsulateDecapsulate a shared secret using ML-KEM-768 KEM — for client-side (zero-trust) decryption.

Returns the same 32-byte shared secret that was produced during encapsulation. Use it to decrypt data that was encrypted client-side. The referenced key must be in active or retired status.

Request body​

FieldTypeRequiredDescription
kem_ciphertextstring (base64)YesThe KEM ciphertext from the encapsulate response.
key_versionintegerYesThe key version used during encapsulation.
algorithmstringNo"Kyber768" (default) or "X-Wing" (hybrid, Starter+).

Response (200 OK)​

FieldTypeDescription
data.shared_secretstring (base64)The same 32-byte shared secret from the original encapsulate call.
data.key_versionintegerThe key version used.
data.algorithmstring"Kyber768" or "X-Wing".

Example​

Decapsulate example
curl -X POST https://api.qpher.ai/api/v1/kem/decapsulate \
  -H "X-API-Key: qph_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "kem_ciphertext": "a2VtX2NpcGhlcnRleHRfZnJvbV9lbmNhcHN1bGF0ZS4uLg==",
    "key_version": 1
  }'

Errors​

HTTP StatusError CodeDescription
400ERR_KEM_002Invalid decapsulate request — invalid kem_ciphertext size or key_version.
401ERR_AUTH_001Missing or invalid API key.
404ERR_NOT_FOUND_001Key version not found or key is in archived status.
429ERR_RATE_LIMIT_001Rate limit exceeded for your plan tier.

Key Wrap​

POST/api/v1/kem/key/wrapWrap a symmetric key using ML-KEM-768 KEM-DEM hybrid encryption.

Wraps (encrypts) a symmetric key using the KEM-DEM scheme with the specified key version. Use this to protect existing AES keys, HMAC secrets, or other symmetric material with quantum-safe encryption. The key must be in active status.

Request body​

FieldTypeRequiredDescription
symmetric_keystring (base64)YesBase64-encoded symmetric key to wrap. Supported sizes: 16, 24, 32, 48, or 64 bytes.
key_versionintegerYesThe version of the ML-KEM-768 key to use. Must reference an active key.
algorithmstringNo"Kyber768" (default) or "X-Wing" (hybrid, Starter+). If omitted, defaults to ML-KEM-768.

Response (200 OK)​

FieldTypeDescription
data.wrapped_keystring (base64)The wrapped (encrypted) symmetric key, base64-encoded.
data.key_versionintegerThe key version used for wrapping.
data.algorithmstring"Kyber768" or "X-Wing", matching the algorithm used for the operation.
data.wrapping_methodstringThe wrapping method used, e.g. "KEM-DEM".

Example​

Key Wrap example
curl -X POST https://api.qpher.ai/api/v1/kem/key/wrap \
  -H "X-API-Key: qph_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "symmetric_key": "c3ltbWV0cmljX2tleV8zMl9ieXRlcw==",
    "key_version": 1
  }'

Errors​

HTTP StatusError CodeDescription
400ERR_KEM_010Invalid wrap request — missing symmetric_key, invalid key_version, bad encoding, or unsupported key size. Supported sizes: 16, 24, 32, 48, or 64 bytes.
401ERR_AUTH_001Missing or invalid API key.
404ERR_NOT_FOUND_001Key version not found or key is not in active status.
429ERR_RATE_LIMIT_001Rate limit exceeded for your plan tier.

Key Unwrap​

POST/api/v1/kem/key/unwrapUnwrap a symmetric key using ML-KEM-768 KEM-DEM hybrid decryption.

Unwraps (decrypts) a symmetric key that was previously wrapped with the Key Wrap endpoint. The referenced key must be in active or retired status.

Request body​

FieldTypeRequiredDescription
wrapped_keystring (base64)YesBase64-encoded wrapped key returned by the wrap endpoint.
key_versionintegerYesThe key version that was used to wrap this key.
algorithmstringNo"Kyber768" (default) or "X-Wing" (hybrid, Starter+). If omitted, defaults to ML-KEM-768.

Response (200 OK)​

FieldTypeDescription
data.symmetric_keystring (base64)The unwrapped symmetric key, base64-encoded.
data.key_versionintegerThe key version used for unwrapping.
data.algorithmstring"Kyber768" or "X-Wing", matching the algorithm used for the operation.

Example​

Key Unwrap example
curl -X POST https://api.qpher.ai/api/v1/kem/key/unwrap \
  -H "X-API-Key: qph_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "wrapped_key": "d3JhcHBlZF9rZXlfZGF0YV9oZXJl...",
    "key_version": 1
  }'

Errors​

HTTP StatusError CodeDescription
400ERR_KEM_011Invalid unwrap request — missing wrapped_key, bad base64 encoding, or data too short.
400ERR_KEM_012Wrapped key integrity check failed — corrupt data or wrong key_version.
401ERR_AUTH_001Missing or invalid API key.
404ERR_NOT_FOUND_001Key version not found or key is in archived status.
429ERR_RATE_LIMIT_001Rate limit exceeded for your plan tier.

Key Version Requirements​

On encrypt, key_version is optional — omit it to use your tenant's active key (the response returns the version used). On decrypt, encapsulate, decapsulate, and key wrap/unwrap, key_version is required.

OperationAllowed Key Statuses
Encryptactive only
Decryptactive or retired
Encapsulateactive only
Decapsulateactive or retired
Key Wrapactive only
Key Unwrapactive or retired

Use the Key Management API to list your keys and find the current active version.