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 qpherRequirements: 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=1,
)
print(result.ciphertext) # bytes â the encrypted payload
print(result.key_version) # int â the key version used
print(result.request_id) # str â UUID for tracing
| Parameter | Type | Required | Description |
|---|---|---|---|
plaintext | bytes | Yes | The data to encrypt |
key_version | int | Yes | Key version to use (must be active) |
mode | str | No | "standard" (default) or "deterministic" |
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) |
Returns: DecryptResult with fields plaintext (bytes), key_version (int), algorithm (str), and request_id (str).
Qpher does not support implicit "latest" key selection. You must specify the key_version for every operation. This prevents silent key-mismatch errors and ensures auditability.
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=1,
)
print(result.signature) # bytes â the Dilithium3 signature
print(result.key_version) # int
print(result.request_id) # str
| Parameter | Type | Required | Description |
|---|---|---|---|
message | bytes | Yes | The message to sign |
key_version | int | Yes | Signing key version (must be active) |
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) |
Returns: VerifyResult with fields valid (bool), key_version (int), algorithm (str), and request_id (str).
Key Managementâ
Manage PQC key lifecycle: generate, list, get, rotate, and retire keys.
Generate Keyâ
result = client.keys.generate(algorithm="Kyber768")
print(result.key_version) # int â the new key version
print(result.algorithm) # str â "Kyber768"
print(result.status) # str â "active"
print(result.public_key) # bytes â the public key (private key stays in Qpher)
print(result.created_at) # str â ISO-8601 timestamp
print(result.request_id) # str â UUID for tracing
| Parameter | Type | Required | Description |
|---|---|---|---|
algorithm | str | Yes | "Kyber768" or "Dilithium3" |
Returns: GenerateResult with fields key_version (int), algorithm (str), status (str), public_key (bytes), created_at (str), and request_id (str).
List Keysâ
result = client.keys.list(algorithm="Kyber768")
print(f"Total keys: {result.total}")
for key in result.keys:
print(f"Version {key.key_version}: {key.status}")
# Version 1: retired
# Version 2: active
| Parameter | Type | Required | Description |
|---|---|---|---|
algorithm | str | No | Filter by "Kyber768" or "Dilithium3" |
status | str | No | Filter by "active", "retired", or "archived" |
Returns: KeyListResult with fields keys (list[KeyInfo]), total (int), and request_id (str). Each KeyInfo has key_version (int), algorithm (str), status (str), created_at (str), and public_key (bytes).
Get Active Keyâ
key = client.keys.get_active(algorithm="Kyber768")
print(key.key_version) # int â the currently active version
print(key.algorithm) # str â "Kyber768"
print(key.public_key) # bytes â the public key
Get Key by Versionâ
key = client.keys.get(algorithm="Kyber768", key_version=1)
print(key.key_version) # int â 1
print(key.status) # str â "active", "retired", or "archived"
print(key.public_key) # bytes â the public key
print(key.created_at) # str â ISO-8601 timestamp
Rotate Keyâ
new_key = client.keys.rotate(algorithm="Kyber768")
print(new_key.key_version) # int â the new active version
print(new_key.old_key_version) # int â the previous version (now retired)
print(new_key.public_key) # bytes â the new public key
print(new_key.request_id) # str â UUID for tracing
Returns: RotateResult with fields key_version (int), algorithm (str), public_key (bytes), old_key_version (int), and request_id (str).
Retire Keyâ
result = client.keys.retire(algorithm="Kyber768", key_version=1)
print(result.status) # str â "retired"
# Key version 1 is now retired â decrypt/verify still work, encrypt/sign do not
Returns: RetireResult with fields key_version (int), algorithm (str), status (str), public_key (bytes), created_at (str), and request_id (str).
Retired keys can still decrypt and verify, but cannot encrypt or sign. Archived keys cannot perform any operations. Plan your rotation strategy accordingly.
Error Handlingâ
All API errors raise QpherError (or a subclass) with structured fields for programmatic handling.
from qpher import Qpher, QpherError
client = Qpher(api_key="qph_your_key_here")
try:
result = client.kem.encrypt(plaintext=b"data", key_version=999)
except QpherError as e:
print(e.error_code) # str â e.g., "ERR_KEM_005"
print(e.message) # str â human-readable description
print(e.status_code) # int â HTTP status code, e.g., 404
print(e.request_id) # str â UUID for support inquiries
Error Subclassesâ
The SDK provides specific exception subclasses for common error categories. All inherit from QpherError:
from qpher.errors import (
QpherError, # Base class for all Qpher errors
AuthenticationError, # 401 â invalid or missing API key
ValidationError, # 400 â invalid request parameters
NotFoundError, # 404 â resource not found
ForbiddenError, # 403 â operation not allowed by policy
RateLimitError, # 429 â rate limit exceeded
ServerError, # 500+ â internal server error
TimeoutError, # 504 â request timed out
ConnectionError, # 503 â service unavailable
)
You can catch specific error types for fine-grained handling:
from qpher.errors import NotFoundError, RateLimitError
try:
result = client.kem.encrypt(plaintext=b"data", key_version=999)
except NotFoundError:
print("Key version does not exist")
except RateLimitError:
print("Rate limit exceeded â SDK already retried")
except QpherError as e:
print(f"Other API error: {e.error_code}")
Common Error Codesâ
| Error Code | Status | Meaning |
|---|---|---|
ERR_AUTH_001 | 401 | Invalid or missing API key |
ERR_RATE_001 | 429 | Rate limit exceeded (auto-retried) |
ERR_KEM_005 | 404 | Key version not found |
ERR_INVALID_001 | 400 | Invalid request parameters |
ERR_FORBIDDEN_001 | 403 | Operation not allowed by policy |
ERR_SERVICE_001 | 503 | Service temporarily unavailable |
Retry Behaviorâ
The SDK automatically retries on transient errors (HTTP 429, 502, 503, 504) with exponential backoff (0.5s, 1s, 2s, capped at 10s). After max_retries attempts, the final QpherError is raised.
Full Round-Trip Exampleâ
This example demonstrates a complete encrypt-then-decrypt workflow with proper error handling.
import os
from qpher import Qpher, QpherError
def main():
# Initialize client
client = Qpher(
api_key=os.environ["QPHER_API_KEY"],
timeout=30,
max_retries=3,
)
try:
# 1. Get the active Kyber768 key
active_key = client.keys.get_active(algorithm="Kyber768")
print(f"Using key version: {active_key.key_version}")
# 2. Encrypt sensitive data
plaintext = b"Patient record: John Doe, DOB 1990-01-15"
encrypt_result = client.kem.encrypt(
plaintext=plaintext,
key_version=active_key.key_version,
)
print(f"Encrypted {len(plaintext)} bytes -> {len(encrypt_result.ciphertext)} bytes")
print(f"Request ID: {encrypt_result.request_id}")
# 3. Decrypt to recover the original data
decrypt_result = client.kem.decrypt(
ciphertext=encrypt_result.ciphertext,
key_version=encrypt_result.key_version,
)
# 4. Verify round-trip integrity
assert decrypt_result.plaintext == plaintext
print("Round-trip successful: plaintext matches")
except QpherError as e:
print(f"Qpher API error: {e.error_code} - {e.message}")
print(f"Status: {e.status_code}, Request ID: {e.request_id}")
except Exception as e:
print(f"Unexpected error: {e}")
if __name__ == "__main__":
main()
Sign and Verify Exampleâ
import os
from qpher import Qpher, QpherError
client = Qpher(api_key=os.environ["QPHER_API_KEY"])
# Get the active Dilithium3 signing key
signing_key = client.keys.get_active(algorithm="Dilithium3")
# Sign a document
document = b"Invoice #1234: $5,000.00 due 2026-03-01"
sign_result = client.signatures.sign(
message=document,
key_version=signing_key.key_version,
)
print(f"Signature length: {len(sign_result.signature)} bytes")
# Verify the signature
verify_result = client.signatures.verify(
message=document,
signature=sign_result.signature,
key_version=sign_result.key_version,
)
print(f"Signature valid: {verify_result.valid}") # True
# Verify with tampered message
verify_tampered = client.signatures.verify(
message=b"Invoice #1234: $50,000.00 due 2026-03-01",
signature=sign_result.signature,
key_version=sign_result.key_version,
)
print(f"Tampered signature valid: {verify_tampered.valid}") # False
Configuration Referenceâ
Environment Variablesâ
The SDK respects these environment variables as fallbacks:
| Variable | Purpose |
|---|---|
QPHER_API_KEY | API key (used if api_key parameter is omitted) |
QPHER_BASE_URL | Base URL (used if base_url parameter is omitted) |
Loggingâ
The SDK uses Python's standard logging module under the qpher logger name. To enable debug logging:
import logging
logging.getLogger("qpher").setLevel(logging.DEBUG)
Debug logging never prints your API key or response bodies containing ciphertext. Sensitive fields are redacted as [REDACTED].
Next Stepsâ
- Node.js SDK â if your backend uses JavaScript or TypeScript
- Go SDK â if your backend uses Go
- REST API â for raw HTTP access from any language
- API Reference â full OpenAPI specification