Domain health
TLS signature-health grade for a domain
https://ecdsa.com/api/v1/health/{domain}Connects to the domain on port 443, examines the served certificate chain, key and signature algorithms, protocol versions and lifetimes, and returns a letter grade with findings, plus the leaf certificate's expiry and key algorithm as structured fields. Results are cached for 1 hour. Only public DNS hostnames are accepted — IP addresses, single-label names and private/reserved addresses are refused. Extra rate limit: 10 requests/minute per IP. A fresh scan can take several seconds.
- API key
- Optional; a key needs the scope scan
- Quota cost with a key
- 5 units per call
- Rate limit
- 60 requests/min per IP, plus 10/min for scans
- Scan budget
- 6 real scans of one host per 10 min and 300 per minute in all, shared by everyone; cached answers spend nothing
Request
Path parameters
| Field | Type | Required | Description |
|---|---|---|---|
| domain | string | yes | Public DNS hostname, e.g. "github.com". |
Responses
200Health grade, findings and the served leaf certificate.
| Field | Type | Required | Description |
|---|---|---|---|
| domain | string | yes | |
| grade | object | yes | |
| grade.letter | string | yes |
|
| grade.score | integer | yes |
|
| findings | HealthFinding[] | yes | |
| scannedAt | string | yes |
|
| cached | boolean | yes | Best-effort: true when the result came from the 1-hour cache. |
| cacheTtlSeconds | integer | yes |
|
| reportUrl | string | yes | Human-readable report for the same domain.
|
| certificate | HealthCertificate | null | yes | null only when the report carries no certificate. |
Headers: X-Request-Id · X-RateLimit-Limit · X-RateLimit-Remaining · X-RateLimit-Reset · X-Quota-Limit · X-Quota-Remaining · X-Quota-Reset
| Status | Meaning | Codes |
|---|---|---|
| 400 | Bad request. Input limits: a request body of at most 100 KB (a larger one is answered with 413) and arrays and objects nested at most 64 levels deep, JSON documents inside string fields included. | |
| 401 | Unauthorized. | |
| 403 | Forbidden. | |
| 404 | Not found. | |
| 405 | Method not allowed. | |
| 429 | Too many requests. | |
| 500 | Internal server error. | |
| 502 | Bad gateway. | |
| 503 | Service unavailable. | |
| 504 | Gateway timeout. |
Every error answer carries the JSON envelope; the error table says what each code means and what to do.
Example
A public domain
curl -sS 'https://ecdsa.com/api/v1/health/github.com'const response = await fetch("https://ecdsa.com/api/v1/health/github.com");
console.log(response.status, await response.json());import httpx
response = httpx.get("https://ecdsa.com/api/v1/health/github.com", timeout=30.0)
print(response.status_code, response.json()){
"domain": "github.com",
"grade": {
"letter": "A+",
"score": 100
},
"findings": [
{
"id": "pqc-migration-ahead",
"severity": "info",
"title": "Classical algorithm — post-quantum migration ahead",
"detail": "ECDSA P-256 is a sound choice today. It is, however, quantum-vulnerable: NIST IR 8547 (draft) plans to disallow classical signature algorithms — ECDSA, EdDSA and RSA — after 2035; at this key size there is no earlier 2030 milestone — that step covers only 112-bit parameter sets such as RSA-2048. No penalty and no emergency — but certificate rotation is the easy part; knowing everywhere else your systems sign is the real migration work."
},
{
"id": "key-modern",
"severity": "ok",
"title": "Modern signature key (ECDSA P-256)",
"detail": "ECDSA P-256 gives compact keys and signatures at full strength — the current best practice for TLS server certificates."
},
{
"id": "cert-valid",
"severity": "ok",
"title": "Within validity period",
"detail": "Valid until 2026-11-29 — 41 days remaining."
},
{
"id": "cert-short-lifetime",
"severity": "ok",
"title": "Short certificate lifetime (90 days)",
"detail": "Lifetimes of 90 days or less are the signature of automated issuance and rotation (ACME et al.) — exactly the posture the shrinking CA/Browser Forum limits are pushing everyone toward. Well done."
},
{
"id": "legacy-tls-refused",
"severity": "ok",
"title": "Legacy TLS refused",
"detail": "Handshakes capped at TLS 1.1 were rejected — the deprecated protocol versions are switched off."
},
{
"id": "tls13",
"severity": "ok",
"title": "TLS 1.3 negotiated",
"detail": "The handshake settled on TLS 1.3 with TLS_AES_128_GCM_SHA256 — the current protocol generation, with a cleaner handshake and only AEAD ciphers."
},
{
"id": "chain-complete",
"severity": "ok",
"title": "Certificate chain complete (as served)",
"detail": "Every issuer → subject link matches and the chain ends at a self-signed root (chain as served)."
}
],
"scannedAt": "2026-10-20T07:14:02.839Z",
"cached": true,
"cacheTtlSeconds": 3600,
"reportUrl": "https://ecdsa.com/health/github.com",
"certificate": {
"notAfter": "2026-11-29T23:59:59.000Z",
"daysRemaining": 41,
"keyAlgorithm": "ECDSA",
"curve": "P-256"
}
}The API’s answer to this example, produced for this page on October 20, 2026; relative times and day counts in it refer to that day. The scan of github.com is the one recorded from production that day; a live call scans afresh or answers from the one-hour cache.
Error codes
| Code | Status | Meaning |
|---|---|---|
| invalid_json | 400 | The body is not valid JSON or not a JSON object; or the input nests arrays and objects more than 64 levels deep (JSON documents inside string fields count too), or is too complex to inspect for private keys. |
| private_key_rejected | 400 | The request contains private-key material (a PEM PRIVATE KEY block, an OpenSSH, SSH2 or PuTTY private key, or a JWK with "d"). Never send private keys anywhere. |
| invalid_domain | 400 | Not a public DNS hostname (IP addresses, single-label names and paths are refused). |
| scan_dns_error | 400 | The domain does not resolve in public DNS. |
| scan_private_address | 400 | The domain resolves (at least partly) to a private or reserved address. |
| unauthorized | 401 | The Authorization or X-API-Key header is malformed, the API key is not valid (wrong checksum, unknown, revoked or expired), or the operation needs a key. |
| forbidden_scope | 403 | The API key is valid but lacks a scope this operation requires; the message names it. |
| not_found | 404 | No operation exists at this path. |
| method_not_allowed | 405 | The path exists but does not accept this HTTP method; see the Allow header. |
| rate_limited | 429 | Too many requests — see Retry-After. |
| quota_exceeded | 429 | A daily or monthly quota of the API key's account is used up. X-Quota-Reset (and Retry-After) say when it starts over; retrying earlier cannot succeed. |
| internal_error | 500 | Unexpected server error. |
| scan_unreachable | 502 | The domain refused or dropped the connection on port 443. |
| scan_tls_error | 502 | The TLS handshake failed or presented no usable certificate. |
| service_unavailable | 503 | API keys cannot be verified right now (the key store is unreachable); retry shortly. |
| scan_timeout | 504 | The scan did not finish in time. |
Schemas
HealthFinding
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | yes | Stable identifier, e.g. "cert-expired" or "tls13". |
| severity | string | yes |
|
| title | string | yes | |
| detail | string | yes |
HealthCertificate
The served leaf certificate, as structured fields.
| Field | Type | Required | Description |
|---|---|---|---|
| notAfter | string | yes | End of the leaf certificate's validity.
|
| daysRemaining | integer | yes | Whole days from scannedAt until notAfter, rounded up; zero or negative once expired. |
| keyAlgorithm | string | yes | Public-key algorithm of the leaf certificate: ECDSA, RSA, Ed25519, Ed448, an ML-DSA or SLH-DSA parameter set, or the scanner's label for anything else. |
| curve | string | null | yes | Named curve of an ECDSA key ("P-256", "P-384", …); null otherwise. |
Error
The error envelope of every non-2xx response.
| Field | Type | Required | Description |
|---|---|---|---|
| error | object | yes | |
| error.code | string | yes | Machine-readable error code. Each response lists the codes it can carry; new codes may be added within v1, so treat an unknown code by its HTTP status. |
| error.message | string | yes | Human-readable explanation (English). |
| error.requestId | string | no | The X-Request-Id of this request — quote it when reporting a problem. Sent since v1.1. |
| error.detail | object | no | Structured context for the error, such as the offending field; omitted when there is none.
|
| error.docs | string | no | Where the error codes are explained. Sent since v1.1.
|
All operations are listed in the API reference.