# ssl.sh JSON API: certificate check, chain, CT, CAA, HSTS

> REST endpoints under https://ssl.sh/api/v1/ for certificate checks, full chain PEM, CT search, CAA, HSTS, mixed content and certificate decoding.

Source: https://ssl.sh/docs/api/
Last updated: 2026-11-10

The ssl.sh API is a set of read-only JSON endpoints under `https://ssl.sh/api/v1/`. Send a GET request with the host name as a query parameter; there is no sign-up for normal use.

## Authentication and limits

- **Anonymous:** 30 checks per minute per IP address (token bucket). Results are cached for 5 minutes (failed checks for 1 minute) and cached answers do not count against the limit.
- **API key:** send `Authorization: Bearer <key>` (or `?key=<key>`). The limit follows your plan. An invalid key returns `401 invalid_key`.
- **Over the limit:** `429 rate_limited` with a `Retry-After` header in seconds.
- `fresh=1` on check endpoints skips the cache (and counts against the limit).

## Errors

Every error response has the same shape:

```json
{"error": "blocked_address", "message": "10.0.0.1 resolves to 10.0.0.1: private or reserved range 10.0.0.0/8"}
```

| Status | `error` codes |
|---|---|
| 400 | `missing_host`, `invalid_host`, `invalid_port`, `port_not_allowed`, `invalid_starttls`, `private_key_rejected`, `parse_failed` |
| 401 | `invalid_key` |
| 403 | `blocked_address` (private, loopback, link-local, CGNAT or reserved IP) |
| 404 | `not_found` |
| 413 | `too_large` |
| 429 | `rate_limited` |
| 502 | `dns_failed`, `connect_refused`, `connect_failed`, `timeout`, `tls_handshake_failed`, `protocol_mismatch`, `starttls_failed`, `ct_unavailable`, `fetch_failed` |

For `/api/v1/check`, a 502 still carries the full result object (with `error`, `error_detail` and `message`), so you can tell a DNS failure from a refused connection.

## GET /api/v1/check

Checks one host. Parameters:

| Parameter | Default | Description |
|---|---|---|
| `host` | required | Host name, `host:port` or a URL. |
| `port` | 443 | Allowed: 443, 8443, 4443, 9443, 10443, 465, 587, 25, 993, 995, 143, 110, 21, 990, 636, 853, 5222, 5432, 6443, 2083, 2087, 8883. |
| `starttls` | auto | `smtp`, `imap`, `pop3`, `ftp`, `xmpp`, `postgres`. Automatic on 25, 587, 143, 110, 21, 5222, 5432. |
| `full` | off | `full=1` also tests each TLS version, HSTS, CAA and revocation. |
| `fresh` | off | `fresh=1` bypasses the cache. |

```bash
curl -s 'https://ssl.sh/api/v1/check?host=example.com&full=1'
```

Response (shortened):

```json
{
  "host": "example.com",
  "port": 443,
  "starttls": null,
  "ip": "203.0.113.10",
  "checked_at": "2026-11-10T08:00:00Z",
  "ok": true,
  "error": null,
  "tls_version": "TLSv1.3",
  "cipher": "TLS_AES_256_GCM_SHA384",
  "key_exchange_group": null,
  "alpn": "h2",
  "protocols": {"TLSv1.0": false, "TLSv1.1": false, "TLSv1.2": true, "TLSv1.3": true},
  "chain": [
    {
      "subject": "CN=example.com",
      "subject_cn": "example.com",
      "sans": ["example.com", "www.example.com"],
      "issuer_cn": "R12",
      "issuer_org": "Let's Encrypt",
      "not_before": "2026-10-15T07:12:00Z",
      "not_after": "2027-01-13T07:11:59Z",
      "days_left": 64,
      "validity_days": 90,
      "key_type": "RSA 2048",
      "sig_alg": "sha256WithRSAEncryption",
      "validation_level": "DV",
      "sct_count": 2,
      "eku": ["serverAuth"],
      "pem": "-----BEGIN CERTIFICATE-----\n…"
    }
  ],
  "chain_complete": true,
  "trusted": true,
  "hostname_match": true,
  "hsts": "max-age=31536000; includeSubDomains",
  "caa": ["0 issue \"letsencrypt.org\""],
  "revocation": "good",
  "findings": [
    {"level": "ok", "code": "valid", "message": "Certificate is valid and trusted, expires in 64 days.", "message_zh": "证书有效且受信任，64 天后过期。", "link": null}
  ],
  "grade": "A+",
  "days_left": 64,
  "root_subject": "CN=ISRG Root X1,O=Internet Security Research Group,C=US",
  "fullchain_pem": "-----BEGIN CERTIFICATE-----\n…",
  "report_url": "https://ssl.sh/example.com"
}
```

