Search the CT certificate index
Search for certificates by domain, subject CN, issuer, SAN, validity, and more. SCOPING COMES FIRST: pass domain whenever you know the target — a scoped search is cheap and fast. Omitting domain while using a substring filter (cn / issuer / san_contains) makes the query a GLOBAL sweep of the entire index — a paid, separately rate-limited operation that requires the explicit global_sweep=true opt-in (GH #242); without it the request is refused with a structured global_sweep_optin_required error. Returns paginated results with cert metadata. Set expired=false to filter to currently valid certificates only. The response carries a has_more boolean (true when at least one more page is available).
Result-set size (task #1021 / GH #307): the response carries total_matched — how many certificates the filters matched — so you can size a result set WITHOUT paging to exhaustion. Read it together with total_is_lower_bound:
- total_is_lower_bound: false → total_matched is EXACT: that is the whole match set.
- total_is_lower_bound: true → total_matched is a FLOOR: at least that many matched, and the true figure may be far higher. A floor is not a reason to repaginate — paging further reveals more rows, never a truer total. Narrow the filters if you need an exact figure, or use POST /v1/sweeps for a large enumeration. total_matched_basis names which strategy produced the number: exact (a bounded count that finished below its ceiling), capped (the count hit its ceiling), or page_probe (derived from the pagination over-fetch — exact on a terminal page, a floor otherwise). All three keys are absent when no honest number could be produced inside budget; their absence never means zero. An unbounded exact COUNT(*) is deliberately NOT attempted here — over the full certificates table with a leading-wildcard filter it exhausts the server-side statement timeout (task #152), which is why this surface reports a bounded, explicitly-labelled figure instead. (The /v1/certificates/search route keeps its own exact total contract, unchanged.)
A domain-scoped, filter-free search that finds zero rows surfaces the cold-domain backfill_status sentinel (same semantics as the other domain tools): in_progress means wait retry_after_seconds and retry; in_progress_over_budget means the scan continues but do NOT poll in a loop (see hint); when it is previously_completed, failed, not_triggered, or refused_over_budget, or domain_not_found, do NOT retry — the result will not change. domain_not_found means the registrable domain does not exist in DNS (no backfill was started) — check the spelling. refused_over_budget means the domain is valid but too large to backfill inline (read the hint); it is NOT a 'no such domain' answer.
Provenance (task #796): every result row carries reconciled_from_crtsh — true when the row was ingested via the crt.sh reconciliation/backfill path (stored log_id == 'crt.sh') rather than observed directly in a CT log. Such rows reflect crt.sh's observation timing, not CertIndex's own CT-crawl detection latency.
Request
- Base URL: https://api.ctindex.io
- URL: https://api.ctindex.io/mcp-api/search_certificates
- Auth: one of:
- API key in header X-API-Key
- HTTP bearer
Query parameters
Exact domain / subdomain
Subject CN ILIKE substring match
Issuer name ILIKE substring, matched against both issuer CN and issuer organization (e.g. "Let's Encrypt" matches certs whose issuer_cn is "R12")
Exact SAN value (element-in-array match)
SAN substring match (task #612) — matches certs with ANY name containing this substring. Works globally (no 'domain' needed) at 3+ characters; global use rides the global_sweep_per_hr rate class.
Comma-separated attribution classes to keep after classifying each row: first_party, lookalike_candidate, third_party_integration, substring_collision, shared_hosting. Requires a brand token (cn or san_contains). Unknown values are rejected (422), never ignored. The SQL total remains unfiltered, so total_is_lower_bound is true when this is set. Prefer POST /v1/sweeps for a complete classified inventory.
Comma-separated domains the caller asserts are the brand's own. Used only with match_class_filter; each name is reduced to its registrable domain and treated as an authoritative first_party allowlist.
true = expired only; false = valid only
Filter to wildcard certs only
first_seen >= this ISO-8601 datetime. first_seen is the CertIndex database write/ingest timestamp, not the CT-log append timestamp; it is not a substitute for logged_at.
first_seen <= this ISO-8601 datetime. first_seen is the CertIndex database write/ingest timestamp, not the CT-log append timestamp; it is not a substitute for logged_at.
logged_at >= this ISO-8601 datetime — only certs whose CT log timestamp is on or after this instant. logged_at is when the log committed the entry (RFC 6962 §3.4), not when CertIndex ingested it. Null means the CT timestamp is unavailable for that row; first_seen is never substituted. Null on historical rows and crt.sh-sourced rows.
logged_at <= this ISO-8601 datetime — only certs whose CT log timestamp is on or before this instant. See logged_at_after.
not_after >= this ISO-8601 datetime (expiry window start)
not_after <= this ISO-8601 datetime (expiry window end)
Page number (1-indexed). Ignored when 'cursor' is supplied.
Results per page (max 50 in offset mode; up to the caller's tier ceiling in cursor mode)
Opaque keyset cursor (task #889 / GH #263), mirroring /v1/certificates/search. Pass cursor='*' (the coalescing-proof start token) to start a cursor enumeration, then the next_cursor from each response until it is null (cursor-mode pages ALWAYS carry the next_cursor key; an explicit null marks the terminal page). Cursor pages are ordered newest-first (first_seen DESC with a monotone internal-id tie-break) and are stable against concurrent inserts. The cursor is scoped to the exact filter combination it was minted for; replaying it with different filters fails 400 invalid_cursor. In cursor mode limit may go up to the caller's tier ceiling (base 200, enterprise 1000) and page is ignored. Omit cursor entirely for offset pagination (unchanged). Legacy: cursor= (empty string) also starts enumeration but may be coalesced to null by some transports.
Explicit opt-in for a GLOBAL (domain-less) sweep (GH #242). Required when 'domain' is omitted and a substring filter (cn / issuer / san_contains) is present — global sweeps ride the paid global_sweep_per_hr rate class.
When false, cold domains return a 'not_indexed' sentinel immediately without triggering a background backfill. Useful for wide-list probes where you only want what is already indexed. Default true preserves the existing self-healing behaviour.
Cookies
Response
Successful Response
Changes
No recorded changes to this operation across all 1 revision of this API.