Skip to main content

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-go

Requirements: 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)
}
}
FieldTypeDefaultDescription
apiKeystringrequiredYour Qpher API key (starts with qph_)
BaseURLstringhttps://api.qpher.aiAPI base URL
Timeoutint30Request timeout in seconds
MaxRetriesint3Retries for transient errors (429, 502, 503, 504)

Pass nil as the second argument to NewClient to use all defaults.

Protect your API key

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",
})
FieldTypeRequiredDescription
Plaintext[]byteYesThe data to encrypt
KeyVersionintYesKey version to use (must be active)
AlgorithmstringNo"Kyber768" (default) or "X-Wing" (hybrid, Pro/Enterprise)
ModestringNo"standard" (default) or "deterministic"
Salt[]byteNoRequired 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
FieldTypeRequiredDescription
Ciphertext[]byteYesThe encrypted payload to decrypt
KeyVersionintYesKey version used during encryption (active or retired)
AlgorithmstringNo"Kyber768" (default) or "X-Wing" (hybrid, Pro/Enterprise)

Returns: (*DecryptResult, error) where DecryptResult has fields Plaintext ([]byte) and RequestID (string).

Key version is always required

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
FieldTypeRequiredDescription
SymmetricKey / WrappedKey[]byteYesThe symmetric key to wrap, or the wrapped key to unwrap
KeyVersionintYesKey version (wrap: active only; unwrap: active or retired)
AlgorithmstringNo"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
FieldTypeRequiredDescription
KEMCiphertext[]byteYes (Decapsulate)The KEM ciphertext from Encapsulate
KeyVersionintYesKey version to use
AlgorithmstringNo"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"
FieldTypeRequiredDescription
Plaintext[]byteYesData to encrypt (EncryptLocal)
envelope*EncryptedEnvelopeYesThe envelope from EncryptLocal (DecryptLocal)
KeyVersionintYesKey version to use (EncryptLocal)
AlgorithmstringNo"Kyber768" (default) or "X-Wing" (hybrid)
True client-side encryption

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",
})
FieldTypeRequiredDescription
Message[]byteYesThe message to sign
KeyVersionintYesSigning key version (must be active)
AlgorithmstringNo"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
FieldTypeRequiredDescription
Message[]byteYesThe original message
Signature[]byteYesThe signature to verify
KeyVersionintYesKey version used during signing (active or retired)
AlgorithmstringNo"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"
FieldTypeRequiredDescription
Hash[]byteYesPre-computed hash (SHA-256: 32B, SHA-384: 48B, SHA-512: 64B)
HashAlgorithmstringYes"SHA-256", "SHA-384", or "SHA-512"
KeyVersionintYesSigning key version (must be active)
AlgorithmstringNo"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
FieldTypeRequiredDescription
Hash[]byteYesThe same hash that was signed
HashAlgorithmstringYes"SHA-256", "SHA-384", or "SHA-512"
Signature[]byteYesThe signature to verify
KeyVersionintYesKey version used during signing (active or retired)
AlgorithmstringNo"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
FieldTypeRequiredDescription
AlgorithmstringYes"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
}
FieldTypeRequiredDescription
AlgorithmstringNoFilter by "Kyber768", "Dilithium3", "X-Wing", or "Composite-ML-DSA"
StatusstringNoFilter 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).

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. 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")
}
}
MethodStatus CodeDescription
IsAuthenticationError()401Invalid or missing API key
IsValidationError()400Invalid request parameters
IsNotFoundError()404Resource not found
IsForbiddenError()403Operation not allowed by policy
IsRateLimitError()429Rate limit exceeded
IsServerError()500+Internal server error
IsTimeoutError()504Request timed out
IsConnectionError()503Service unavailable

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 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:

VariablePurpose
QPHER_API_KEYAPI key (used if the apiKey argument is empty)
QPHER_BASE_URLBase URL (used if BaseURL option is empty)
Security

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