REST API
You can call the Qpher API directly over HTTP from any language. This page shows cURL examples for every operation. All request and response bodies use JSON. Binary fields (plaintext, ciphertext, signatures, public keys) are base64-encoded strings.
Base URL
https://api.qpher.ai
Headers
| Header | Required | Description |
|---|---|---|
X-API-Key | Yes | Your Qpher API key (starts with qph_) |
Content-Type | Yes (for POST) | Always application/json |
X-Request-ID | No | UUID for distributed tracing. If omitted, the API generates one. |
Every response includes these headers:
| Header | Description |
|---|---|
X-Request-ID | UUID for the request (same as your input, or auto-generated) |
Content-Type | application/json |
Response Format
All responses follow a consistent envelope:
{
"data": { },
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-15T10:30:00.000Z"
}
Error responses use the same envelope with an error field instead of data:
{
"error": {
"code": "ERR_AUTH_001",
"message": "Invalid or missing API key"
},
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-15T10:30:00.000Z"
}
KEM Operations
Encrypt
Encrypt data using Kyber768 (ML-KEM-768). The plaintext field must be a base64-encoded string.
Request:
curl -X POST https://api.qpher.ai/api/v1/kem/encrypt \
-H "X-API-Key: qph_your_key_here" \
-H "Content-Type: application/json" \
-H "X-Request-ID: 550e8400-e29b-41d4-a716-446655440000" \
-d '{
"plaintext": "c2Vuc2l0aXZlIGRhdGEgdG8gcHJvdGVjdA==",
"key_version": 1
}'
Response (200):
{
"data": {
"ciphertext": "YWJjZGVmZzEyMzQ1Njc4OTAxMjM0NTY3ODkw...",
"key_version": 1
},
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-15T10:30:00.123Z"
}
| Field | Type | Description |
|---|---|---|
plaintext | string (base64) | The data to encrypt |
key_version | integer | Key version to use (must be active) |
mode | string | Optional. "standard" (default) or "deterministic" |
salt | string (base64) | Required when mode is "deterministic" |
Decrypt
Decrypt data that was previously encrypted with Kyber768.
Request:
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": "YWJjZGVmZzEyMzQ1Njc4OTAxMjM0NTY3ODkw...",
"key_version": 1
}'
Response (200):
{
"data": {
"plaintext": "c2Vuc2l0aXZlIGRhdGEgdG8gcHJvdGVjdA=="
},
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2026-02-15T10:30:01.456Z"
}
| Field | Type | Description |
|---|---|---|
ciphertext | string (base64) | The encrypted payload |
key_version | integer | Key version used during encryption (active or retired) |
Qpher does not support implicit "latest" key selection. You must specify the key_version for every operation.
Signature Operations
Sign
Sign a message using Dilithium3 (ML-DSA-65).
Request:
curl -X POST https://api.qpher.ai/api/v1/signature/sign \
-H "X-API-Key: qph_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"message": "SW52b2ljZSAjMTIzNDogJDUsMDAwLjAwIGR1ZSAyMDI2LTAzLTAx",
"key_version": 1
}'
Response (200):
{
"data": {
"signature": "TUVVQ0lRQ2dBd0lCQU...",
"key_version": 1
},
"request_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"timestamp": "2026-02-15T10:30:02.789Z"
}
| Field | Type | Description |
|---|---|---|
message | string (base64) | The message to sign |
key_version | integer | Signing key version (must be active) |
Verify
Verify a Dilithium3 signature against the original message.
Request:
curl -X POST https://api.qpher.ai/api/v1/signature/verify \
-H "X-API-Key: qph_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"message": "SW52b2ljZSAjMTIzNDogJDUsMDAwLjAwIGR1ZSAyMDI2LTAzLTAx",
"signature": "TUVVQ0lRQ2dBd0lCQU...",
"key_version": 1
}'
Response (200):
{
"data": {
"valid": true
},
"request_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"timestamp": "2026-02-15T10:30:03.012Z"
}
| Field | Type | Description |
|---|---|---|
message | string (base64) | The original message |
signature | string (base64) | The signature to verify |
key_version | integer | Key version used during signing (active or retired) |
Key Management Operations
Generate Key
Generate a new PQC key pair. The private key is stored securely inside Qpher's enclave and is never returned.
Request:
curl -X POST https://api.qpher.ai/api/v1/kms/keys/generate \
-H "X-API-Key: qph_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"algorithm": "Kyber768"
}'
Response (201):
{
"data": {
"key_version": 1,
"algorithm": "Kyber768",
"status": "active",
"public_key": "MIIBIjANBgkqhkiG9w0BAQEF...",
"created_at": "2026-02-15T10:30:04.000Z"
},
"request_id": "d4e5f6a7-b8c9-0123-def0-234567890123",
"timestamp": "2026-02-15T10:30:04.000Z"
}
| Field | Type | Description |
|---|---|---|
algorithm | string | "Kyber768" or "Dilithium3" |
List Keys
List all PQC keys for a given algorithm.
Request:
curl -X GET "https://api.qpher.ai/api/v1/kms/keys?algorithm=Kyber768" \
-H "X-API-Key: qph_your_key_here"
Response (200):
{
"data": {
"keys": [
{
"key_version": 1,
"algorithm": "Kyber768",
"status": "retired",
"public_key": "MIIBIjANBgkqhkiG9w0BAQEF...",
"created_at": "2026-02-01T08:00:00.000Z"
},
{
"key_version": 2,
"algorithm": "Kyber768",
"status": "active",
"public_key": "MIIBIjANBgkqhkiG9w0BAQEF...",
"created_at": "2026-02-15T10:30:04.000Z"
}
]
},
"request_id": "e5f6a7b8-c9d0-1234-ef01-345678901234",
"timestamp": "2026-02-15T10:30:05.000Z"
}
Get Active Key
Retrieve the currently active key for a given algorithm.
Request:
curl -X GET "https://api.qpher.ai/api/v1/kms/keys/active?algorithm=Kyber768" \
-H "X-API-Key: qph_your_key_here"
Response (200):
{
"data": {
"key_version": 2,
"algorithm": "Kyber768",
"status": "active",
"public_key": "MIIBIjANBgkqhkiG9w0BAQEF...",
"created_at": "2026-02-15T10:30:04.000Z"
},
"request_id": "f6a7b8c9-d0e1-2345-f012-456789012345",
"timestamp": "2026-02-15T10:30:06.000Z"
}
Get Key by Version
Retrieve a specific key version by algorithm and version number.
Request:
curl -X GET "https://api.qpher.ai/api/v1/kms/keys/Kyber768/1" \
-H "X-API-Key: qph_your_key_here"
Response (200):
{
"data": {
"key_version": 1,
"algorithm": "Kyber768",
"status": "retired",
"public_key": "MIIBIjANBgkqhkiG9w0BAQEF...",
"created_at": "2026-02-01T08:00:00.000Z"
},
"request_id": "06a7b8c9-d0e1-2345-f012-456789012346",
"timestamp": "2026-02-15T10:30:06.500Z"
}
Rotate Key
Generate a new key version and set it as active. The previous active key is automatically retired.
Request:
curl -X POST https://api.qpher.ai/api/v1/kms/keys/rotate \
-H "X-API-Key: qph_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"algorithm": "Kyber768"
}'
Response (200):
{
"data": {
"key_version": 3,
"algorithm": "Kyber768",
"status": "active",
"public_key": "MIIBIjANBgkqhkiG9w0BAQEF...",
"created_at": "2026-02-15T10:30:07.000Z"
},
"request_id": "a7b8c9d0-e1f2-3456-0123-567890123456",
"timestamp": "2026-02-15T10:30:07.000Z"
}
Retire Key
Retire a specific key version. Retired keys can still be used for decryption and verification, but not for encryption or signing.
Request:
curl -X POST https://api.qpher.ai/api/v1/kms/keys/retire \
-H "X-API-Key: qph_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"algorithm": "Kyber768",
"key_version": 2
}'
Response (200):
{
"data": {
"key_version": 2,
"algorithm": "Kyber768",
"status": "retired"
},
"request_id": "b8c9d0e1-f2a3-4567-1234-678901234567",
"timestamp": "2026-02-15T10:30:08.000Z"
}
Once a key is retired, it cannot encrypt or sign new data. It can still decrypt or verify data that was encrypted or signed with that key version. Archiving a key disables all operations.
Error Responses
All errors follow the same envelope. The HTTP status code indicates the error category.
401 Unauthorized
curl -X POST https://api.qpher.ai/api/v1/kem/encrypt \
-H "X-API-Key: invalid_key" \
-H "Content-Type: application/json" \
-d '{"plaintext": "dGVzdA==", "key_version": 1}'
{
"error": {
"code": "ERR_AUTH_001",
"message": "Invalid or missing API key"
},
"request_id": "c9d0e1f2-a3b4-5678-2345-789012345678",
"timestamp": "2026-02-15T10:30:09.000Z"
}
400 Bad Request
{
"error": {
"code": "ERR_INVALID_001",
"message": "Validation failed: key_version is required"
},
"request_id": "d0e1f2a3-b4c5-6789-3456-890123456789",
"timestamp": "2026-02-15T10:30:10.000Z"
}
404 Not Found
{
"error": {
"code": "ERR_KEM_005",
"message": "Key version not found"
},
"request_id": "e1f2a3b4-c5d6-7890-4567-901234567890",
"timestamp": "2026-02-15T10:30:11.000Z"
}
429 Rate Limit Exceeded
{
"error": {
"code": "ERR_RATE_001",
"message": "Rate limit exceeded. Retry after 2 seconds."
},
"request_id": "f2a3b4c5-d6e7-8901-5678-012345678901",
"timestamp": "2026-02-15T10:30:12.000Z"
}
403 Forbidden
{
"error": {
"code": "ERR_FORBIDDEN_001",
"message": "Operation not allowed by policy"
},
"request_id": "a3b4c5d6-e7f8-9012-6789-123456789012",
"timestamp": "2026-02-15T10:30:13.000Z"
}
503 Service Unavailable
{
"error": {
"code": "ERR_SERVICE_001",
"message": "Service temporarily unavailable"
},
"request_id": "b4c5d6e7-f8a9-0123-7890-234567890123",
"timestamp": "2026-02-15T10:30:14.000Z"
}
Error Code Reference
| Code | HTTP Status | Description |
|---|---|---|
ERR_AUTH_001 | 401 | Invalid or missing API key |
ERR_INVALID_001 | 400 | Request validation failed |
ERR_FORBIDDEN_001 | 403 | Operation denied by policy |
ERR_NOT_FOUND_001 | 404 | Resource not found |
ERR_KEM_005 | 404 | Key version not found |
ERR_RATE_001 | 429 | Rate limit exceeded |
ERR_CRYPTO_001 | 500 | Internal cryptography error |
ERR_INTERNAL_001 | 500 | Internal server error |
ERR_BAD_GATEWAY_001 | 502 | Downstream service error |
ERR_SERVICE_001 | 503 | Service unavailable |
ERR_TIMEOUT_001 | 504 | Gateway timeout |
Full cURL Workflow
Here is a complete workflow using only cURL: generate a key, encrypt data, then decrypt it.
# Step 1: Generate a Kyber768 key
curl -s -X POST https://api.qpher.ai/api/v1/kms/keys/generate \
-H "X-API-Key: qph_your_key_here" \
-H "Content-Type: application/json" \
-d '{"algorithm": "Kyber768"}' | jq .
# Step 2: Encrypt data (base64-encode your plaintext first)
# "sensitive data to protect" -> "c2Vuc2l0aXZlIGRhdGEgdG8gcHJvdGVjdA=="
curl -s -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": "c2Vuc2l0aXZlIGRhdGEgdG8gcHJvdGVjdA==",
"key_version": 1
}' | jq .
# Step 3: Decrypt (use the ciphertext from step 2)
curl -s -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": "<ciphertext_from_step_2>",
"key_version": 1
}' | jq .
# Step 4: Sign a message
curl -s -X POST https://api.qpher.ai/api/v1/signature/sign \
-H "X-API-Key: qph_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"message": "SW52b2ljZSAjMTIzNA==",
"key_version": 1
}' | jq .
# Step 5: Verify the signature (use the signature from step 4)
curl -s -X POST https://api.qpher.ai/api/v1/signature/verify \
-H "X-API-Key: qph_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"message": "SW52b2ljZSAjMTIzNA==",
"signature": "<signature_from_step_4>",
"key_version": 1
}' | jq .
Remember to base64-encode binary fields before sending them. On macOS and Linux, use echo -n "your data" | base64 to encode. The SDKs handle this automatically.
All API requests must use HTTPS. The API rejects plain HTTP connections. Your API key and all data are encrypted in transit via TLS 1.3.
Next Steps
- Python SDK — typed Python wrapper with automatic retries
- Node.js SDK — TypeScript SDK with async/await
- Go SDK — idiomatic Go SDK with context support
- API Reference — full OpenAPI specification