---
title: "Search the CT certificate index"
method: GET
path: "/mcp-api/search_certificates"
tags: ["mcp"]
---

# Search the CT certificate index

`GET /mcp-api/search_certificates`

## 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

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.

## Query parameters

- `domain` string, nullable: Exact domain / subdomain
- `cn` string, nullable: Subject CN ILIKE substring match
- `issuer` string, nullable: Issuer name ILIKE substring, matched against both issuer CN and issuer organization (e.g. "Let's Encrypt" matches certs whose issuer_cn is "R12")
- `san` string, nullable: Exact SAN value (element-in-array match)
- `san_contains` string, nullable: 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.
- `match_class_filter` string, nullable: 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.
- `first_party_domains` string, nullable: 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.
- `expired` boolean, nullable: true = expired only; false = valid only
- `is_wildcard` boolean, nullable: Filter to wildcard certs only
- `first_seen_after` string, nullable: 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_before` string, nullable: 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_after` string, nullable: 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_before` string, nullable: logged_at <= this ISO-8601 datetime — only certs whose CT log timestamp is on or before this instant. See logged_at_after.
- `not_after_after` string, nullable: not_after >= this ISO-8601 datetime (expiry window start)
- `not_after_before` string, nullable: not_after <= this ISO-8601 datetime (expiry window end)
- `page` integer: Page number (1-indexed). Ignored when 'cursor' is supplied.
- `limit` integer: Results per page (max 50 in offset mode; up to the caller's tier ceiling in cursor mode)
- `cursor` string, nullable: 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.
- `global_sweep` boolean, nullable: 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.
- `wait_for_backfill` boolean: 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

- `ctix_session` string, nullable

## Response `200`

Successful Response

- MCPCertificateSearchSentinelResponse: ``search_certificates`` REST-mirror response (GH #242). Extends the base MCP search shape with the cold-domain backfill sentinel fields so a domain-scoped, filter-free search that hits an unindexed domain surfaces the same self-heal semantics as the other domain-shaped tools instead of a bare empty result set. The route sets ``response_model_exclude_none=True`` so warm-path responses render without any sentinel keys. Task #776 (Run 6 §9): empty domain-scoped pages (filtered or not) also carry the advisory ``coverage`` block ({confidence, basis, hint}) so agents can distinguish "index confirms 0 matches" from "domain not ingested yet".
  - `backfill_status` string, nullable
  - `retry_after_seconds` integer, nullable
  - `hint` string, nullable
  - `progress` object, nullable
  - `typical_eta_seconds` integer, nullable
  - `eta_seconds` integer, nullable
  - `eta_basis` string, nullable
  - `index_freshness` object, nullable
  - `page` integer, required
  - `limit` integer, required
  - `has_more` boolean, required
  - `results` CertificateSummary[], required
    - `sha256` string, required
    - `subject_cn` string, nullable
    - `subject_o` string, nullable
    - `sans` string[]
    - `issuer_cn` string, nullable
    - `issuer_o` string, nullable
    - `not_before` string, date-time, nullable
    - `not_after` string, date-time, nullable
    - `is_wildcard` boolean
    - `is_precert` boolean: True when this row is a CT precertificate (poison-extension log entry), False for a final leaf certificate. Precert and final-leaf twins have different SHA-256 hashes and are returned as separate rows; present on every list and detail row. Exception (GH #262): get_expiring_certs collapses twin pairs to the final leaf by default.
    - `precert_sha256` string, nullable: SHA-256 fingerprint of the CT precertificate twin that was collapsed into this final-leaf row (get_expiring_certs default twin collapsing, GH #262). Omitted when no twin was collapsed.
    - `key_type` string, nullable
    - `key_size` integer, nullable
    - `signature_algorithm` string, nullable
    - `is_weak_crypto` boolean, nullable
    - `key_fields_source` string, nullable
    - `first_seen` string, date-time, nullable: When CertIndex wrote this certificate to its database (batch-write timestamp). Multiple unrelated certificates often share an identical first_seen within the same ingestion batch. It is not the CT-log append time and is never substituted for a missing logged_at. Compare with logged_at to measure detection latency.
    - `logged_at` string, date-time, nullable: Timestamp from the CT log entry itself (MerkleTreeLeaf TimestampedEntry.timestamp, RFC 6962 §3.4) — when the log committed this certificate, NOT when CertIndex ingested it. Null for rows indexed before this field shipped, for crt.sh-sourced rows (reconciled_from_crtsh=true), and for any path that could not supply it. A null value means the CT-log timestamp is unavailable for that row; it does not mean the certificate was observed at first_seen, and first_seen is never synthesized into logged_at. Compare with first_seen (CertIndex write time) to measure detection latency. See logged_at_coverage disclosure block.
    - `reconciled_from_crtsh` boolean, nullable
    - `last_seen` string, date-time, nullable
    - `match_class` string, nullable: Attribution class when match_class_filter was supplied on search: first_party, lookalike_candidate, third_party_integration, substring_collision, or shared_hosting. Null on unfiltered search.
    - `match_reason` string, nullable: Semicolon-separated rule=value audit string for match_class. Null on unfiltered search.
    - `from_cold_tier` boolean, nullable: True when this certificate was served exclusively from the Tier 1 cold ClickHouse index (historical data not present in the hot Neon index). Absent/null for certificates served from the primary index. Cold-sourced rows carry key_fields_source='unavailable' and no raw_pem.
  - `total_matched` integer, nullable
  - `total_is_lower_bound` boolean, nullable
  - `total_matched_basis` string, nullable
  - `limit_clamped` boolean, nullable
  - `next_cursor` string, nullable
  - `crypto_enrichment` object, nullable
  - `logged_at_coverage` object, nullable
  - `coverage` object, nullable

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/ctindex/apis/certindex-api.md) · [All operations](https://skmtc.dev/ctindex/apis/certindex-api/llms.txt) · [OpenAPI document](https://skmtc.dev/ctindex/apis/certindex-api/revisions/d5bc0ec85bc9?raw)
