SSL/TLS Report API — /api/v1/security/ssl-info
Live TLS handshake + full DER parse + per-version probe + HSTS/CAA inspection + Qualys-style grade + AI diagnosis, in one call. Returns the leaf certificate, full chain (typed), negotiated protocol/cipher/PFS, protocol matrix, HSTS state, CAA records, computed A+ to F grade with per-factor breakdown, 15 pass/warn/fail checks, and (optionally cached) AI verdict + copy-paste fix snippets.
What this endpoint returns
One aggregated report per call. No follow-ups needed — the chain, TLS version probe, HSTS, CAA, grade, and AI meta pointer all come back in the same payload.
- Method GET
- Path /api/v1/security/ssl-info
- Stability STABLE
- Version v2026-07-18
- Summary Full SSL/TLS posture report for a hostname on port 443 — cert, chain, protocols, HSTS, CAA, grade, AI diagnosis.
- AI header Response includes `X-Analysis-Status: complete | pending`. When pending, poll the AI response via the id in `AiAnalysisMeta`.
- Auth Cookie-based session (browser) or `Bearer ds_*` API key (Phase 2). Sends `Origin`/`Referer` from the frontend domain.
Request parameters
Pass as query-string parameters. Only `domain` is required.
- domain (required) — string Hostname or URL (scheme/path stripped server-side). Underscore-prefixed labels not supported here — this endpoint is for public HTTPS hosts on port 443.
- raw — boolean, default false Include PEM-encoded certificates on the leaf and every chain link. Adds ~4 KB per cert to the payload; only turn on when you need to save/pin the cert bytes.
- refresh — boolean, default false Force a fresh AI diagnosis instead of reusing the 7-day cache. The live TLS handshake always runs live; this flag only affects the AI layer.
Response schema
Wrapped in the standard `{ success: true, data: {...} }` envelope. The `data` object has nine top-level groups.
- data.found — boolean True when the handshake succeeded and a certificate was returned. When false, only `domain`, `error`, and `fetchedAt` are populated.
- data.domain — string Normalised hostname (scheme/path stripped).
- data.fetchedAt — ISO8601 When the report was generated.
- data.connection — object `{remoteAddress, port, servername, handshakeMs, authorized, authorizationError}` — actual TCP/TLS connection metadata.
- data.certificate — object The end-entity leaf. See breakdown below.
- data.chain — array<object> Full chain, position 0 = leaf, tagged with `type: EndEntity | Intermediate | Root`.
- data.chain_issues — array<object> Detected chain problems with `severity: critical | warning | info` and a human-readable `message`.
- data.handshake — object `{protocol, cipher, ephemeral_key, pfs, alpn}` — what was actually negotiated on the wire.
- data.protocols — object Per-version probe results keyed by `TLS 1.3 / TLS 1.2 / TLS 1.1 / TLS 1.0` — each with `enabled`, `cipher`, `error`, `released`, `deprecated`.
- data.hsts — object `{present, header, max_age, include_subdomains, preload, preloadable, preload_listed, status}` — from the HTTPS root response.
- data.caa — object `{present, records[], note}` — from a `dns.resolveCaa` on the registered domain.
- data.grade — object `{letter, score, factors, caps, reasons}` — Qualys-style A+ to F with per-factor breakdown.
- data.tests — array<object> 15 pass/warn/fail checks — `{id, name, category, status, severity, message}`.
- data.compliance — object `{pci_dss, hipaa_ready, modern_browsers, warnings[]}` — derived from protocol matrix + PFS.
- data.AiAnalysis — object Present when the AI diagnosis is cached: `{summary, quick_recommendations, security_assessment[], fixes[]}`. Absent while pending — poll via `AiAnalysisMeta.id`.
- data.AiAnalysisMeta — object Present when AI is pending: `{id, analysisCode, status, avgMs, etaMs, elapsedMs}` — pass `id` to the AI poll endpoint.
- certificate.subject — object `{common_name, organization, organizational_unit, country, state, locality, email}` — decoded X.500 DN.
- certificate.issuer — object Same shape as `subject`. The CA that signed the leaf.
- certificate.validity — object `{from, to, days, days_remaining, is_expired, age_days}` — dates are ISO8601, days are integers.
- certificate.serial_number — string Uppercase hex, no separators.
- certificate.fingerprint — string SHA-1 colon-separated. Legacy — do not use for new pinning.
- certificate.fingerprint256 — string SHA-256 colon-separated. Current standard for pinning + audit.
- certificate.public_key — object `{type, bits, curve, strength, label}` — `type` is RSA/EC/ED25519/ED448; `bits` is derived from curve for EC; `strength` is weak/strong/very-strong; `label` is display-ready (e.g. `ECDSA P-256`).
- certificate.signature_algorithm — object `{oid, name, hash, weak}` — MD5 and SHA-1 based algorithms are flagged `weak: true`.
- certificate.subject_alt_names — array<string> Every DNS name the cert covers. Includes wildcards. `san_count` gives length. `is_wildcard: true` if any entry starts with `*.`.
- certificate.key_usage — array<string> Extended Key Usage OIDs from the certificate extensions.
- certificate.ocsp — object `{urls[], must_staple}` — OCSP responder URLs from AIA + Must-Staple flag from the TLS Feature extension (RFC 7633).
- certificate.ca_issuer_urls — array<string> AIA CA Issuers URLs (where to fetch missing intermediates).
- certificate.cert_transparency — object `{embedded, sct_count}` — SCT count parsed directly from the CT extension DER (OID 1.3.6.1.4.1.11129.2.4.2).
- certificate.validation_level — string `DV | OV | EV | IV` — detected from CA/Browser Forum policy OIDs, falling back to known CA-specific EV OIDs, then to a subject-field heuristic.
- certificate.policy_oids — array<string> Certificate Policies extension OIDs — useful for CA-specific policy lookups.
- certificate.is_wildcard, is_ca, is_self_signed — booleans Structural flags on the cert.
- certificate.pem — string (when raw=true) PEM-encoded certificate body.
How the letter grade is computed
Five factors, each scored 0-100, combined with a weighted average. A single low factor drags the total down (worst-in-class principle).
- certificate (weight 25) Key strength (RSA < 2048 = weak, ECDSA P-256+ = strong), signature algorithm strength (SHA-1 = weak = capped at 40), days remaining (< 14d = capped at 60).
- key_exchange (weight 25) PFS check. ECDHE / DHE ephemeral = 100. No ephemeral key = 60.
- cipher_strength (weight 20) AEAD (GCM, ChaCha20) = 100. CBC-mode non-AEAD = 80. RC4/DES/EXPORT/NULL = 20.
- protocols (weight 20) TLS 1.3 = 100. 1.2 only = 90 max. 1.1 enabled = 70 max. 1.0 enabled = 60 max. No modern TLS = 20.
- features (weight 10) +40 HSTS, +15 preloadable, +15 CAA, +20 CT SCTs, +10 Must-Staple (max 100).
- A+ criteria A grade AND HSTS preloadable AND TLS 1.3 supported AND TLS 1.0/1.1 disabled AND PFS AND CT SCTs embedded.
- Hard caps Expired cert → F outright. Trust chain error (self-signed, hostname mismatch, unknown CA) → capped at 50.
Example response
Abridged shape for `?domain=github.com`. Fields omitted for brevity marked with `…`.
{
"success": true,
"data": {
"found": true,
"domain": "github.com",
"fetchedAt": "2026-07-18T…",
"connection": {
"remoteAddress": "20.207.73.82",
"port": 443,
"servername": "github.com",
"handshakeMs": 129,
"authorized": true,
"authorizationError": null
},
"certificate": {
"subject": { "common_name": "github.com" },
"issuer": { "common_name": "Sectigo Public Server Authentication CA DV E36", "organization": "Sectigo Limited", "country": "GB" },
"validity": { "from": "2026-07-03T…", "to": "2026-10-01T…", "days": 90, "days_remaining": 75, "is_expired": false, "age_days": 15 },
"serial_number": "72010E03F4A067FE4E796266430718F6",
"fingerprint": "A5:47:1F:…",
"fingerprint256": "17:F8:FD:2E:…",
"public_key": { "type": "EC", "bits": 256, "curve": "prime256v1", "strength": "strong", "label": "ECDSA P-256" },
"signature_algorithm": { "oid": "1.2.840.113549.1.1.11", "name": "SHA256-RSA", "hash": "SHA256", "weak": false },
"subject_alt_names": ["github.com", "www.github.com"],
"san_count": 2,
"is_wildcard": false,
"validation_level": "DV",
"cert_transparency": { "embedded": true, "sct_count": 3 },
"ocsp": { "urls": ["http://ocsp.sectigo.com"], "must_staple": false },
"ca_issuer_urls": ["http://…"]
},
"chain": [
{ "position": 0, "type": "EndEntity", "subject": {…}, "issuer": {…}, "…": "…" },
{ "position": 1, "type": "Intermediate", "subject": {…}, "issuer": {…}, "…": "…" },
{ "position": 2, "type": "Intermediate", "subject": {…}, "issuer": {…}, "…": "…" },
{ "position": 3, "type": "Root", "subject": {…}, "issuer": {…}, "…": "…" }
],
"chain_issues": [],
"handshake": {
"protocol": "TLSv1.3",
"cipher": { "name": "TLS_AES_128_GCM_SHA256", "version": "TLSv1.3" },
"ephemeral_key": { "type": "ECDH", "name": "X25519", "size": 253 },
"pfs": true,
"alpn": "h2"
},
"protocols": {
"TLS 1.3": { "enabled": true, "cipher": "TLS_AES_128_GCM_SHA256", "released": 2018, "deprecated": false },
"TLS 1.2": { "enabled": true, "cipher": "…", "released": 2008, "deprecated": false },
"TLS 1.1": { "enabled": false, "error": "ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION", "released": 2006, "deprecated": true },
"TLS 1.0": { "enabled": false, "error": "ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION", "released": 1999, "deprecated": true }
},
"hsts": {
"present": true,
"header": "max-age=31536000; includeSubdomains; preload",
"max_age": 31536000,
"include_subdomains": true,
"preload": true,
"preloadable": true,
"preload_listed": true,
"status": 200
},
"caa": {
"present": true,
"records": [ { "critical": false, "tag": "issue", "value": "digicert.com" } ]
},
"grade": {
"letter": "A+",
"score": 99,
"factors": {
"certificate": { "score": 100, "weight": 25, "reasons": ["Strong key: ECDSA P-256"] },
"key_exchange": { "score": 100, "weight": 25, "reasons": ["PFS via X25519 253 bits"] },
"cipher_strength": { "score": 100, "weight": 20, "reasons": ["Modern AEAD cipher"] },
"protocols": { "score": 100, "weight": 20, "reasons": ["TLS 1.3 supported"] },
"features": { "score": 90, "weight": 10, "reasons": ["HSTS enabled", "HSTS preloadable", "CAA record set", "3 SCTs embedded (CT)"] }
},
"caps": [],
"reasons": ["…"]
},
"tests": [ { "id": "cert_valid", "category": "certificate", "status": "pass", "severity": "critical", "message": "Valid for 75 more days." } ],
"compliance": { "pci_dss": true, "hipaa_ready": true, "modern_browsers": true, "warnings": [] },
"AiAnalysisMeta": { "id": "…", "analysisCode": "SSL_INFO@OVERALL_ANALYSIS", "status": "pending", "avgMs": 7500, "etaMs": 7500 }
}
}One-line invocations
Copy-paste ready. Replace `github.com` with your target.
curl -s 'https://api.domainscan.in/v1/security/ssl-info?domain=github.com' \
-H 'Origin: https://domainscan.in' | jq '.data.grade.letter'curl -s 'https://api.domainscan.in/v1/security/ssl-info?domain=github.com&raw=true&refresh=1' \
-H 'Origin: https://domainscan.in' | jq '.data.certificate.pem'days=$(curl -s 'https://api.domainscan.in/v1/security/ssl-info?domain=github.com' \
-H 'Origin: https://domainscan.in' | jq '.data.certificate.validity.days_remaining')
if [ "$days" -lt 30 ]; then
echo "⚠️ Cert expires in $days days — renew now"
fiError codes
Errors return HTTP status codes paired with stable machine-readable codes. Match on the code field rather than the human-readable message. On soft failures (host unreachable, TLS timeout) the response is still 200 with `data.found: false` and `data.error` populated.
- 200 + data.found=false TLS handshake failed (host down, port 443 closed, TCP timeout, TLS alert). `data.error` holds the underlying reason (e.g. `ENOTFOUND`, `ECONNREFUSED`, `TLS timeout after 6000ms`).
- 400 VALIDATION_FAILED `domain` missing or malformed. Response has `error.details` listing the offending fields.
- 401 AUTH_REQUIRED No valid session cookie or API key on this request. Log in via `/api/v1/auth/session` and retry.
- 403 FORBIDDEN Origin/Referer not allowlisted, or User-Agent flagged as bot/headless. Send from a real browser or supply a valid `Origin` matching your account.
- 500 INTERNAL Unexpected server-side error — log the `x-txn-id` response header and report to support.
Waiting on the AI diagnosis
AI never blocks the main SSL response. First request usually returns `AiAnalysisMeta` with `status: pending`; subsequent requests within the 7-day cache return `AiAnalysis` inline.
- Ready `X-Analysis-Status: complete` + `data.AiAnalysis: {summary, quick_recommendations, security_assessment, fixes}`.
- Pending `X-Analysis-Status: pending` + `data.AiAnalysisMeta: {id, status, avgMs, etaMs, elapsedMs}`. Poll `/api/v1/ai/analysis/:id` at `etaMs` intervals until `status: complete`.
- Force refresh Add `refresh=1` to invalidate the cache and re-run the AI analysis. Cert data still comes back live regardless.
- Cache TTL AI responses are cached for 7 days per (domain, tool). Force refresh with `refresh=1` when you need a fresh diagnosis.
Integration tips
- Alert on `certificate.validity.days_remaining < 30`. Cheapest insurance against a renewal-automation failure. Alert on `< 14` for critical infra.
- Fail deploys on `grade.letter` regression. Store the previous grade, compare after each deploy, fail if it dropped.
- Ship the full chain. If `chain.length === 1`, your server isn't sending intermediates — browsers work via AIA, Java + curl + older Android don't. Fix at the server config level.
- Watch `handshake.pfs === false`. No forward secrecy = past traffic exposed if the private key ever leaks. Almost always fixable via nginx `ssl_ciphers` prioritising ECDHE/DHE suites.
- Use `refresh=1` sparingly. AI generation is the expensive path (~7s). Cached responses are effectively free — force refresh only after a real configuration change.
- For chain-only inspection use `/security/ssl-chain`. Same underlying data, minimal payload — just the chain + issues.
Frequently asked questions
- Is OCSP revocation checked? The OCSP responder URL is extracted from the certificate's AIA extension and returned in `certificate.ocsp.urls[0]`. Live OCSP validation against the CA responder isn't performed here — most clients defer to short-lived certs (Let's Encrypt 90-day) or OCSP Must-Staple instead. If Must-Staple is set on the cert, `certificate.ocsp.must_staple: true`.
- Why is my TLS 1.3 detected as disabled when the server actually supports it? The old shell-based openssl detection had this bug — the new endpoint uses Node's native `tls.connect` with `minVersion`/`maxVersion` pinning, which is authoritative. If the endpoint says `TLS 1.3 disabled`, your server genuinely rejected the pinned-version handshake.
- Why does the grade differ from Qualys SSL Labs? Same 5-factor philosophy, different weights + criteria. Our A+ requires HSTS preloadable + CT SCTs + PFS + TLS 1.3 — Labs weighs each factor slightly differently and also runs cipher-suite compatibility checks against historical browsers, which we don't.
- Is the AI cached? Yes, 7-day cache per (domain, analysis code). Force refresh with `refresh=1`.