Signature API
The Signature API provides post-quantum digital signatures using the ML-DSA-65 algorithm (FIPS 204; the API wire value is Dilithium3). Use it to sign messages and verify signatures with quantum-resistant security.
Signing happens inside Qpher's isolated key service (KMS-Orchestrator). Your private signing keys are never exposed or exported.
Pass "algorithm": "Composite-ML-DSA" in sign/verify requests to use hybrid signatures
(ECDSA P-256 + ML-DSA-65). If omitted, the default is "Dilithium3" (PQC-only, backward-compatible).
Both ECDSA and ML-DSA components must verify for the signature to be valid, ensuring security if either algorithm is compromised.
Important: Composite ML-DSA signatures use Qpher's internal wire format and must be verified using this API. They are not interoperable with external Composite ML-DSA implementations.
See Security Architecture for details.
Pass "algorithm": "ML-DSA-87" to use the NIST Category 5 (highest-strength)
ML-DSA parameter set â the general-signature algorithm named in the NSA CNSA 2.0
suite. Most callers select it via the SDK compliance_profile="cnsa-2.0"
selector rather than per call: the SDK then pins this algorithm on every
request and raises on an override. It also sends an
X-Compliance-Profile: cnsa-2.0 header to signal intent, but the API does not
consume that header â the pinning is client-side. This is algorithm
alignment with CNSA 2.0, not a compliance certification.
Pass "algorithm": "SLH-DSA-SHA2-128s", "âĻ-128f", "âĻ-192s", or "âĻ-256s"
to sign with SLH-DSA, the NIST hash-based signature standard (FIPS 205).
Its security rests on hash functions rather than lattices, giving cryptographic
diversification for long-term / archival use. Signatures are larger (~7.8â30 KB)
and signing is slower (~0.3â2 s) than ML-DSA-65; 128s is the smallest and
192s/256s suit long-lived documents. Qpher implements the FIPS 205
algorithms â this is not a FIPS validation or certification. See the
algorithm table for parameter
sets and sizes.
Signâ
Signs the provided message with your active key (or the active version you pin with key_version).
Request bodyâ
| Field | Type | Required | Description |
|---|---|---|---|
message | string (base64) | Yes | Base64-encoded message to sign. |
key_version | integer | No | Omit to use your active key (the response returns the version used). If supplied, it must reference an active key. |
algorithm | string | No | "Dilithium3" (default, all plans), "Composite-ML-DSA", or "SLH-DSA-SHA2-128s/128f/192s/256s" (Composite and SLH-DSA require Starter+). If omitted, defaults to ML-DSA-65. |
Response (200 OK)â
| Field | Type | Description |
|---|---|---|
data.signature | string (base64) | The signature, base64-encoded. |
data.key_version | integer | The key version used for signing. |
data.algorithm | string | "Dilithium3", "Composite-ML-DSA", or "SLH-DSA-SHA2-*", matching the algorithm used for the operation. |
Exampleâ
/api/v1/signature/signX-API-Key: qph_your_key_here
Content-Type: application/json{
"message": "SW52b2ljZSAjMTIzNCAtIFRvdGFsOiAkOTkuMDA="
}{
"data": {
"signature": "c2lnbmF0dXJlX2J5dGVzX2hlcmUuLi4=",
"key_version": 1,
"algorithm": "Dilithium3"
},
"request_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"timestamp": "2026-01-15T10:30:02.000Z"
}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": "SW52b2ljZSAjMTIzNCAtIFRvdGFsOiAkOTkuMDA="
}'Errorsâ
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | ERR_SIG_001 | Invalid sign request â missing message, bad base64 encoding, or invalid key_version. |
| 400 | ERR_SIG_002 | Message exceeds maximum allowed size. |
| 401 | ERR_AUTH_001 | Missing or invalid API key. |
| 404 | ERR_NOT_FOUND_001 | Key version not found or key is not in active status. |
| 429 | ERR_RATE_LIMIT_001 | Rate limit exceeded â reduce request frequency or upgrade your plan. |
Verifyâ
Verifies that a signature is valid for the given message and key version. Verification can use keys in active or retired status.
Request bodyâ
| Field | Type | Required | Description |
|---|---|---|---|
message | string (base64) | Yes | Base64-encoded original message that was signed. |
signature | string (base64) | Yes | Base64-encoded signature to verify. |
key_version | integer | Yes | The key version that was used to create the signature. |
algorithm | string | No | "Dilithium3" (default, all plans), "Composite-ML-DSA", or "SLH-DSA-SHA2-128s/128f/192s/256s" (Composite and SLH-DSA require Starter+). If omitted, defaults to ML-DSA-65. |
Response (200 OK)â
| Field | Type | Description |
|---|---|---|
data.valid | boolean | true if the signature is valid for the given message and key, false otherwise. |
data.key_version | integer | The key version used for verification. |
data.algorithm | string | "Dilithium3", "Composite-ML-DSA", or "SLH-DSA-SHA2-*", matching the algorithm used for the operation. |
Exampleâ
/api/v1/signature/verifyX-API-Key: qph_your_key_here
Content-Type: application/json{
"message": "SW52b2ljZSAjMTIzNCAtIFRvdGFsOiAkOTkuMDA=",
"signature": "c2lnbmF0dXJlX2J5dGVzX2hlcmUuLi4=",
"key_version": 1
}{
"data": {
"valid": true,
"key_version": 1,
"algorithm": "Dilithium3"
},
"request_id": "d4e5f6a7-b8c9-0123-4567-890abcdef012",
"timestamp": "2026-01-15T10:30:03.000Z"
}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": "SW52b2ljZSAjMTIzNCAtIFRvdGFsOiAkOTkuMDA=",
"signature": "c2lnbmF0dXJlX2J5dGVzX2hlcmUuLi4=",
"key_version": 1
}'Errorsâ
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | ERR_SIG_001 | Invalid verify request â bad base64 encoding for message or signature. |
| 400 | ERR_SIG_002 | Message or signature exceeds maximum allowed size. |
| 401 | ERR_AUTH_001 | Missing or invalid API key. |
| 404 | ERR_NOT_FOUND_001 | Key version not found or key is in archived status. |
| 429 | ERR_RATE_LIMIT_001 | Rate limit exceeded â reduce request frequency or upgrade your plan. |
A signature that does not match returns {"valid": false} with HTTP 200 â not an error. Errors (4xx/5xx) indicate malformed requests or missing resources, not invalid signatures.
Sign Hashâ
Signs a pre-computed hash digest instead of the raw message. This is ideal for large files or artifacts where you compute the hash client-side and send only the digest. The key must be in active status.
Request bodyâ
| Field | Type | Required | Description |
|---|---|---|---|
hash | string (base64) | Yes | Base64-encoded hash digest to sign. |
hash_algorithm | string | Yes | Hash algorithm used to compute the digest: "SHA-256", "SHA-384", or "SHA-512". |
key_version | integer | No | Omit to use your active key (the response returns the version used). If supplied, it must reference an active key. |
algorithm | string | No | "Dilithium3" (default, all plans), "Composite-ML-DSA", or "SLH-DSA-SHA2-128s/128f/192s/256s" (Composite and SLH-DSA require Starter+). If omitted, defaults to ML-DSA-65. |
Response (200 OK)â
| Field | Type | Description |
|---|---|---|
data.signature | string (base64) | The signature, base64-encoded. |
data.key_version | integer | The key version used for signing. |
data.algorithm | string | "Dilithium3", "Composite-ML-DSA", or "SLH-DSA-SHA2-*", matching the algorithm used for the operation. |
data.hash_algorithm | string | The hash algorithm that was specified in the request. |
data.signature_type | string | Always "detached" â indicates the signature covers a hash, not the raw message. |
Exampleâ
curl -X POST https://api.qpher.ai/api/v1/signature/sign-hash \
-H "X-API-Key: qph_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"hash": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=",
"hash_algorithm": "SHA-256",
"key_version": 1
}'Errorsâ
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | ERR_SIG_010 | Unsupported hash algorithm â use SHA-256, SHA-384, or SHA-512. |
| 400 | ERR_SIG_011 | Hash length does not match declared hash_algorithm (SHA-256=32, SHA-384=48, SHA-512=64 bytes). |
| 401 | ERR_AUTH_001 | Missing or invalid API key. |
| 404 | ERR_NOT_FOUND_001 | Key version not found or key is not in active status. |
| 429 | ERR_RATE_LIMIT_001 | Rate limit exceeded â reduce request frequency or upgrade your plan. |
Verify Hashâ
Verifies that a signature is valid for the given hash digest and key version. Verification can use keys in active or retired status.
Request bodyâ
| Field | Type | Required | Description |
|---|---|---|---|
hash | string (base64) | Yes | Base64-encoded hash digest that was signed. |
hash_algorithm | string | Yes | Hash algorithm used to compute the digest: "SHA-256", "SHA-384", or "SHA-512". |
signature | string (base64) | Yes | Base64-encoded signature to verify. |
key_version | integer | Yes | The key version that was used to create the signature. |
algorithm | string | No | "Dilithium3" (default, all plans), "Composite-ML-DSA", or "SLH-DSA-SHA2-128s/128f/192s/256s" (Composite and SLH-DSA require Starter+). If omitted, defaults to ML-DSA-65. |
Response (200 OK)â
| Field | Type | Description |
|---|---|---|
data.valid | boolean | true if the signature is valid for the given hash and key, false otherwise. |
data.key_version | integer | The key version used for verification. |
data.algorithm | string | "Dilithium3", "Composite-ML-DSA", or "SLH-DSA-SHA2-*", matching the algorithm used for the operation. |
data.hash_algorithm | string | The hash algorithm that was specified in the request. |
Exampleâ
curl -X POST https://api.qpher.ai/api/v1/signature/verify-hash \
-H "X-API-Key: qph_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"hash": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=",
"hash_algorithm": "SHA-256",
"signature": "c2lnbmF0dXJlX2J5dGVzX2hlcmUuLi4=",
"key_version": 1
}'Errorsâ
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | ERR_SIG_010 | Unsupported hash algorithm â use SHA-256, SHA-384, or SHA-512. |
| 400 | ERR_SIG_011 | Hash length does not match declared hash_algorithm (SHA-256=32, SHA-384=48, SHA-512=64 bytes). |
| 401 | ERR_AUTH_001 | Missing or invalid API key. |
| 404 | ERR_NOT_FOUND_001 | Key version not found or key is in archived status. |
| 429 | ERR_RATE_LIMIT_001 | Rate limit exceeded â reduce request frequency or upgrade your plan. |
A signature that does not match returns {"valid": false} with HTTP 200 â not an error. Errors (4xx/5xx) indicate malformed requests or missing resources, not invalid signatures.
Key Version Requirementsâ
On sign and sign-hash, key_version is optional â omit it and Qpher uses your active key and returns its version. On verify and verify-hash it is required.
| Operation | Allowed Key Statuses |
|---|---|
| Sign | active only |
| Verify | active or retired |
| Sign Hash | active only |
| Verify Hash | active or retired |
Use the Key Management API to list your keys and find the current active version.