PostQ API Reference
Submit scans and read your scan history. Two endpoints, three official SDKs, one Bearer token.
Introduction
The PostQ REST API lets you push scan results from any scanner, CI pipeline, or service into your PostQ organisation, and read your scan history back out.
The current public surface is intentionally small — two endpoints, both stable. Hybrid signing, key listing, and policy APIs will land in their own minor versions when they ship; see Roadmap.
Base URL: https://api.postq.dev. Public, versioned routes live under /v1.
Quickstart
Install an SDK, submit your first scan, and view it in the dashboard — about five minutes.
1. Get an API key
Contact us to request a key. It looks like pq_live_….
2. Install an SDK (optional — you can also use curl)
npm install @postq/sdk3. Submit a scan
import { PostQ } from "@postq/sdk";
const pq = new PostQ({ apiKey: process.env.POSTQ_API_KEY! });
const result = await pq.scans.submit({
type: "url",
target: "example.com",
riskScore: 85,
riskLevel: "High",
findings: [{ severity: "high", title: "RSA-2048 public key" }],
});
console.log(result.url);Authentication
Every request to the public /v1 API must include a Bearer token. Keys are scoped to your organisation and can be created, listed, and revoked from Settings → API Keys.
Authorization: Bearer pq_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxServer-side only. Never embed pq_live_… keys in client-side code, mobile apps, or browser extensions. Use environment variables and call from a backend you control.
POST /v1/scans
Submit a scan result. The body describes one assessed target, its risk level, and any findings. The response includes a permalink to the scan in the dashboard.
POST /v1/scans
Host: api.postq.dev
Authorization: Bearer pq_live_…
Content-Type: application/json
{
"type": "url", // "url" | "repo" | "k8s" | "cloud" | string
"target": "example.com", // human-readable identifier
"riskScore": 85, // 0..100
"riskLevel": "High", // "Critical" | "High" | "Medium" | "Low" | "Info"
"findings": [
{
"severity": "high",
"title": "RSA-2048 public key",
"description": "Optional details",
"evidence": "Optional supporting data"
}
],
"source": "sdk", // optional, defaults to "sdk"
"metadata": { "ci": "github-actions" } // optional, free-form
}200 OK
Content-Type: application/json
{
"id": "scan_01H…",
"url": "https://postq.dev/scans/scan_01H…",
"type": "url",
"target": "example.com",
"riskScore": 85,
"riskLevel": "High",
"createdAt": "2026-04-24T18:30:00.000Z"
}GET /v1/scans
List scans for your organisation, newest first. Cursor-based pagination — results never reorder, even as new scans arrive.
| Query param | Type | Description |
|---|---|---|
| limit | integer, 1–100 | Page size. Default 50. |
| cursor | string | Opaque cursor from previous nextCursor. |
GET /v1/scans?limit=50&cursor=… HTTP/1.1
Host: api.postq.dev
Authorization: Bearer pq_live_…200 OK
Content-Type: application/json
{
"data": [
{
"id": "scan_01H…",
"type": "url",
"target": "example.com",
"riskScore": 85,
"riskLevel": "High",
"createdAt": "2026-04-24T18:30:00.000Z"
}
],
"nextCursor": "eyJpZCI6InNjYW5fMDFI…" // null when no more pages
}GET /health
Liveness probe. Always returns 200 OK. Does not require authentication.
200 OK
Content-Type: application/json
{ "status": "ok" }Errors
Errors return a JSON body with a stable shape:
401 Unauthorized
Content-Type: application/json
{
"error": {
"code": "unauthorized",
"message": "Invalid or expired API key."
}
}| Status | When | SDK class |
|---|---|---|
| 400 | Validation failed | PostQError |
| 401 | Bad / missing API key | PostQAuthError |
| 404 | Resource not found | PostQNotFoundError |
| 429 | Rate limit exceeded | PostQRateLimitError |
| 5xx | Server error | PostQServerError |
Rate limits
Default limits while we’re in early access:
- POST /v1/scans: 60 requests / minute / API key
- GET /v1/scans: 600 requests / minute / API key
Need higher limits? Email support@postq.dev.
Scan model
A scan is one assessment of one target. Submitting the same type + target pair multiple times creates multiple scan records — we preserve history rather than overwriting. The dashboard groups them by target.
| Field | Type | Notes |
|---|---|---|
| type | string | e.g. url, repo, k8s, cloud |
| target | string | Human-readable identifier (hostname, repo URL, cluster) |
| riskScore | number, 0–100 | Your scanner’s aggregate score |
| riskLevel | enum | Critical, High, Medium, Low, Info |
| findings | Finding[] | See below |
| source | string | Optional. Defaults to sdk. |
| metadata | object | Optional, free-form key/value |
Findings
Each scan can include zero or more findings — specific quantum-vulnerable items detected on the target.
| Field | Type | Notes |
|---|---|---|
| severity | enum | critical, high, medium, low, info |
| title | string | Short label, e.g. “RSA-2048 public key” |
| description | string | Optional longer detail |
| evidence | string | Optional supporting data (cert excerpt, file path) |
Sources
The source field tells us where a scan came from. Common values:
sdk— default, any of the official SDKscli— the PostQ CLIagent— the in-cluster agent- Anything else — your own scanner or integration name
Kubernetes Agent
The PostQ agent runs inside your cluster and continuously discovers quantum-vulnerable cryptography across TLS Secrets, Ingresses, ConfigMaps, cert-manager Certificates, and Istio / Linkerd mTLS posture. Findings stream to your dashboard via POST /ingest/kubernetes. The agent and chart are open source at PostQDev/postq-scanner-tool.
Install
The chart is published as an OCI artifact in GHCR. Helm 3.8+ required.
helm install postq-agent oci://ghcr.io/postqdev/charts/postq-agent \
--version 0.2.0 \
--namespace postq --create-namespace \
--set apiKey=$POSTQ_API_KEY \
--set orgId=$POSTQ_ORG_ID \
--set clusterName=prod-east-1Bring your own Secret
For External Secrets / SOPS / Sealed Secrets workflows, point the chart at a Secret you provision out-of-band:
kubectl -n postq create secret generic postq-agent-key \
--from-literal=POSTQ_API_KEY=$POSTQ_API_KEY
helm install postq-agent oci://ghcr.io/postqdev/charts/postq-agent \
--version 0.2.0 \
--namespace postq \
--set existingSecret.name=postq-agent-key \
--set orgId=$POSTQ_ORG_ID \
--set clusterName=prod-east-1What it scans
kubernetes.io/tlsSecrets — leaf cert + private key analysis (algorithm, bit length, signature, expiry).- Ingresses — cross-references vulnerable certs that are actually serving traffic and bumps severity.
- ConfigMaps — finds embedded PEM blocks that escape Secret-only audits.
- cert-manager
Certificates,Issuers,ClusterIssuers— reads the desired spec, so you see vulnerabilities before they rotate into the cluster. - Istio
PeerAuthentication/DestinationRule/Gatewayand LinkerdMeshTLSAuthentication— service-mesh mTLS posture.
Defaults
| Image | ghcr.io/postqdev/postq-agent:0.2.0 (linux/amd64, linux/arm64) |
| Schedule | Hourly CronJob (override with --set schedule) |
| Pod identity | Non-root UID 65532, read-only root FS, all caps dropped, RuntimeDefault seccomp |
| RBAC | Read-only ClusterRole on secrets/configmaps/ingresses + optional cert-manager + Istio + Linkerd CRDs |
| Resources | requests 50m / 64Mi — limits 200m / 128Mi |
| Egress | One outbound HTTPS connection to https://api.postq.dev |
One-shot mode
Add --set oneShot=true to swap the CronJob for a single Job — useful for smoke tests and CI gating before you hand the cluster to ops.
PostQ CLI
For ad-hoc scans and CI gating, the PostQ CLI ships as a single 2 MB Go binary. It uses the same /v1/scans endpoint as the SDKs and exits 2when it finds a Critical or High — drop it into a pipeline as a hard gate.
brew install PostQDev/tap/postq
postq scan url example.com --concurrency 5Hybrid Signing
Hybrid signing is PostQ’s managed signing service. Every key signs with BOTH a NIST-standardized post-quantum algorithm (ML-DSA, FIPS 204) and classical Ed25519. Verification is an AND combiner: a future break in either algorithm alone does not allow forgery.
Three endpoints cover the full lifecycle. Private bytes never leave PostQ — they live AES-256-GCM-encrypted under your org-scoped KEK. Every /v1/sign and /v1/verify call writes a tamper-evident audit row that stores only sha256(payload), never the payload itself.
1. Create a managed signing key
Default algorithm is mldsa65+ed25519 (NIST Cat. 3, ~192-bit classical security). Mint a key from the dashboard or the SDK.
import { PostQ } from "@postq/sdk";
const pq = new PostQ({ apiKey: process.env.POSTQ_API_KEY! });
const key = await pq.hybridKeys.create({
name: "release-signing",
algorithm: "mldsa65+ed25519",
});
// key.publicKey is a JSON string you can publish — anyone can verify
// signatures offline against it without touching the API.2. Sign & verify
One call to pq.sign() returns a single composite signature (base64 JSON envelope holding both halves). Pass it back to pq.verify() with either the same keyId or the published publicKey for offline verification.
const { signature } = await pq.sign({
keyId: key.id,
payload: new TextEncoder().encode("ship it"),
});
const result = await pq.verify({
keyId: key.id,
payload: "ship it",
signature,
});
// result.ok === true
// result.classicalOk === true
// result.pqOk === trueSame calls in Python and .NET:
from postq import PostQ
pq = PostQ(api_key=...)
key = pq.hybrid_keys.create(name="release-signing")
sig = pq.sign(key_id=key.id, payload=b"ship it")
ok = pq.verify(
key_id=key.id,
payload=b"ship it",
signature=sig.signature,
).okusing var pq = new PostQClient(new() { ApiKey = "..." });
var key = await pq.HybridKeys.CreateAsync(new() {
Name = "release-signing"
});
var sig = await pq.SignAsync(new() {
KeyId = key.Id,
Payload = Encoding.UTF8.GetBytes("ship it"),
});
var result = await pq.VerifyAsync(new() {
KeyId = key.Id,
Payload = Encoding.UTF8.GetBytes("ship it"),
Signature = sig.Signature,
});Scopes: signing requires the sign:write scope on your API key; verification needs sign:read. New keys get sign:read by default. Mint a separate key with sign:write for production signing workloads.
3. Sign & verify from the CLI
The same operations are available from the PostQ CLI — handy for signing release artifacts in CI without writing glue code. postq verify exits with code 2 when a signature is rejected, so it slots straight into a build gate.
# Mint a key (one-off)
postq keys create --name release-signing
# Sign an artifact in CI
postq sign --key <key-id> \
--in dist/app.tar.gz \
--out dist/app.sig
# Verify before deploy — exits 2 on failure
postq verify --key <key-id> \
--in dist/app.tar.gz \
--sig dist/app.sigMitigations — Hybrid TLS Rollover
When the cloud scanner flags an Azure App Service with a weak TLS posture (TLS-1.0-MIN, TLS-1.1-MIN, or HTTPS-NOT-ENFORCED), PostQ can mint a real hybrid leaf certificate for that site — classical ECDSA-P256 chain plus a detached ML-DSA-65 sidecar signature embedded in a non-critical X.509 extension — and walk you through a staged rollover. PostQ never touches your subscription: every Azure call is in a generated az bundle you run yourself.
Open any qualifying finding from a cloud scan and click Hybrid TLS rollover. The dashboard returns a one-shot bundle containing the cert, the matching ECDSA private key (delivered once, never stored by PostQ), an Azure CLI script set, and a Bicep snippet.
1. The cert & the sidecar signature
The leaf is a normal ECDSA-P256 self-signed cert that every TLS 1.2/1.3 client validates today. The hybrid layer is a detached ML-DSA-65 (NIST FIPS 204, Cat. 3) signature over a deterministic bindingobject — algorithm, hostname, SANs, serial, validity window, and BOTH public keys. The signature plus the binding plus the PQ public key are packed into a non-critical X.509 v3 extension under OID 1.3.9999.42.1. Legacy clients ignore unknown non-critical extensions; PQ-aware verifiers (including PostQ’s scanner) validate the hybrid binding alongside the classical chain.
# Inspect the embedded extension on the minted cert
openssl x509 -in postq-hybrid.pem -noout -text \
| sed -n '/1.3.9999.42.1/,/^[A-Z]/p'The cert’s classical private key is generated ephemerally inside the API process and returned exactly once in the create response. PostQ stores only the public halves, the cert PEM, the ML-DSA-65 signature, and the rollover plan — enough to verify the binding on a live TLS handshake but never to forge one.
2. Azure rollover scripts
The bundle ships five artifacts. Run them from a directory holding the downloaded postq-hybrid.pem and postq-hybrid.key:
bundle.sh— the full sequence: build a PFX, import to Key Vault, grant the App Service’s managed identityget, bind the cert via SNI, force HTTPS-only and TLS 1.2 minimum, set FTPS-only.import.sh— just the Key Vault import step.bind.sh— just the App Service binding step.rollback.sh— unbinds the cert from the hostname and disables the cert version in Key Vault. The previous binding is preserved.site.bicep— idempotent Bicep equivalent of the binding for IaC users.
Required Azure permissions: Key Vault Certificates Officer on the target vault and Website Contributor on the App Service. The generated scripts are pure azCLI — no external tooling.
3. Rollover lifecycle
Every rollover walks a four-stage state machine. The dashboard exposes transition and rollback actions, and every transition writes a tamper-evident entry to the PostQ Ledger.
planned
Cert minted, sidecar signed, bundle delivered. No Azure state changed yet.
observation
Cert bound with SNI alongside any existing binding (no cutover). Watch handshake error rates for the configured window (default 24h).
cutover
Hybrid cert promoted to primary; HTTPS-only and minimum TLS 1.2 enforced. PostQ’s scanner re-checks the endpoint within 60s.
completed
Old TLS-1.0/1.1 minimum and any legacy cert retired. Hybrid binding is the source of truth.
rolled_back is a terminal state reachable from any other status. Running rollback.sh and POSTing to /v1/mitigations/tls-rollover/:id/rollback both leave the previous binding intact.
4. API surface
| Method | Path | Scope | Returns |
|---|---|---|---|
| POST | /v1/mitigations/tls-rollover | sign:write | cert + key + scripts + plan (one-shot) |
| GET | /v1/mitigations/tls-rollover | sign:read | list rollovers |
| GET | /v1/mitigations/tls-rollover/:id | sign:read | get one (no key bytes) |
| GET | /v1/mitigations/tls-rollover/:id/download | sign:read | artifact download (cert.pem, *.sh, *.bicep) |
| POST | /v1/mitigations/tls-rollover/:id/transition | sign:write | advance status |
| POST | /v1/mitigations/tls-rollover/:id/rollback | sign:write | mark rolled_back |
# Mint a hybrid TLS cert for an Azure App Service finding
curl -X POST https://api.postq.dev/v1/mitigations/tls-rollover \
-H "Authorization: Bearer $POSTQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"findingId": "8b3a…-uuid-of-the-tls-finding",
"hostname": "app.contoso.example"
}'
# Save the artifacts the response gives you (one-shot delivery).
# Then walk the staged plan:
curl -X POST https://api.postq.dev/v1/mitigations/tls-rollover/$ID/transition \
-H "Authorization: Bearer $POSTQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "observation" }'5. PQ verification recipe
Verifying the hybrid binding from a live TLS handshake is two steps: validate the classical ECDSA chain the way you always have, then re-derive the canonical binding bytes and check the ML-DSA-65 sidecar.
import { ml_dsa65 } from "@noble/post-quantum/ml-dsa";
import { X509Certificate } from "@peculiar/x509";
// 1. Pull leaf from a TLS handshake or PostQ /v1/mitigations endpoint.
const cert = new X509Certificate(certPem);
// 2. Read the PostQ hybrid extension (OID 1.3.9999.42.1).
// It contains: { algorithmOid, pqPublicKey, pqSignature, bindingBytes }
const ext = parsePostqHybridExt(cert.getExtension("1.3.9999.42.1"));
// 3. Verify ML-DSA-65 over the canonical binding.
const pqOk = ml_dsa65.verify(ext.signature, ext.binding, ext.publicKey);
// pqOk === true → hybrid binding is intactThe binding object is canonical UTF-8 JSON with field order: v, alg, hostname, altNames, serial, notBefore, notAfter, classicalPub, pqPub. PostQ’s scanner uses exactly this routine to flip an App Service’s hybrid-bound badge in the dashboard within 60s of cutover.
What’s next
The roadmap below is what we’re working on next. Endpoints are marked planneduntil they ship; the SDKs only ever expose what’s real today.
GET /v1/scans/:id
Fetch a single scan by id, including all findings.
POST /v1/sign · POST /v1/verify
Hybrid signing — classical Ed25519 + ML-DSA composite signatures. See the Hybrid Signing section above.
GET /v1/keys
List managed keys with algorithm, backend, and PQ-ready flag.
Policies, webhooks, OpenAPI spec
Programmatic policy management, push notifications for new Critical findings, and an auto-generated OpenAPI 3.1 document.
Ready to integrate?
Pick an SDK or hit the API directly with curl.