---
title: "Search Certificates"
method: GET
path: "/v1/certificates/search"
tags: ["certificates"]
---

# Search Certificates

`GET /v1/certificates/search`

## Request

- Base URL: `https://api.ctindex.io`
- URL: `https://api.ctindex.io/v1/certificates/search`
- Auth: one of:
  - API key in header `X-API-Key`
  - HTTP bearer

Search the certificate index with optional filters.

Aliased params: `cn` == `subject_cn`, `issuer` == `issuer_cn`.
When both an alias and its canonical name are supplied, they are ANDed.

## Query parameters

- `domain` string, nullable: Exact domain/subdomain match via domain_certs lookup
- `domain_wildcard` boolean, nullable: When true, match domain as a wildcard pattern: cert must have a SAN entry matching '*.{domain}' or '%{domain}'. Requires domain to also be set.
- `cn` string, nullable: Alias for subject_cn — Subject CN ILIKE match
- `subject_cn` string, nullable: Subject CN ILIKE match (use 'cn' or 'subject_cn', both accepted)
- `issuer` string, nullable: Alias for issuer_cn — Issuer CN ILIKE match
- `issuer_cn` string, nullable: Issuer CN ILIKE match
- `issuer_o` string, nullable: Issuer organisation ILIKE match
- `san` string, nullable: SAN array contains exact element match
- `san_contains` string, nullable: SAN substring match (task #612). Matches certificates with ANY name (SAN or CN-derived) containing this substring. Without a 'domain' scope this is a GLOBAL sweep: it must be at least the trigram minimum length and is governed by 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.
- `valid_after` string, nullable: not_after >= this ISO-8601 datetime — only certs valid after this point
- `valid_before` string, nullable: not_after <= this ISO-8601 datetime — only certs that expire on or before
- `not_after_before` string, nullable: not_after <= this ISO-8601 datetime (expires on or before)
- `not_after_after` string, nullable: not_after >= this ISO-8601 datetime (expires on or after)
- `expired` boolean, nullable: true → only expired certs (not_after < NOW()), false → only valid
- `expiring_within_days` integer, nullable: Only certs whose not_after falls within the next N days (NOW() <= not_after <= NOW() + N days). Drives the 47-day expiry-cliff view.
- `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 the TimestampedEntry.timestamp from the MerkleTreeLeaf (RFC 6962 §3.4): when the log committed the certificate, not when CertIndex ingested it. Null on historical rows and crt.sh-sourced rows; see the logged_at_coverage disclosure block.
- `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 for semantics.
- `key_type` string, nullable: Exact public-key type match (RSA / ECDSA / Ed25519 / Ed448 / DSA)
- `signature_algorithm` string, nullable: Signature algorithm ILIKE match (e.g. sha256WithRSAEncryption)
- `is_weak_crypto` boolean, nullable: true → only weak-crypto certs (undersized key or SHA-1/MD5 sig)
- `min_key_size` integer, nullable: Only certs whose key_size >= this many bits
- `is_wildcard` boolean, nullable
- `is_precert` boolean, nullable
- `prefer_leaf` boolean, nullable: When true, collapse precert/leaf duplicate pairs by dropping precert rows whose leaf counterpart is also indexed (GH #203). Requires a 'domain' or 'san' scope — the live counterpart match is bounded by that predicate.
- `page` integer: DEPRECATED offset pagination (GH #243). Cursor pagination (``cursor=``) is the primary mode; offset mode remains for one release and is answered with a ``Deprecation`` warning header. Ignored when ``cursor`` is supplied.
- `limit` integer
- `cursor` string, nullable: Opaque keyset cursor (GH #243 / GH #263) — the PRIMARY pagination mode for this route. 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 free, 1000 paid); ``page`` is ignored. Global (domain-less) sweeps keep the same total depth cap as offset mode. Omit ``cursor`` entirely for the deprecated offset pagination. Legacy: ``cursor=`` (empty string) also starts enumeration but may be coalesced to null by some transports.

## Cookies

- `ctix_session` string, nullable

## Response `200`

Successful Response

- CertificateSearchResponse
  - `total` integer, required
  - `page` integer, required
  - `limit` integer, 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_is_lower_bound` boolean
  - `next_cursor` string, nullable
  - `crypto_enrichment` object, nullable
  - `logged_at_coverage` object, nullable
  - `archive_search` 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)
