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.
All decryption operations happen inside the Qpher secure enclave. Your private keys are never exposed or exported.
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.
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.
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â
Encrypts the provided plaintext using the KEM-DEM scheme with the specified key version. The key must be in active status.
Request bodyâ
| Field | Type | Required | Description |
|---|---|---|---|
plaintext | string (base64) | Yes | Base64-encoded data to encrypt. Maximum size: 1 MB. |
key_version | integer | No | The 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. |
algorithm | string | No | "Kyber768" (default) or "X-Wing" (hybrid, Starter+). If omitted, defaults to ML-KEM-768. |
mode | string | No | Encryption mode: "standard" (default) or "deterministic". |
salt | string (base64) | Conditional | Base64-encoded salt. Required when mode is "deterministic". |
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.
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)â
| Field | Type | Description |
|---|---|---|
data.ciphertext | string (base64) | The encrypted data, base64-encoded. |
data.key_version | integer | The key version used for encryption. |
data.algorithm | string | "Kyber768" or "X-Wing", matching the algorithm used for the operation. |
Exampleâ
/api/v1/kem/encryptX-API-Key: qph_your_key_here
Content-Type: application/json{
"plaintext": "SGVsbG8sIFdvcmxkIQ=="
}{
"data": {
"ciphertext": "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5...",
"key_version": 1,
"algorithm": "Kyber768"
},
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2026-01-15T10:30:00.000Z"
}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 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 Status | Error Code | Description |
|---|---|---|
| 400 | ERR_KEM_001 | Invalid encryption request â missing plaintext, invalid key_version, or bad encoding. |
| 401 | ERR_AUTH_001 | Missing or invalid API key. |
| 404 | ERR_NOT_FOUND_001 | Key version not found or key is not in active status. |
| 429 | ERR_RATE_LIMIT_001 | Rate limit exceeded for your plan tier. |
Decryptâ
Decrypts ciphertext that was previously encrypted with the KEM encrypt endpoint. The referenced key must be in active or retired status.
Request bodyâ
| Field | Type | Required | Description |
|---|---|---|---|
ciphertext | string (base64) | Yes | Base64-encoded ciphertext returned by the encrypt endpoint. |
key_version | integer | Yes | The key version that was used to encrypt this data. |
algorithm | string | No | "Kyber768" (default) or "X-Wing" (hybrid, Starter+). If omitted, defaults to ML-KEM-768. |
Response (200 OK)â
| Field | Type | Description |
|---|---|---|
data.plaintext | string (base64) | The decrypted data, base64-encoded. |
data.key_version | integer | The key version used for decryption. |
data.algorithm | string | "Kyber768" or "X-Wing", matching the algorithm used for the operation. |
Exampleâ
/api/v1/kem/decryptX-API-Key: qph_your_key_here
Content-Type: application/json{
"ciphertext": "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5...",
"key_version": 1
}{
"data": {
"plaintext": "SGVsbG8sIFdvcmxkIQ==",
"key_version": 1,
"algorithm": "Kyber768"
},
"request_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"timestamp": "2026-01-15T10:30:01.000Z"
}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 Status | Error Code | Description |
|---|---|---|
| 400 | ERR_KEM_002 | Invalid decryption request â corrupt ciphertext or wrong key_version. |
| 401 | ERR_AUTH_001 | Missing or invalid API key. |
| 404 | ERR_NOT_FOUND_001 | Key version not found or key is in archived status. |
| 429 | ERR_RATE_LIMIT_001 | Rate limit exceeded for your plan tier. |
Encapsulateâ
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.
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â
| Field | Type | Required | Description |
|---|---|---|---|
key_version | integer | Yes | The version of the KEM key to use. Must reference an active key. |
algorithm | string | No | "Kyber768" (default) or "X-Wing" (hybrid, Starter+). |
Response (200 OK)â
| Field | Type | Description |
|---|---|---|
data.kem_ciphertext | string (base64) | The KEM ciphertext. Store this â you need it for decapsulate. |
data.shared_secret | string (base64) | 32-byte shared secret (base64). Use as your AES-256-GCM key. Ephemeral â Qpher does not store this. |
data.key_version | integer | The key version used. |
data.algorithm | string | "Kyber768" or "X-Wing". |
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 Status | Error Code | Description |
|---|---|---|
| 400 | ERR_KEM_001 | Invalid encapsulate request â invalid key_version. |
| 401 | ERR_AUTH_001 | Missing or invalid API key. |
| 404 | ERR_NOT_FOUND_001 | Key version not found or key is not in active status. |
| 429 | ERR_RATE_LIMIT_001 | Rate limit exceeded for your plan tier. |
Decapsulateâ
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â
| Field | Type | Required | Description |
|---|---|---|---|
kem_ciphertext | string (base64) | Yes | The KEM ciphertext from the encapsulate response. |
key_version | integer | Yes | The key version used during encapsulation. |
algorithm | string | No | "Kyber768" (default) or "X-Wing" (hybrid, Starter+). |
Response (200 OK)â
| Field | Type | Description |
|---|---|---|
data.shared_secret | string (base64) | The same 32-byte shared secret from the original encapsulate call. |
data.key_version | integer | The key version used. |
data.algorithm | string | "Kyber768" or "X-Wing". |
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 Status | Error Code | Description |
|---|---|---|
| 400 | ERR_KEM_002 | Invalid decapsulate request â invalid kem_ciphertext size or key_version. |
| 401 | ERR_AUTH_001 | Missing or invalid API key. |
| 404 | ERR_NOT_FOUND_001 | Key version not found or key is in archived status. |
| 429 | ERR_RATE_LIMIT_001 | Rate limit exceeded for your plan tier. |
Key Wrapâ
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â
| Field | Type | Required | Description |
|---|---|---|---|
symmetric_key | string (base64) | Yes | Base64-encoded symmetric key to wrap. Supported sizes: 16, 24, 32, 48, or 64 bytes. |
key_version | integer | Yes | The version of the ML-KEM-768 key to use. Must reference an active key. |
algorithm | string | No | "Kyber768" (default) or "X-Wing" (hybrid, Starter+). If omitted, defaults to ML-KEM-768. |
Response (200 OK)â
| Field | Type | Description |
|---|---|---|
data.wrapped_key | string (base64) | The wrapped (encrypted) symmetric key, base64-encoded. |
data.key_version | integer | The key version used for wrapping. |
data.algorithm | string | "Kyber768" or "X-Wing", matching the algorithm used for the operation. |
data.wrapping_method | string | The wrapping method used, e.g. "KEM-DEM". |
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 Status | Error Code | Description |
|---|---|---|
| 400 | ERR_KEM_010 | Invalid wrap request â missing symmetric_key, invalid key_version, bad encoding, or unsupported key size. Supported sizes: 16, 24, 32, 48, or 64 bytes. |
| 401 | ERR_AUTH_001 | Missing or invalid API key. |
| 404 | ERR_NOT_FOUND_001 | Key version not found or key is not in active status. |
| 429 | ERR_RATE_LIMIT_001 | Rate limit exceeded for your plan tier. |
Key Unwrapâ
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â
| Field | Type | Required | Description |
|---|---|---|---|
wrapped_key | string (base64) | Yes | Base64-encoded wrapped key returned by the wrap endpoint. |
key_version | integer | Yes | The key version that was used to wrap this key. |
algorithm | string | No | "Kyber768" (default) or "X-Wing" (hybrid, Starter+). If omitted, defaults to ML-KEM-768. |
Response (200 OK)â
| Field | Type | Description |
|---|---|---|
data.symmetric_key | string (base64) | The unwrapped symmetric key, base64-encoded. |
data.key_version | integer | The key version used for unwrapping. |
data.algorithm | string | "Kyber768" or "X-Wing", matching the algorithm used for the operation. |
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 Status | Error Code | Description |
|---|---|---|
| 400 | ERR_KEM_011 | Invalid unwrap request â missing wrapped_key, bad base64 encoding, or data too short. |
| 400 | ERR_KEM_012 | Wrapped key integrity check failed â corrupt data or wrong key_version. |
| 401 | ERR_AUTH_001 | Missing or invalid API key. |
| 404 | ERR_NOT_FOUND_001 | Key version not found or key is in archived status. |
| 429 | ERR_RATE_LIMIT_001 | Rate 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.
| Operation | Allowed Key Statuses |
|---|---|
| Encrypt | active only |
| Decrypt | active or retired |
| Encapsulate | active only |
| Decapsulate | active or retired |
| Key Wrap | active only |
| Key Unwrap | active or retired |
Use the Key Management API to list your keys and find the current active version.