Skip to main content

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 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
)
ParameterTypeDefaultDescription
api_keystrrequiredYour Qpher API key (starts with qph_)
base_urlstrhttps://api.qpher.aiAPI base URL
timeoutint30Request timeout in seconds
max_retriesint3Number of retries for transient errors (429, 502, 503, 504)
Protect your API key

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
ParameterTypeRequiredDescription
plaintextbytesYesThe data to encrypt
key_versionintYesKey version to use (must be active)
modestrNo"standard" (default) or "deterministic"
saltbytesNoRequired 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
ParameterTypeRequiredDescription
ciphertextbytesYesThe encrypted payload to decrypt
key_versionintYesKey version used during encryption (active or retired)

Returns: DecryptResult with fields plaintext (bytes), key_version (int), algorithm (str), and request_id (str).

Key version is always required

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
ParameterTypeRequiredDescription
messagebytesYesThe message to sign
key_versionintYesSigning 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
ParameterTypeRequiredDescription
messagebytesYesThe original message
signaturebytesYesThe signature to verify
key_versionintYesKey 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
ParameterTypeRequiredDescription
algorithmstrYes"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
ParameterTypeRequiredDescription
algorithmstrNoFilter by "Kyber768" or "Dilithium3"
statusstrNoFilter 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).

Key lifecycle

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 CodeStatusMeaning
ERR_AUTH_001401Invalid or missing API key
ERR_RATE_001429Rate limit exceeded (auto-retried)
ERR_KEM_005404Key version not found
ERR_INVALID_001400Invalid request parameters
ERR_FORBIDDEN_001403Operation not allowed by policy
ERR_SERVICE_001503Service 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:

VariablePurpose
QPHER_API_KEYAPI key (used if api_key parameter is omitted)
QPHER_BASE_URLBase 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)
Security

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