SSL/TLS Checker API
This API runs a full SSL/TLS scan for a host and returns a JSON report: certificate chain, protocol and cipher support, known vulnerabilities, ALPN, the HTTP redirect chain, HSTS, CAA, revocation status, and a letter grade. No API key, no signup.
Every check is a full scan (no quick/extended toggle) and results are cached for 24 hours, keyed by host, port, and protocol.
European by default. Scanning servers in Germany and Finland, reports stored in an EU database. How the data is handled.
Quick start
curl "https://api.ssltest.com/v1/check?host=example.com"
A cold check has nothing cached yet, so the first call returns
202 with a poll URL. Poll it (or use the
SSE stream below) until the report is ready:
{
"status": "processing",
"report_id": "a1b2c3d4e5f60718293a",
"poll": "/v1/check?host=example.com"
}Endpoints
| Endpoint | Description |
|---|---|
| GET /v1/check?host= | Cache-or-scan report (JSON) |
| GET /v1/check/stream?host= | Live scan progress (SSE) |
Target format
host accepts a bare host (example.com),
a host with an explicit port (example.com:8443), or
protocol/host[:port] for a non-HTTPS check
(smtp/mail.example.com,
imap/mail.example.com:993). A protocol and port can
also be passed as separate proto=/port=
query params instead of the combined form.
Direct-TLS protocols: https, ftps, smtps, pop3s, imaps, ldaps. STARTTLS protocols: smtp, smtp-submission, imap, pop3, ftp, ldap, xmpp, nntp. Each has a default port used when none is given.
Response
Returned once the report is ready (fields trimmed for brevity):
{
"host": "example.com",
"port": 443,
"protocol": "https",
"scanned_at": 1753272000,
"cached": true,
"primary_ip": "93.184.216.34",
"consistent": true,
"days_until_expiry": 207,
"primary_result": {
"hostname": "example.com",
"port": 443,
"success": true,
"chain_valid": true,
"hostname_match": true,
"connection": {
"protocol": "TLSv1.3",
"cipher": "TLS_AES_128_GCM_SHA256"
},
"grade": {
"grade": "A",
"cap": null,
"cap_reason": null,
"trust_only": false
},
"alpn": {
"checked": true,
"supported": true,
"protocols": [
"h2"
]
},
"http": {
"checked": true,
"status_code": 200,
"downgrades_to_http": false
},
"alt_chains": {
"checked": true,
"chains": [],
"problem": false
},
"certificates": [
{
"subject": {
"common_name": "example.com"
},
"public_key": {
"algorithm": "id-ecPublicKey",
"size": 256
},
"key_strength": 3072,
"validation_type": "DV",
"pin_sha256": "Cq2gW1DVwWX5gG0Rf1Fq3zVe8i4mQe5xO6Yb0Y8pQfE="
}
]
},
"ips": {
"93-184-216-34": {
"ip": "93.184.216.34",
"grade": {
"grade": "A"
}
}
}
}Status codes
| Code | Meaning | Detail |
|---|---|---|
| 200 | The report, or a failure | A finished report, or status: "failed" for a scan that ran and could not produce one, or status: "no_connection" for a host that resolved but completed no handshake. All three are terminal. A report with status: "not_graded" is complete but has no grade: a test the grade depends on got no answer, and reason says which. |
| 202 | processing | No fresh report yet. The scan is queued or running: poll the same URL, or stream it. |
| 403 | opted_out | The host owner published the DNS opt-out record, so the host is never scanned and no report exists. |
| 422 | invalid_target | host is missing, or does not parse as a hostname/IP, an optional port, and a supported protocol. |
| 429 | rate_limited | Too many requests. Retry-After carries the wait in seconds. |
A failure is reported for several minutes before another scan of the same target is attempted, so a poll loop against an unreachable host settles instead of queueing work.
Response headers
| Header | When | Value |
|---|---|---|
X-RateLimit-Limit | Every JSON response | Requests allowed in the window of whichever limit is closest to refusing |
X-RateLimit-Remaining | Every JSON response | Requests left before that limit refuses |
X-RateLimit-Reset | Every JSON response | Unix time at which that window resets |
Retry-After | 403 and 429 responses | Seconds to wait |
Rate limits
No key is needed, so limits are per client address. Polling a scan already running and reading a cached report are free: only starting a scan counts.
| Scope | Limit | Window |
|---|---|---|
| One target | 6 scans | 1 minute |
| One client address | 60 scans | 10 minutes |
Scan duration
Measured over a 50-host sample: half the scans finished within a
minute of the first request, and the slowest took six. A host
resolving to many addresses is the slow case, because every
address gets its own full handshake and its own vulnerability
probes. The 202 response carries queue_position and
an indicative eta_seconds for choosing a polling
interval.
Cross-origin requests
Every endpoint here answers with
Access-Control-Allow-Origin: *, so a browser script
reaches the JSON, the stream and the spec directly. The
Retry-After and X-RateLimit-* headers are
listed in Access-Control-Expose-Headers, which is what
lets a script read them.
Streaming events
GET /v1/check/stream?host=... enqueues a scan the same
way /v1/check does, then streams named Server-Sent
Events until the report is ready:
| Event | Payload |
|---|---|
queued | {ahead} while the scan waits for a worker, resent whenever the number changes |
progress | {progress, status} while the scan runs |
result | the finished report, same shape as /v1/check |
error | {error} for a scan that failed or timed out |
curl -N "https://api.ssltest.com/v1/check/stream?host=example.com"MCP
The same check is also exposed as an MCP tool
(ssl_check) over Streamable HTTP at
api.ssltest.com/mcp. See
mcp.ssltest.com for
connection instructions.
Limitations
One scan runs per host, port and protocol at a time, and a report is cached for 24 hours. Scans reach public addresses only: private, loopback and otherwise non-routable targets are refused. A host whose owner published the opt-out record is never scanned.
Frequently asked
Until the response is a report or a failure. Both arrive with status 200, and both are terminal. A scan reaching no conclusion answers with status "failed" and a retry_after, so a poll loop always has somewhere to stop.
The second request joins the first scan instead of starting another. One scan runs per host, port and protocol at a time, and every caller waiting on it receives the same report.
Read the grade out of the report and compare. Certificate expiry works the same way through days_until_expiry, which is worth watching now certificates last 200 days and will last 47 from 2029.
A report is served from cache for 24 hours, which suits a daily expiry check. A scan after a configuration change needs the Re-scan button on the report page.
A scan is a normal TLS handshake against a public port, the same connection a browser or mail client opens. Host owners who prefer no scans publish a DNS record and every request for the host answers 403 from then on.
Yes. The JSON endpoint, the stream, the spec and the MCP endpoint all send permissive CORS headers, and the rate-limit headers are exposed to scripts.
Usually the port is closed, filtered, or answering something other than TLS. The error field carries the reason in plain words. A host answering slowly is retried, so a timeout reflects the host rather than one unlucky connection.
Terms and policies
Use of the API is subject to the Terms of Service and Privacy Policy. To report abuse, or to stop a host being scanned, see Opt-out.