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
| Field | Type | Required | Description |
|---|---|---|---|
Plaintext | []byte | Yes | The data to encrypt |
KeyVersion | int | Yes | Key version to use (must be active) |
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) |
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.
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
| Field | Type | Required | Description |
|---|---|---|---|
Message | []byte | Yes | The message to sign |
KeyVersion | int | Yes | Signing key version (must be active) |
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) |
Returns: (*VerifyResult, error) where VerifyResult has fields Valid (bool), KeyVersion (int), Algorithm (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" or "Dilithium3" |
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" or "Dilithium3" |
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.
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