Field notes:

- `chain` is in the order the server sent it; `aia_fetched` lists intermediates the server did not send and ssl.sh downloaded via AIA. When it is non-empty, `chain_complete` is `false`.
- `findings[].code` is machine-readable: `expired`, `not_yet_valid`, `expires_soon`, `hostname_mismatch`, `self_signed`, `untrusted_root`, `incomplete_chain`, `revoked`, `weak_key`, `sha1`, `tls10_enabled`, `tls11_enabled`, `weak_cipher`, `legacy_only`, `no_tls13`, `no_hsts`, `hsts_short`, `caa_missing`, `client_auth_eku`. `link` points to the ssl.sh page that explains the fix.
- `grade` is `A+`, `A`, `B`, `C`, `F` or `T` (untrusted); it is `null` when no TLS handshake was possible.
- `protocols`, `hsts`, `caa` and `revocation` are only filled with `full=1`. `key_exchange_group` is `null` when it cannot be determined.

## GET /api/v1/chain

Returns the completed chain: `{host, port, trusted, chain_complete, root, pem_fullchain, certs: [...]}`. `pem_fullchain` is the leaf followed by intermediates (no root), in path order. Each item in `certs` has the same fields as `chain` above plus `source`: `server` or `aia`.

```bash
curl -s 'https://ssl.sh/api/v1/chain?host=example.com' | jq -r .pem_fullchain > fullchain.pem
```

## GET /api/v1/ct

Searches Certificate Transparency logs through crt.sh, falling back to Cert Spotter. Results are cached for 6 hours.

| Parameter | Default | Description |
|---|---|---|
| `domain` | required | Domain name. |
| `include_subdomains` | 1 | `0` to search the exact name only. |
| `include_expired` | 0 | `1` to include expired certificates. |

```json
{
  "domain": "example.com",
  "include_subdomains": true,
  "include_expired": false,
  "count": 1,
  "certificates": [
    {"id": 1234567890, "issuer": "C=US, O=Let's Encrypt, CN=R12", "not_before": "2026-10-15T07:12:00Z",
     "not_after": "2027-01-13T07:11:59Z", "dns_names": ["example.com", "www.example.com"],
     "serial": "04a1…", "source": "crt.sh"}
  ]
}
```

## GET /api/v1/caa

`?domain=example.com` returns the CAA records in effect, found by climbing from the name to its parents as RFC 8659 requires: `{domain, found_at, records, issue, issuewild, iodef, any_ca_allowed}`.

## GET /api/v1/hsts

`?host=example.com` fetches `https://example.com/` and returns `{host, port, http_status, header, max_age, max_age_days, include_subdomains, preload_directive, preload_list}`. `preload_list` is the domain's status from the hstspreload.org API (`preloaded`, `pending`, `unknown` …) or `null` if that service did not answer.

## GET /api/v1/mixed-content

`?url=https://example.com/page` (or `?host=`) downloads the page (up to 2 MB, redirects followed) and lists sub-resources loaded over `http://`: `{url, final_url, status, page_is_https, count, active, passive, insecure_forms, items: [{tag, attr, url, type}]}`. `active` items (scripts, stylesheets, iframes) are blocked by browsers; `passive` items (images, media) trigger warnings. See [mixed content warnings](https://ssl.sh/errors/mixed-content-warning/).

## POST /api/v1/decode

Send a certificate or CSR in the body as PEM or base64 DER (raw text, JSON `{"pem": "…"}` or form field `pem`). Up to 256 KB. Any input containing `PRIVATE KEY` is rejected with `400 private_key_rejected` and is not processed.

```bash
curl -s https://ssl.sh/api/v1/decode --data-binary @cert.pem
```

Certificates return `{"type": "certificate", "count": n, "certificates": [...]}` with the same fields as `chain`; CSRs return `{"type": "csr", "csr": {subject, subject_cn, sans, key_type, sig_alg, signature_valid}}`.
