Go SDK
The Qpher Go SDK provides an idiomatic Go interface to the Qpher REST API. All cryptographic operations execute server-side. The SDK handles authentication, base64 encoding, retries, and structured error mapping. Every method accepts a context.Context for cancellation and deadline control.
Installationâ
go get github.com/qpher/qpher-goRequirements: Go 1.21 or later. No CGo dependencies.
Client Setupâ
package main
import (
"context"
"fmt"
"os"
qpher "github.com/qpher/qpher-go"
)
func main() {
client, err := qpher.NewClient("qph_your_key_here", nil)
if err != nil {
log.Fatal(err)
}
// Or with custom options:
client, err = qpher.NewClient(os.Getenv("QPHER_API_KEY"), &qpher.ClientOptions{
BaseURL: "https://api.qpher.ai", // default
Timeout: 30, // seconds, default: 30
MaxRetries: 3, // default: 3
})
if err != nil {
log.Fatal(err)
}
}
| Field | Type | Default | Description |
|---|---|---|---|
apiKey | string | required | Your Qpher API key (starts with qph_) |
BaseURL | string | https://api.qpher.ai | API base URL |
Timeout | int | 30 | Request timeout in seconds |
MaxRetries | int | 3 | Retries for transient errors (429, 502, 503, 504) |
Pass nil as the second argument to NewClient to use all defaults.
Never hard-code API keys in source files. Load them from environment variables:
qpher.NewClient(os.Getenv("QPHER_API_KEY"), nil)
KEM Encryption and Decryptionâ
Qpher uses Kyber768 (ML-KEM-768) for key encapsulation. The SDK accepts []byte for binary data and returns []byte for ciphertext. Base64 encoding on the wire is handled automatically.
Encryptâ
ctx := context.Background()
result, err := client.KEM.Encrypt(ctx, &qpher.EncryptInput{
Plaintext: []byte("sensitive data to protect"),
KeyVersion: 1,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Ciphertext) // []byte â the encrypted payload
fmt.Println(result.KeyVersion) // int â the key version used
fmt.Println(result.RequestID) // string â UUID for tracing
Hybrid encryption (Pro/Enterprise): Pass
Algorithm: "X-Wing"for hybrid X25519 + ML-KEM-768:result, err := client.KEM.Encrypt(ctx, &qpher.EncryptInput{
Plaintext: []byte("sensitive data to protect"),
KeyVersion: 1,
Algorithm: "X-Wing",
})
| Field | Type | Required | Description |
|---|---|---|---|
Plaintext | []byte | Yes | The data to encrypt |
KeyVersion | int | Yes | Key version to use (must be active) |
Algorithm | string | No | "Kyber768" (default) or "X-Wing" (hybrid, Pro/Enterprise) |
Mode | string | No | "standard" (default) or "deterministic" |
Salt | []byte | No | Required when Mode is "deterministic" |
Returns: (*EncryptResult, error) where EncryptResult has fields Ciphertext ([]byte), KeyVersion (int), and RequestID (string).
Decryptâ
result, err := client.KEM.Decrypt(ctx, &qpher.DecryptInput{
Ciphertext: encryptedCiphertext,
KeyVersion: 1,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Plaintext) // []byte â the original data
fmt.Println(result.RequestID) // string
| Field | Type | Required | Description |
|---|---|---|---|
Ciphertext | []byte | Yes | The encrypted payload to decrypt |
KeyVersion | int | Yes | Key version used during encryption (active or retired) |
Algorithm | string | No | "Kyber768" (default) or "X-Wing" (hybrid, Pro/Enterprise) |
Returns: (*DecryptResult, error) where DecryptResult has fields Plaintext ([]byte) and RequestID (string).
Qpher does not support implicit "latest" key selection. You must specify the KeyVersion for every operation to ensure auditability and prevent silent key-mismatch errors.
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, err := client.KEM.Wrap(ctx, &qpher.KeyWrapInput{
SymmetricKey: aesKey, // []byte â 16, 24, 32, 48, or 64 bytes
KeyVersion: 1,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.WrappedKey) // []byte â store this safely
fmt.Println(result.KeyVersion) // int
fmt.Println(result.Algorithm) // string â "Kyber768"
// Unwrap to recover the original key
unwrapped, err := client.KEM.Unwrap(ctx, &qpher.KeyUnwrapInput{
WrappedKey: result.WrappedKey,
KeyVersion: 1,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(unwrapped.SymmetricKey) // []byte â the original key
| Field | Type | Required | Description |
|---|---|---|---|
SymmetricKey / WrappedKey | []byte | Yes | The symmetric key to wrap, or the wrapped key to unwrap |
KeyVersion | int | Yes | Key version (wrap: active only; unwrap: active or retired) |
Algorithm | string | 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, err := client.KEM.Encapsulate(ctx, &qpher.EncapsulateInput{
KeyVersion: 1,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.SharedSecret) // []byte â 32-byte shared secret
fmt.Println(result.KEMCiphertext) // []byte â send this to the decapsulator
// Decapsulate â recovers the shared secret
decap, err := client.KEM.Decapsulate(ctx, &qpher.DecapsulateInput{
KEMCiphertext: result.KEMCiphertext,
KeyVersion: 1,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(decap.SharedSecret) // []byte â same 32-byte shared secret
| Field | Type | Required | Description |
|---|---|---|---|
KEMCiphertext | []byte | Yes (Decapsulate) | The KEM ciphertext from Encapsulate |
KeyVersion | int | Yes | Key version to use |
Algorithm | string | 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, err := client.KEM.EncryptLocal(ctx, &qpher.EncryptLocalInput{
Plaintext: []byte("ultra-sensitive data"),
KeyVersion: 1,
})
if err != nil {
log.Fatal(err)
}
// envelope contains: KEMCiphertext, IV, AESCiphertext, KeyVersion, Algorithm
// Decrypt locally
plaintext, err := client.KEM.DecryptLocal(ctx, envelope)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(plaintext)) // "ultra-sensitive data"
| Field | Type | Required | Description |
|---|---|---|---|
Plaintext | []byte | Yes | Data to encrypt (EncryptLocal) |
envelope | *EncryptedEnvelope | Yes | The envelope from EncryptLocal (DecryptLocal) |
KeyVersion | int | Yes | Key version to use (EncryptLocal) |
Algorithm | string | No | "Kyber768" (default) or "X-Wing" (hybrid) |
With EncryptLocal / DecryptLocal, 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. Memory is zeroed after use.
Digital Signaturesâ
Qpher uses Dilithium3 (ML-DSA-65) for post-quantum digital signatures.
Signâ
result, err := client.Signatures.Sign(ctx, &qpher.SignInput{
Message: []byte("document content to sign"),
KeyVersion: 1,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Signature) // []byte â the Dilithium3 signature
fmt.Println(result.KeyVersion) // int
fmt.Println(result.RequestID) // string
Hybrid signatures (Pro/Enterprise): Pass
Algorithm: "Composite-ML-DSA"for hybrid ECDSA + ML-DSA-65:result, err := client.Signatures.Sign(ctx, &qpher.SignInput{
Message: []byte("document content to sign"),
KeyVersion: 1,
Algorithm: "Composite-ML-DSA",
})
| Field | Type | Required | Description |
|---|---|---|---|
Message | []byte | Yes | The message to sign |
KeyVersion | int | Yes | Signing key version (must be active) |
Algorithm | string | No | "Dilithium3" (default) or "Composite-ML-DSA" (hybrid, Pro/Enterprise) |
Returns: (*SignResult, error) where SignResult has fields Signature ([]byte), KeyVersion (int), and RequestID (string).
Verifyâ
result, err := client.Signatures.Verify(ctx, &qpher.VerifyInput{
Message: []byte("document content to sign"),
Signature: signatureBytes,
KeyVersion: 1,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Valid) // bool â true if the signature is valid
fmt.Println(result.KeyVersion) // int
fmt.Println(result.RequestID) // string
| Field | Type | Required | Description |
|---|---|---|---|
Message | []byte | Yes | The original message |
Signature | []byte | Yes | The signature to verify |
KeyVersion | int | Yes | Key version used during signing (active or retired) |
Algorithm | string | No | "Dilithium3" (default) or "Composite-ML-DSA" (hybrid, Pro/Enterprise) |
Returns: (*VerifyResult, error) where VerifyResult has fields Valid (bool), KeyVersion (int), Algorithm (string), and RequestID (string).
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 "crypto/sha256"
// Compute hash locally
data, _ := os.ReadFile("artifact.tar.gz")
hash := sha256.Sum256(data)
// Sign the hash â only 32 bytes sent to Qpher, not the file
result, err := client.Signatures.SignHash(ctx, &qpher.SignHashInput{
Hash: hash[:],
HashAlgorithm: "SHA-256",
KeyVersion: 1,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Signature) // []byte â detached signature
fmt.Println(result.SignatureType) // string â "detached"
| Field | Type | Required | Description |
|---|---|---|---|
Hash | []byte | Yes | Pre-computed hash (SHA-256: 32B, SHA-384: 48B, SHA-512: 64B) |
HashAlgorithm | string | Yes | "SHA-256", "SHA-384", or "SHA-512" |
KeyVersion | int | Yes | Signing key version (must be active) |
Algorithm | string | No | "Dilithium3" (default) or "Composite-ML-DSA" (hybrid) |
Returns: (*SignHashResult, error) where SignHashResult has fields Signature ([]byte), KeyVersion (int), Algorithm (string), HashAlgorithm (string), SignatureType (string), and RequestID (string).
Verify Hashâ
result, err := client.Signatures.VerifyHash(ctx, &qpher.VerifyHashInput{
Hash: hash[:],
HashAlgorithm: "SHA-256",
Signature: signatureBytes,
KeyVersion: 1,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Valid: %t\n", result.Valid) // true if signature matches
| Field | Type | Required | Description |
|---|---|---|---|
Hash | []byte | Yes | The same hash that was signed |
HashAlgorithm | string | Yes | "SHA-256", "SHA-384", or "SHA-512" |
Signature | []byte | Yes | The signature to verify |
KeyVersion | int | Yes | Key version used during signing (active or retired) |
Algorithm | string | No | "Dilithium3" (default) or "Composite-ML-DSA" (hybrid) |
Returns: (*VerifyHashResult, error) where VerifyHashResult has fields Valid (bool), KeyVersion (int), Algorithm (string), HashAlgorithm (string), and RequestID (string).
Key Managementâ
Manage PQC key lifecycle: generate, list, get, rotate, and retire keys.
Generate Keyâ
result, err := client.Keys.Generate(ctx, &qpher.GenerateInput{
Algorithm: "Kyber768",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.KeyVersion) // int â the new key version
fmt.Println(result.Algorithm) // string â "Kyber768"
fmt.Println(result.Status) // string â "active"
fmt.Println(result.PublicKey) // []byte â the public key (private key stays in Qpher)
fmt.Println(result.CreatedAt) // string â ISO-8601 timestamp
fmt.Println(result.RequestID) // string â UUID for tracing
| Field | Type | Required | Description |
|---|---|---|---|
Algorithm | string | Yes | "Kyber768", "Dilithium3", "X-Wing", or "Composite-ML-DSA" |
Returns: (*GenerateResult, error) where GenerateResult has fields KeyVersion (int), Algorithm (string), Status (string), PublicKey ([]byte), CreatedAt (string), and RequestID (string).
List Keysâ
result, err := client.Keys.List(ctx, &qpher.ListKeysInput{
Algorithm: "Kyber768",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Total keys: %d\n", result.Total)
for _, key := range result.Keys {
fmt.Printf("Version %d: %s\n", key.KeyVersion, key.Status)
// Version 1: retired
// Version 2: active
}
| Field | Type | Required | Description |
|---|---|---|---|
Algorithm | string | No | Filter by "Kyber768", "Dilithium3", "X-Wing", or "Composite-ML-DSA" |
Status | string | No | Filter by "active", "retired", or "archived" |
Returns: (*KeyListResult, error) where KeyListResult has fields Keys ([]KeyInfo), Total (int), and RequestID (string). Each KeyInfo has KeyVersion (int), Algorithm (string), Status (string), CreatedAt (string), and PublicKey ([]byte).
Get Active Keyâ
key, err := client.Keys.GetActive(ctx, &qpher.GetActiveInput{
Algorithm: "Kyber768",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(key.KeyVersion) // int â the currently active version
fmt.Println(key.Algorithm) // string â "Kyber768"
fmt.Println(key.PublicKey) // []byte â the public key
Get Key by Versionâ
key, err := client.Keys.Get(ctx, &qpher.GetKeyInput{
Algorithm: "Kyber768",
KeyVersion: 1,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(key.KeyVersion) // int â 1
fmt.Println(key.Status) // string â "active", "retired", or "archived"
fmt.Println(key.PublicKey) // []byte â the public key
fmt.Println(key.CreatedAt) // string â ISO-8601 timestamp
Rotate Keyâ
newKey, err := client.Keys.Rotate(ctx, &qpher.RotateInput{
Algorithm: "Kyber768",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(newKey.KeyVersion) // int â the new active version
fmt.Println(newKey.OldKeyVersion) // int â the previous version (now retired)
fmt.Println(newKey.PublicKey) // []byte â the new public key
fmt.Println(newKey.RequestID) // string â UUID for tracing
Returns: (*RotateResult, error) where RotateResult has fields KeyVersion (int), Algorithm (string), PublicKey ([]byte), OldKeyVersion (int), and RequestID (string).
Retire Keyâ
result, err := client.Keys.Retire(ctx, &qpher.RetireInput{
Algorithm: "Kyber768",
KeyVersion: 1,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Status) // string â "retired"
// Key version 1 is now retired â decrypt/verify still work, encrypt/sign do not
Returns: (*RetireResult, error) where RetireResult has fields KeyVersion (int), Algorithm (string), Status (string), PublicKey ([]byte), CreatedAt (string), and RequestID (string).
Retired keys can still decrypt and verify, but cannot encrypt or sign. Archived keys cannot perform any operations. Plan your rotation strategy accordingly. Key archival is intentionally not available in the SDK â it must be performed through the Portal UI to prevent accidental private key destruction.
Error Handlingâ
API errors are returned as *qpher.Error, which implements the error interface. Use errors.As to extract structured fields.
import "errors"
result, err := client.KEM.Encrypt(ctx, &qpher.EncryptInput{
Plaintext: []byte("data"),
KeyVersion: 999,
})
if err != nil {
var qErr *qpher.Error
if errors.As(err, &qErr) {
fmt.Println(qErr.Code) // string â e.g., "ERR_KEM_005"
fmt.Println(qErr.Message) // string â human-readable description
fmt.Println(qErr.StatusCode) // int â HTTP status code, e.g., 404
fmt.Println(qErr.RequestID) // string â UUID for support inquiries
} else {
fmt.Println("Unexpected error:", err)
}
}
Error Type Helpersâ
The *qpher.Error type provides convenience methods for checking error categories:
var qErr *qpher.Error
if errors.As(err, &qErr) {
if qErr.IsNotFoundError() {
fmt.Println("Key version does not exist")
} else if qErr.IsRateLimitError() {
fmt.Println("Rate limit exceeded â SDK already retried")
} else if qErr.IsAuthenticationError() {
fmt.Println("Invalid API key")
}
}
| Method | Status Code | Description |
|---|---|---|
IsAuthenticationError() | 401 | Invalid or missing API key |
IsValidationError() | 400 | Invalid request parameters |
IsNotFoundError() | 404 | Resource not found |
IsForbiddenError() | 403 | Operation not allowed by policy |
IsRateLimitError() | 429 | Rate limit exceeded |
IsServerError() | 500+ | Internal server error |
IsTimeoutError() | 504 | Request timed out |
IsConnectionError() | 503 | Service unavailable |
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 MaxRetries attempts, the final *qpher.Error is returned.
Full Round-Trip Exampleâ
This example demonstrates a complete encrypt-then-decrypt workflow with proper error handling.
package main
import (
"context"
"fmt"
"log"
"os"
qpher "github.com/qpher/qpher-go"
)
func main() {
ctx := context.Background()
client, err := qpher.NewClient(os.Getenv("QPHER_API_KEY"), nil)
if err != nil {
log.Fatal(err)
}
// 1. Get the active Kyber768 key
activeKey, err := client.Keys.GetActive(ctx, &qpher.GetActiveInput{
Algorithm: "Kyber768",
})
if err != nil {
log.Fatal(err)
}
// 2. Encrypt sensitive data
plaintext := []byte("Patient record: John Doe, DOB 1990-01-15")
enc, err := client.KEM.Encrypt(ctx, &qpher.EncryptInput{
Plaintext: plaintext,
KeyVersion: activeKey.KeyVersion,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Encrypted %d bytes, request: %s\n", len(enc.Ciphertext), enc.RequestID)
// 3. Decrypt to recover the original data
dec, err := client.KEM.Decrypt(ctx, &qpher.DecryptInput{
Ciphertext: enc.Ciphertext,
KeyVersion: enc.KeyVersion,
})
if err != nil {
log.Fatal(err)
}
// 4. Verify round-trip integrity
if string(dec.Plaintext) == string(plaintext) {
fmt.Println("Round-trip successful: plaintext matches")
}
}
Sign and Verify Exampleâ
package main
import (
"context"
"fmt"
"log"
"os"
qpher "github.com/qpher/qpher-go"
)
func main() {
ctx := context.Background()
client, err := qpher.NewClient(os.Getenv("QPHER_API_KEY"), nil)
if err != nil {
log.Fatal(err)
}
// Get the active Dilithium3 signing key
signingKey, err := client.Keys.GetActive(ctx, &qpher.GetActiveInput{
Algorithm: "Dilithium3",
})
if err != nil {
log.Fatal(err)
}
// Sign a document
document := []byte("Invoice #1234: $5,000.00 due 2026-03-01")
signResult, err := client.Signatures.Sign(ctx, &qpher.SignInput{
Message: document,
KeyVersion: signingKey.KeyVersion,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Signature: %d bytes\n", len(signResult.Signature))
// Verify the signature
verifyResult, err := client.Signatures.Verify(ctx, &qpher.VerifyInput{
Message: document,
Signature: signResult.Signature,
KeyVersion: signResult.KeyVersion,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Valid: %t\n", verifyResult.Valid) // true
}
Context and Cancellationâ
Every SDK method accepts a context.Context as its first argument. Use this for timeouts and cancellation:
// With a 5-second timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := client.KEM.Encrypt(ctx, &qpher.EncryptInput{
Plaintext: []byte("data"),
KeyVersion: 1,
})
// If the request takes more than 5 seconds, err will be a context.DeadlineExceeded
Configuration Referenceâ
Environment Variablesâ
The SDK respects these environment variables as fallbacks:
| Variable | Purpose |
|---|---|
QPHER_API_KEY | API key (used if the apiKey argument is empty) |
QPHER_BASE_URL | Base URL (used if BaseURL option is empty) |
The SDK never logs your API key or response bodies containing ciphertext. Sensitive fields are redacted in debug output.
Next Stepsâ
- Python SDK â if your backend uses Python
- Node.js SDK â if your backend uses JavaScript or TypeScript
- REST API â for raw HTTP access from any language
- API Reference â full OpenAPI specification