Documentation

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)

install
npm install @postq/sdk

3. Submit a scan

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 header
Authorization: Bearer pq_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
!

Server-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.

Request
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
}
Response
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 paramTypeDescription
limitinteger, 1–100Page size. Default 50.
cursorstringOpaque cursor from previous nextCursor.
Request
GET /v1/scans?limit=50&cursor=… HTTP/1.1
Host: api.postq.dev
Authorization: Bearer pq_live_…
Response
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.

Response
200 OK
Content-Type: application/json

{ "status": "ok" }

Errors

Errors return a JSON body with a stable shape:

Error response
401 Unauthorized
Content-Type: application/json

{
  "error": {
    "code": "unauthorized",
    "message": "Invalid or expired API key."
  }
}
StatusWhenSDK class
400Validation failedPostQError
401Bad / missing API keyPostQAuthError
404Resource not foundPostQNotFoundError
429Rate limit exceededPostQRateLimitError
5xxServer errorPostQServerError

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.

FieldTypeNotes
typestringe.g. url, repo, k8s, cloud
targetstringHuman-readable identifier (hostname, repo URL, cluster)
riskScorenumber, 0–100Your scanner’s aggregate score
riskLevelenumCritical, High, Medium, Low, Info
findingsFinding[]See below
sourcestringOptional. Defaults to sdk.
metadataobjectOptional, free-form key/value

Findings

Each scan can include zero or more findings — specific quantum-vulnerable items detected on the target.

FieldTypeNotes
severityenumcritical, high, medium, low, info
titlestringShort label, e.g. “RSA-2048 public key”
descriptionstringOptional longer detail
evidencestringOptional 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 SDKs
  • cli — the PostQ CLI
  • agent— 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-1

Bring 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-1

What it scans

  • kubernetes.io/tls Secrets — 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 / Gateway and Linkerd MeshTLSAuthentication — service-mesh mTLS posture.

Defaults

Imageghcr.io/postqdev/postq-agent:0.2.0 (linux/amd64, linux/arm64)
ScheduleHourly CronJob (override with --set schedule)
Pod identityNon-root UID 65532, read-only root FS, all caps dropped, RuntimeDefault seccomp
RBACRead-only ClusterRole on secrets/configmaps/ingresses + optional cert-manager + Istio + Linkerd CRDs
Resourcesrequests 50m / 64Mi — limits 200m / 128Mi
EgressOne 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 5

Hybrid 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 === true

Same 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,
).ok
using 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.sig

Mitigations — 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 identity get, 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

MethodPathScopeReturns
POST/v1/mitigations/tls-rolloversign:writecert + key + scripts + plan (one-shot)
GET/v1/mitigations/tls-rolloversign:readlist rollovers
GET/v1/mitigations/tls-rollover/:idsign:readget one (no key bytes)
GET/v1/mitigations/tls-rollover/:id/downloadsign:readartifact download (cert.pem, *.sh, *.bicep)
POST/v1/mitigations/tls-rollover/:id/transitionsign:writeadvance status
POST/v1/mitigations/tls-rollover/:id/rollbacksign:writemark 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 intact

The 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.

Planned

GET /v1/scans/:id

Fetch a single scan by id, including all findings.

Live

POST /v1/sign · POST /v1/verify

Hybrid signing — classical Ed25519 + ML-DSA composite signatures. See the Hybrid Signing section above.

Planned

GET /v1/keys

List managed keys with algorithm, backend, and PQ-ready flag.

Later

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.