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. The API needs no key and no signup.
Every check is a full scan, and results are cached for 24 hours by host, port and protocol.
European by default. Scanning servers in Germany, Finland and the Netherlands, 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. Polling the URL, or
following the SSE stream described below, returns the report once
the scan is done:
{
"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). Separate
proto= and port= query parameters work as
an alternative to the combined form.
The direct-TLS protocols are https, ftps, smtps, pop3s, imaps, ldaps, and the STARTTLS protocols are smtp, smtp-submission, imap, pop3, ftp, ldap, xmpp, nntp. Each protocol has a default port for requests without one.
Response
The finished report looks like this, with 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, because a test the grade depends on got no answer. The reason field names the test. |
| 202 | processing | No fresh report exists yet, and the scan is queued or running. Poll the same URL, or follow the stream. |
| 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 | The host parameter is missing, or does not parse as a hostname or IP address with an optional port and a supported protocol. |
| 429 | rate_limited | The client sent 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
The API needs no key, so limits apply per client address. Polling a scan already running and reading a cached report are free, and only starting a scan counts against the limit.
| Scope | Limit | Window |
|---|---|---|
| One target | 6 scans | 1 minute |
| One client address | 60 scans | 10 minutes |
Scan duration
In a sample of 50 hosts, half the scans finished within a minute of
the first request, and the slowest took six minutes. 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, so scripts
read them as well.
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 runs as an MCP tool (ssl_check) over
Streamable HTTP at
api.ssltest.com/mcp. The
connection instructions are on
mcp.ssltest.com.
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, and private, loopback and other non-routable targets are refused. A host whose owner published the opt-out record is never scanned.
Frequently asked
A client polls 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 every poll loop has a defined end.
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.
The pipeline reads the grade from the report and compares the grade with the expected one. Certificate expiry works the same way through days_until_expiry, a field worth watching now certificates last 200 days, falling to 47 days from 2029.
No. A report is served from the cache for 24 hours, which suits a daily expiry check. After a configuration change, the Re-scan button on the report page starts a fresh scan.
A scan is a normal TLS handshake against a public port, the same connection a browser or mail client opens. Host owners preferring 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 with something other than TLS. The error field gives 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.