Python SDK
The Qpher Python SDK is a thin wrapper around the Qpher REST API. All cryptographic operations execute server-side â the SDK handles authentication, serialization, retries, and error mapping.
Installationâ
pip install qpherOn modern macOS / Linux, pip install into the system Python fails with an
externally-managed-environment error. Create and activate a virtual environment first:
python -m venv .venv && source .venv/bin/activate # recommended; avoids "externally-managed-environment" errors
pip install qpher
Requirements: Python 3.9 or later. No native dependencies.
Client Setupâ
from qpher import Qpher, QpherError
client = Qpher(
api_key="qph_your_key_here",
base_url="https://api.qpher.ai", # default
timeout=30, # seconds, default: 30
max_retries=3, # default: 3
)
| Parameter | Type | Default | Description |
|---|---|---|---|
api_key | str | required | Your Qpher API key (starts with qph_) |
base_url | str | https://api.qpher.ai | API base URL |
timeout | int | 30 | Request timeout in seconds |
max_retries | int | 3 | Number of retries for transient errors (429, 502, 503, 504) |
Never hard-code API keys in source files. Load them from environment variables or a secrets manager:
client = Qpher(api_key=os.environ["QPHER_API_KEY"])
KEM Encryption and Decryptionâ
Qpher uses Kyber768 (ML-KEM-768) for key encapsulation. The SDK accepts raw bytes for plaintext and returns bytes for ciphertext. Base64 encoding on the wire is handled automatically.
Encryptâ
result = client.kem.encrypt(
plaintext=b"sensitive data to protect",
) # key_version optional â omit it and the server uses your active key
print(result.ciphertext) # bytes â the encrypted payload
print(result.key_version) # int â the key version used
print(result.request_id) # str â UUID for tracing
Hybrid encryption (Pro/Enterprise): Pass
algorithm="X-Wing"for hybrid X25519 + ML-KEM-768:result = client.kem.encrypt(plaintext=b"sensitive data to protect",key_version=1,algorithm="X-Wing",)
| Parameter | Type | Required | Description |
|---|---|---|---|
plaintext | bytes | Yes | The data to encrypt |
key_version | int | No | Optional â omit to use your active key (the server resolves it and returns the version used). If supplied, it must be active. |
algorithm | str | No | "Kyber768" (default) or "X-Wing" (hybrid, Pro/Enterprise) |
mode | str | No | "standard" (default) or "deterministic" (not yet in effect â see Deterministic Encryption) |
salt | bytes | No | Required when mode="deterministic" |
Returns: EncryptResult with fields ciphertext (bytes), key_version (int), algorithm (str), and request_id (str).
Decryptâ
result = client.kem.decrypt(
ciphertext=encrypted_ciphertext,
key_version=1,
)
print(result.plaintext) # bytes â the original data
print(result.request_id) # str
| Parameter | Type | Required | Description |
|---|---|---|---|
ciphertext | bytes | Yes | The encrypted payload to decrypt |
key_version | int | Yes | Key version used during encryption (active or retired) |
algorithm | str | No | "Kyber768" (default) or "X-Wing" (hybrid, Pro/Enterprise) |
Returns: DecryptResult with fields plaintext (bytes), key_version (int), algorithm (str), and request_id (str).
Encrypt and sign: key_version is optional â omit it and the server uses your current active key, returning the resolved version in the response. Decrypt and verify: key_version is required and must match the version used to encrypt/sign (active or retired; archived is rejected). This keeps reads explicit and auditable while writes always use the active key.
Key Wrappingâ
Wrap and unwrap existing symmetric keys (AES, HMAC) using quantum-safe KEM. See the Key Wrap / Unwrap guide for details.
# Wrap a symmetric key
result = client.kem.wrap(
symmetric_key=aes_key, # bytes â 16, 24, 32, 48, or 64 bytes
key_version=1,
)
print(result.wrapped_key) # bytes â store this safely
print(result.key_version) # int
print(result.algorithm) # str â "Kyber768"
# Unwrap to recover the original key
result = client.kem.unwrap(
wrapped_key=result.wrapped_key,
key_version=1,
)
print(result.symmetric_key) # bytes â the original key
| Parameter | Type | Required | Description |
|---|---|---|---|
symmetric_key / wrapped_key | bytes | Yes | The symmetric key to wrap, or the wrapped key to unwrap |
key_version | int | Yes | Key version (wrap: active only; unwrap: active or retired) |
algorithm | str | No | "Kyber768" (default) or "X-Wing" (hybrid) |
Raw KEM (Encapsulate / Decapsulate)â
Low-level KEM operations for advanced use cases. These give you the raw shared secret and KEM ciphertext without the KEM-DEM wrapper.
# Encapsulate â generates a shared secret and KEM ciphertext
result = client.kem.encapsulate(key_version=1)
print(result.shared_secret) # bytes â 32-byte shared secret
print(result.kem_ciphertext) # bytes â send this to the decapsulator
print(result.key_version) # int
# Decapsulate â recovers the shared secret
result = client.kem.decapsulate(
kem_ciphertext=kem_ciphertext,
key_version=1,
)
print(result.shared_secret) # bytes â same 32-byte shared secret
| Parameter | Type | Required | Description |
|---|---|---|---|
kem_ciphertext | bytes | Yes (decapsulate) | The KEM ciphertext from encapsulate |
key_version | int | Yes | Key version to use |
algorithm | str | No | "Kyber768" (default) or "X-Wing" (hybrid) |
Client-Side Encryptionâ
Encrypt data locally so that plaintext never leaves your environment. The SDK encapsulates a shared secret via the API, then performs AES-256-GCM encryption locally.
# Encrypt locally â plaintext never sent to Qpher
envelope = client.kem.encrypt_local(
plaintext=b"ultra-sensitive data",
key_version=1,
)
# envelope contains: kem_ciphertext, iv, aes_ciphertext, key_version, algorithm
# Decrypt locally
plaintext = client.kem.decrypt_local(envelope)
print(plaintext) # b"ultra-sensitive data"
| Parameter | Type | Required | Description |
|---|---|---|---|
plaintext | bytes | Yes | Data to encrypt (encrypt_local) |
envelope | EncryptedEnvelope | Yes | The envelope from encrypt_local (decrypt_local) |
key_version | int | Yes | Key version to use (encrypt_local) |
algorithm | str | No | "Kyber768" (default) or "X-Wing" (hybrid) |
With encrypt_local / decrypt_local, your plaintext never leaves your environment. Only the KEM ciphertext (for shared secret derivation) is sent to the Qpher API. The AES-256-GCM encryption happens entirely in your process.
Digital Signaturesâ
Qpher uses Dilithium3 (ML-DSA-65) for post-quantum digital signatures.
Signâ
result = client.signatures.sign(
message=b"document content to sign",
) # key_version optional â omit it and the server uses your active key
print(result.signature) # bytes â the Dilithium3 signature
print(result.key_version) # int
print(result.request_id) # str
Hybrid signatures (Pro/Enterprise): Pass
algorithm="Composite-ML-DSA"for hybrid ECDSA + ML-DSA-65:result = client.signatures.sign(message=b"document content to sign",key_version=1,algorithm="Composite-ML-DSA",)
| Parameter | Type | Required | Description |
|---|---|---|---|
message | bytes | Yes | The message to sign |
key_version | int | No | Optional â omit to use your active key (the server resolves it). If supplied, must be active. |
algorithm | str | No | "Dilithium3" (default) or "Composite-ML-DSA" (hybrid, Pro/Enterprise) |
Returns: SignResult with fields signature (bytes), key_version (int), and request_id (str).
Verifyâ
result = client.signatures.verify(
message=b"document content to sign",
signature=signature_bytes,
key_version=1,
)
print(result.valid) # bool â True if the signature is valid
print(result.key_version) # int
print(result.request_id) # str
| Parameter | Type | Required | Description |
|---|---|---|---|
message | bytes | Yes | The original message |
signature | bytes | Yes | The signature to verify |
key_version | int | Yes | Key version used during signing (active or retired) |
algorithm | str | No | "Dilithium3" (default) or "Composite-ML-DSA" (hybrid, Pro/Enterprise) |
Returns: VerifyResult with fields valid (bool), key_version (int), algorithm (str), and request_id (str).
Note:
"X-Wing"and"Composite-ML-DSA"are available on Pro and Enterprise plans only. Composite ML-DSA signatures use Qpher's internal wire format and must be verified using Qpher's verify endpoint.
Sign Hashâ
Sign a pre-computed hash digest instead of the full message. Ideal for large files where the hash is computed locally. See the Hash-Based Signing guide for details.
import hashlib
# Compute hash locally
file_hash = hashlib.sha256(open("artifact.tar.gz", "rb").read()).digest()
# Sign the hash â only 32 bytes sent to Qpher, not the file
result = client.signatures.sign_hash(
hash_value=file_hash,
hash_algorithm="SHA-256",
key_version=1,
)
print(result.signature) # bytes â detached signature
print(result.signature_type) # str â "detached"
| Parameter | Type | Required | Description |
|---|---|---|---|
hash_value | bytes | Yes | Pre-computed hash (SHA-256: 32B, SHA-384: 48B, SHA-512: 64B) |
hash_algorithm | str | Yes | "SHA-256", "SHA-384", or "SHA-512" |
key_version | int | Yes | Signing key version (must be active) |
algorithm | str | No | "Dilithium3" (default) or "Composite-ML-DSA" (hybrid) |
Returns: SignHashResult with fields signature (bytes), key_version (int), algorithm (str), hash_algorithm (str), signature_type (str), and request_id (str).
Verify Hashâ
result = client.signatures.verify_hash(
hash_value=file_hash,
hash_algorithm="SHA-256",
signature=signature_bytes,
key_version=1,
)
print(result.valid) # bool â True if signature matches the hash
| Parameter | Type | Required | Description |
|---|---|---|---|
hash_value | bytes | Yes | The same hash that was signed |
hash_algorithm | str | Yes | "SHA-256", "SHA-384", or "SHA-512" |
signature | bytes | Yes | The signature to verify |
key_version | int | Yes | Key version used during signing (active or retired) |
algorithm | str | No | "Dilithium3" (default) or "Composite-ML-DSA" (hybrid) |
Returns: VerifyHashResult with fields valid (bool), key_version (int), algorithm (str), hash_algorithm (str), and request_id (str).