pqdoctor has three JSON shapes:
Command
Output
Shape
pqdoctor <host> --json
One pretty-printed object
Report
pqdoctor scan <file> --json
One compact object per line (JSON Lines), in completion order
Scan record
pqdoctor ssh <host> --json
One pretty-printed object
SSH report
pqdoctor quic <host> --json
One pretty-printed object
QuicReport (0.2.0)
pqdoctor dtls <host:port> --json
One pretty-printed object
DtlsReport (0.2.0)
pqdoctor diff a b --json
One pretty-printed object
Diff (0.2.0)
Compatibility: fields added in 0.2.0 are left out of the JSON when their check didn’t run, so a 0.1.0 consumer reading a default library report sees exactly the 0.1.0 shape. Cause codes are only ever added, never renamed.
Field names are camelCase . Enum values are camelCase (verdicts) or kebab-case (cause codes).
TLS numbers (versions, cipher suites, groups, extensions) are plain decimal integers . 772 is TLS 1.3 (0x0304). 4588 is X25519MLKEM768 (0x11EC).
Optional fields are null unless noted as omitted when empty .
Times are Unix milliseconds.
headline and cause are human sentences and can change between versions. Match on verdict and causeCode , which are stable.
pqdoctor <host> --json.
Field
Type
host
string
The name checked, lowercased, without a trailing dot
port
number
ip
string | null
The address that was probed
verdict
Verdict
headline
string
One-line result, e.g. "Not post-quantum"
cause
string
The explanation sentence
causeCode
Cause
Stable reason code
group
string | null
The PQ group negotiated, e.g. "X25519MLKEM768". Only set when post-quantum
prefersPq
boolean
The server asked a client with only an X25519 share to retry with ML-KEM
warnings
string[]
E.g. split PQ hellos dropped, or PQ only when offered first
fixes
Fix []
In the order to try them. Empty when post-quantum
fingerprint
Fingerprint
probes
ProbeResult []
All seven probes, in order
verify
string
An openssl s_client command to confirm by hand
checkedAt
number
When the check ran
elapsedMs
number
Time for the main address’s check
auth
AuthReport
The certificate check. Omitted when it didn’t run
starttls
string
smtp, imap, pop3; from 0.2.0 also ftp, xmpp, ldap, postgres, mysql, rdp. Omitted when not used
addresses
AddressResult []
Every address, when the name has more than one. Omitted otherwise
groups
GroupSurvey
0.2.0. Omitted when the survey didn’t run
trust
TrustReport
0.2.0. Omitted when it didn’t run
signatures
SignatureReport
0.2.0, --slh-dsa. Omitted otherwise
audit
AuditReport
0.2.0, --audit. Omitted otherwise
middlebox
MiddleboxReport
0.2.0, --middlebox. Omitted otherwise
quic
QuicReport
0.2.0, --quic. Omitted otherwise
dtls
DtlsReport
0.2.0, --dtls. Omitted otherwise
proxy
string
0.2.0: the proxy used, without credentials, e.g. "socks5h://127.0.0.1:1080". Omitted without one
Under STARTTLS, the Cloudflare Tunnel fix is left out of fixes: a tunnel carries HTTP, not mail. With a proxy that resolves names (http://, socks5h://), ip is null.
Value
Meaning
Exit status
pq
Negotiates a post-quantum group
0
pqWithWarning
Post-quantum, with a warning (split PQ hellos dropped; not on every address; only a non-X25519 PQ group)
0
notPq
TLS 1.3 without post-quantum
1
noTls13
TLS 1.2 only, or no TLS handshake at all
1
broken
Something in the path breaks on the PQ hello
1
unreachable
Couldn’t connect, or nothing answered
2
pq-preferred, pq, pq-split-dropped, pq-other-group (0.2.0), no-mlkem, pinned-nist, kyber-draft-only, tls12-only, large-hello-dropped, pq-group-rejected, no-tls, no-reply, connect-failed. Each is explained on Causes and fixes .
Field
Type
title
string
E.g. "nginx: link against OpenSSL 3.5+ and list the PQ group"
detail
string
Why, and what to check
snippet
string | null
Config lines to paste, newline-separated
Field
Type
serverHeader
string | null
The HTTP Server header, or a mail server’s greeting line (up to 160 characters)
stack
string | null
The software pqdoctor recognized: nginx, apache, caddy, haproxy, iis, envoy, litespeed, cloudflare, google, akamai, aws, vercel, netlify, github, dotnet, java, node, postfix, exim, dovecot, exchange; from 0.2.0 vsftpd, proftpd, mysql, postgres, windows (RDP)
tlsVersion
number | null
From the classical TLS 1.3 probe
cipher
number | null
Cipher suite from the same probe
extensions
number[]
Extension types in the server’s reply
Field
Type
id
string
tls13Classical, pqOnly, pqPreference, kyberDraft, pqSplit, bigClassicalSplit, nistOnly. See Probes
label
string
E.g. "Offer only an X25519MLKEM768 key share"
reply
Reply
summary
string
E.g. "TLS 1.3: asked to retry with X25519"
ms
number
Including a retry, if there was one
Tagged by kind:
kind
Other fields
Meaning
hello
hrr (bool), version, cipher, group (number | null), extensions (number[])
A ServerHello, or a HelloRetryRequest when hrr is true. version comes from supported_versions when present
alert
level, description
A TLS alert, e.g. description 40 handshake_failure, 70 protocol_version
closed
—
The server closed the connection
reset
—
Connection reset
timeout
—
No reply within 5 s
notTls
—
The reply wasn’t TLS (or over 64 KiB arrived without a ServerHello)
connectFailed
message
Couldn’t open the connection
{ "kind" : "hello" , "hrr" : true , "version" : 772 , "cipher" : 4865 , "group" : 29 , "extensions" : [ 43 , 51 ] }
That’s a HelloRetryRequest for TLS 1.3 (772), TLS_AES_128_GCM_SHA256 (4865 = 0x1301), asking for X25519 (29 = 0x001D).
Field
Type
ip
string
verdict
Verdict
causeCode
Cause
group
string | null
headline
string
unreachable
boolean
True when this address couldn’t be reached from here (e.g. no IPv6 route). That’s not a disagreement
The first reachable address gives the main result. The others get the key-exchange probes only.
Field
Type
chain
CertInfo []
Leaf first: the chain a client offering classical schemes then ML-DSA gets
chainBytes
number
Size of the Certificate message
scheme
string | null
The CertificateVerify scheme, e.g. "ecdsa_secp256r1_sha256"
mldsa
string
serves, no-cert, ignores-offer, rejects-codepoints, unknown. See ML-DSA readiness . From 0.2.0, a pinned-nist server that refuses the (X25519) certificate probe is unknown, not rejects-codepoints
mldsaOnlyScheme
string | null
The scheme used when only ML-DSA was offered, e.g. "mldsa44"
headline
string
E.g. "No ML-DSA certificate"
detail
string
fixes
Fix []
probes
[string, string][]
[label, summary] for the two certificate probes
Field
Type
subject
string | null
Common name
issuer
string | null
Issuer common name
key
string
E.g. ec-p256, rsa-2048, ed25519, ml-dsa-44; from 0.2.0 SLH-DSA by OID, e.g. slh-dsa-sha2-128s
signature
string
What the issuer signed it with, e.g. ecdsa-sha256, ml-dsa-44
bytes
number
DER size
postQuantum
boolean
Key or signature is ML-DSA (and from 0.2.0 SLH-DSA or ML-KEM)
notBefore, notAfter
string
RFC 3339, UTC. Omitted when they don’t parse
pqdoctor scan <file> --json, one per line. --out FILE always appends these records, whatever the output format on screen.
Field
Type
host
string
The name probed. After --www-fallback, this is www.<domain>
listed
string
The original list entry. Omitted when it’s the same as host
port
number
ip
string | null
verdict
string
A Verdict , or error, skipped, resolved
cause
string
A Cause , or error, dns, cdn-<provider>, resolved
group
string | null
prefersPq
boolean
stack
string | null
server
string | null
The Server header
headline
string
checkedAt
number
ms
number
0 for error records
protocol
string
0.2.0: ssh or quic for those checks (in --history). Omitted for TLS over TCP
{ "host" : "nginx.org" , "port" : 443 , "ip" : "3.136.61.53" , "verdict" : "noTls13" , "cause" : "tls12-only" , "group" : null , "prefersPq" : false , "stack" : "nginx" , "server" : "nginx/1.31.3" , "headline" : "Not post-quantum: TLS 1.2 only" , "checkedAt" : 1792072811037 , "ms" : 1479 }
Scan-only values:
verdict / cause
When
error / dns
The name didn’t resolve
error / error
The check itself failed
skipped / cdn-<provider>
The address is in a --skip-ranges network, e.g. cdn-cloudflare
resolved / resolved
--resolve-only: resolved and filtered, not contacted
Scans check one address per host (the first IPv4, otherwise the first address) unless --all-addresses is given, and skip the certificate check unless --auth is given.
pqdoctor ssh <host> --json.
Field
Type
host, port, ip
As in Report
verdict
string
pq, draftOnly, notPq, unreachable
headline, cause
string
causeCode
string
ssh-mlkem, ssh-sntrup-only, ssh-draft-only, ssh-kex-config, ssh-old-openssh, ssh-no-pq, connect-failed
group
string | null
The first post-quantum method offered
ident
object | null
{ line, proto, software, comments } from the server’s identification
software
string
E.g. "OpenSSH 10.0"
openssh
[number, number] | null
OpenSSH major and minor version
kex
string[]
Key exchange methods, in the server’s order, marker names removed
pqKex
string[]
The post-quantum ones
draftKex
string[]
Draft-only ones
cnsa
boolean
Offers mlkem1024nistp384-sha384
strictKex
boolean
Strict key exchange (the Terrapin countermeasure)
hostKey, ciphers, macs
string[]
Server-to-client lists
pqHostKey
string[]
0.2.0: post-quantum host-key algorithms offered (draft names: mldsa, slh-dsa, sphincs, falcon). Omitted when none
warnings
string[]
fixes
Fix []
verify
string
An ssh -vv command
checkedAt, elapsedMs
number
Field
Type
results
object[]
One per group: group (name), codepoint (number), accepted (bool), silent (bool: no answer, so unknown), summary
accepted
string[]
Every post-quantum group the server picked when offered alone
cnsa
boolean
Accepts ML-KEM-1024 (MLKEM1024 or SecP384r1MLKEM1024)
cnsaNote
string
The CNSA 2.0 note
Field
Type
verdict
string
trusted, expired, not-yet-valid, name-mismatch, untrusted-root, self-signed, unverifiable
headline, detail
string
problems
string[]
Every problem, most important first. Empty when trusted
anchor
string
The root’s name, when the chain validated. Omitted otherwise
roots
string
Which root store, e.g. "Mozilla's root store (webpki-roots, 121 roots)"
Field
Type
slhdsa
string
serves, no-cert, ignores-offer, unknown
slhdsaScheme
string
E.g. "slhdsa_sha2_128s". Omitted unless it signed
slhdsaProbe
string
The probe’s summary
composite
string
"not offered: IANA has no composite signature codepoints yet"
merkleTreeCertificates
string
Always "not checked: experimental"
Field
Type
versions
object[]
version ("SSL 3.0" … "TLS 1.3"), supported, cipher (the suite picked), summary
suitesUnder
string
The version the suites were tested under. Omitted when no TLS ≤ 1.2
suites
object[]
Accepted suites, strongest first: code, name, accepted, keyExchange ("X25519", "P-256", "ffdhe-2048", …)
tested
number
How many suites were offered
serverPreference, extendedMasterSecret, secureRenegotiation
boolean
Omitted when not known
findings
object[]
id, severity (high, medium, low), title, detail, knownIssue. See the table
note
string
That findings are by configuration only
Field
Type
variants
object[]
id, label, bytes, ok, reply (Reply ), summary, ms
failed
string[]
The ids that got no ServerHello
headline
string
Field
Type
host, port, ip
verdict
Verdict
headline
string
causeCode
string
quic-pq-preferred, quic-pq, quic-no-mlkem, quic-hello-rejected, quic-no-reply, quic-version, quic-skipped
group
string | null
prefersPq, retry
boolean
versionNegotiation
string[]
Versions a Version Negotiation packet offered. Omitted when none
probes
object[]
label, reply (Reply ), summary, ms
note
string
E.g. why it wasn’t checked. Omitted when empty
elapsedMs
number
Field
Type
verdict
Verdict
headline
string
version
string
The highest negotiated, "DTLS 1.3" or "DTLS 1.2". Omitted when none
group
string
Omitted when unknown
probes
object[]
label, reply, summary, version, group, cookie (bool), ms
note
string
fixed, regressed, changed, added, removed: lists of { target, kind, before, after }, where before / after are { verdict, cause, group, checkedAt } (omitted for added / removed). unchanged: a count.