---
title: "List all certificates for a domain"
method: GET
path: "/mcp-api/get_domain_certificates/{domain}"
tags: ["mcp"]
---

# List all certificates for a domain

`GET /mcp-api/get_domain_certificates/{domain}`

## Request

- Base URL: `https://api.ctindex.io`
- URL: `https://api.ctindex.io/mcp-api/get_domain_certificates/{domain}`
- Auth: one of:
  - API key in header `X-API-Key`
  - HTTP bearer

Returns paginated certificates associated with the exact domain name. Use `valid_only=true` to filter to currently-valid certificates.

**Pagination signals (task #852):** every response carries a `has_more` boolean — true when at least one more page of rows exists — in BOTH offset (page) mode and cursor mode. `next_cursor` is a cursor-mode-only field: it is minted only for requests that included the `cursor=` parameter (pass `cursor='*'` to start), because cursor pages are keyset-ordered by the monotone internal id while offset pages are ordered by `not_before` — a cursor cannot be minted from an offset page. Offset-mode callers should use `has_more` (or `total`/`limit`) to detect further pages.

**Precert flag (GH #253):** every result row carries an `is_precert` boolean — True for CT precertificates (poison-extension entries), False for final leaves — so callers can classify or count precerts directly from list rows without per-certificate detail roundtrips. Precert and final-leaf twins have different SHA-256 hashes and are returned as separate rows (no dedup across the pair). This raw twin-per-row behavior is intentional and unchanged by GH #262 — this is the complete observation log; use `get_expiring_certs` (which collapses twin pairs by default) or filter on `is_precert` client-side when you need distinct-certificate counts.

**Self-healing for cold domains (task #113 / GH #266):** if CertIndex has not indexed this domain yet, the response contains zero results plus a `backfill_status` sentinel. When `backfill_status == 'in_progress'` wait `retry_after_seconds` seconds and call this tool again. Use `eta_basis` to understand the projection: `'live'` = measured entry-throughput ETA; `'jobs_coarse'` = approximate estimate from job-completion rate (treat as lower-precision); `'unknown'` = no projection yet — `terminal_at_latest` (ISO-8601 UTC) and `seconds_until_terminal` are emitted so you know the latest point at which the run is guaranteed to be closed out — stop polling then at the latest. `in_progress_over_budget` (no `retry_after_seconds`) means the projection — live or coarse — exceeds the interactive budget: do NOT poll in a loop; retry in a later session (see `hint`). When `backfill_status` is `previously_completed`, `failed`, `not_triggered`, `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 — 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.

**Progress reporting:** while an on-demand backfill is in flight the response carries a top-level `progress` block, and the `coverage` block carries a subset of the SAME snapshot under `coverage.progress`. Both are rendered from one snapshot under one emission rule, so shared quantities always agree. Shard-scan counters that carry no signal are OMITTED, not reported as zeros: an ABSENT `shard_scan_progress_pct` / `shard_scan_entries_scanned` means 'not measurable yet', not 'no progress'. `shard_scan_progress_pct` falls back to the jobs ratio when the entry-level aggregate is unavailable, so it can never read 0.0 while `shard_scan_jobs_done` climbs, and `shard_scan_status` can never read `pending` next to a non-zero `shard_scan_jobs_done`.

**Coverage confidence:** responses carry an advisory `coverage` block (`{confidence, basis, hint, as_of}`). `coverage.basis` is EXACTLY one of the values below — **never prefix-match** `domain_backfill_completed`: that prefix spans one trustworthy verdict, two scope-limited ones and an empty-inventory one, so mapping the prefix onto 'the inventory is exhaustive' reads the WORST case as a confirmed-complete answer. Match the exact string. `domain_backfill_completed_full` (HIGH — a backfill completed having walked the FULL certificate-transparency history and found records; the inventory is exhaustive as far as CertIndex can enumerate), `domain_backfill_completed_active_only` (RETAINED — no longer emitted), `domain_backfill_completed_active_since_ingestion` (MEDIUM — the free pass enumerated names since CertIndex's ingestion horizon; `coverage.total_is_lower_bound` is true with `lower_bound_reason: ingestion_horizon`), `domain_backfill_completed_scope_unknown` (MEDIUM — the pass finished but did not establish what it reached: a lane truncated its enumeration, or the row predates scope recording; the inventory is a LOWER BOUND), `domain_backfill_completed` (RETIRED — no longer emitted, but payloads minted by an older fleet still carry it. It could not distinguish the three cases above, so do NOT read it as high-confidence exhaustive), `domain_backfill_completed_empty` (LOW — backfill completed but enumerated zero records; treat as UNKNOWN, NOT as a confirmed empty surface — absence of records is not evidence of absence of certificates), `domain_backfill_never_ran` (LOW — the row reads terminal with zero records but the scan never actually executed, i.e. a dispatch failure; weaker evidence still than completed-empty — re-trigger), `domain_backfill_in_progress` (LOW — backfill running, inventory growing; re-query later; `total_is_lower_bound` is true and `lower_bound_reason` is `backfill_in_progress`), `domain_backfill_stalled` (LOW — the backfill stopped advancing; TERMINAL, stop polling — an operator re-trigger or a paid deep-historical run forces a fresh attempt), `domain_backfill_failed` (LOW — historical backfill failed; the historical inventory may be incomplete, but live CT-log ingestion continues so newly issued certificates still appear), `inherited_from_apex_backfill` (confidence varies — no backfill for this exact name, but a backfill for the ancestor named in `coverage.inherited_from` covers it; backfills are enumerated apex-wide, so a subdomain inherits its apex's verdict rather than falling back to the global signal), `global_active_coverage` (confidence varies — no per-domain or ancestor backfill history; confidence derives from global index coverage), or `no_signal` (UNKNOWN — no signal available). The block also carries `as_of` (UTC timestamp of the verdict).

**Completeness scope (GH #300 / #304 / #536):** every completed-basis block, and the `global_active_coverage` / `no_signal` fallbacks for a name with no `domain_backfills` row, also carries `coverage.enumeration_scope` — `full`, `active_only` or `unknown`, i.e. what the pass that produced this inventory could actually reach — plus `coverage.scope_limited` (bool; true for anything but `full`) and `coverage.total_is_lower_bound` (bool). The NESTED `coverage.total_is_lower_bound` is true when the scan is incomplete WITHIN the advertised product scope (`enumeration_scope=unknown`, an unfinished pass, or no `domain_backfills` row — a never-scanned name). A zero total with `total_is_lower_bound: true` means "we cannot tell"; `0` with `total_is_lower_bound: false` means "none exist within the advertised enumeration scope". A completed `active_only` pass is exact for currently-valid names: missing expired-only names are outside this product (read `enumeration_scope` / `scope_limited`), not a coverage floor. The TOP-LEVEL field of the same name is the OR of that nested flag and this response's own row-scan cap, so the top-level flag can be true while the nested one is false — never the reverse. This envelope also carries top-level `total_is_lower_bound` and `lower_bound_reason` (omitted when the total is exact). An in-progress backfill always floors the total (`lower_bound_reason=backfill_in_progress`); re-query later. Completed `active_only` does not.

**Verdict precedence:** the verdict tracks the live backfill record — it changes as the scan progresses — with fixed precedence: in-progress (any active scan always outranks completed-empty — a running scan and a completed-empty verdict carry opposite instructions, so they must never coexist) > completed (non-empty) > completed-with-follow-up-pass-running (with `sub_verdicts: {active: completed, historical: in_progress}` — never a silent high-to-low regression) > completed-empty (low, terminal only) > failed > global coverage. Field precedence when `retry_after_seconds` is present in the sentinel: the backfill is still running; `coverage.confidence` reflects the state of the data available NOW.

## Path parameters

- `domain` string, required

## Query parameters

- `valid_only` boolean: When true, return only non-expired certificates
- `include_enrichment` boolean: When true, attach RDAP + DNS + ASN/hosting context for the domain under ``enrichment`` (task #501).
- `include_signals` boolean: When true, attach derived attribution signals under ``enrichment.signals``: issuer_diversity_score, wildcard_breadth, third_party_vendors (from the domain's TXT/SPF inventory), and weak_crypto_reasons. Implies enrichment computation. Requires a paid plan (Pro or higher); the free tier receives HTTP 403 ``tier_not_entitled``.
- `page` integer
- `limit` integer
- `cursor` string, nullable: Opaque keyset cursor (task #635 / GH #263). 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 by observation (monotone internal id) and are stable against concurrent inserts. In cursor mode ``limit`` may go up to the caller's tier ceiling (base 200 free, 1000 paid) instead of the offset-mode maximum of 50. When ``cursor`` is supplied, ``page`` is ignored. Omit ``cursor`` entirely for the classic offset pagination (unchanged). Legacy: ``cursor=`` (empty string) also starts enumeration but may be coalesced to null by some transports.
- `wait_for_backfill` boolean: When false, cold domains return a 'not_indexed' sentinel immediately without triggering a background backfill. Default true preserves the existing self-healing behaviour.

## Cookies

- `ctix_session` string, nullable

## Response `200`

Successful Response

- MCPDomainCertResponse
  - `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
  - `domain` string, required
  - `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.
  - `has_more` boolean
  - `next_cursor` string, nullable
  - `enrichment` DomainEnrichment: RDAP + DNS + ASN context for a single domain (task #501).
    - `domain` string, required
    - `registration` EnrichmentRegistration
      - `registrar` string, nullable
      - `created` string, nullable
      - `updated` string, nullable
      - `expires` string, nullable
      - `status` string[]
      - `nameservers` string[]
    - `dns` EnrichmentDNS
      - `a` string[]
      - `aaaa` string[]
      - `mx` EnrichmentMX[]
        - `preference` integer, required
        - `exchange` string, required
      - `ns` string[]
      - `txt` string[]
    - `asn` EnrichmentASN[]
      - `ip` string, required
      - `asn` integer, nullable
      - `prefix` string, nullable
      - `country` string, nullable
      - `network_owner` string, nullable
    - `signals` EnrichmentSignals: Derived attribution signals for a domain. Attached under ``enrichment.signals`` only when the caller passes ``include_signals=true`` AND holds a paid plan (Pro or higher — the free tier gets a 403 ``tier_not_entitled``). Signals are derived from the domain's currently-valid certificates and TXT inventory: issuer diversity, wildcard breadth, third-party vendor fingerprints, and explicit weak-crypto reasons.
      - `issuer_diversity_score` number
      - `distinct_issuers` integer
      - `issuers` string[]
      - `wildcard_breadth` integer
      - `wildcard_names` string[]
      - `third_party_vendors` ThirdPartyVendor[]
        - `vendor` string, required
        - `source` string, required
        - `evidence` string, nullable
      - `weak_crypto_reasons` string[]
      - `certs_considered` integer
      - `certs_capped` boolean
      - `partial` boolean
      - `errors` string, nullable
    - `fetched_at` string, nullable
    - `partial` boolean
    - `errors` object, nullable
    - `cached` boolean
  - `coverage` object, nullable
  - `total_is_lower_bound` boolean: True when `total` is a FLOOR rather than an exact count. An in-progress backfill (`lower_bound_reason=backfill_in_progress`) always sets this — the inventory is still growing. Other causes match the subdomain envelope: `coverage_scope` (scan did not finish or never ran), `query_budget` (this page did not fit). Completed `active_only` is exact within that window and does NOT set this flag.
  - `lower_bound_reason` string, nullable: Why `total` is a lower bound. `backfill_in_progress` = an on-demand backfill is still running; re-query later. `coverage_scope` = the scan did not finish within the advertised product scope, did not establish what it reached, or never ran; retrying returns the identical count. `query_budget` = this page did not fit; repaginate. `query_budget_and_backfill_in_progress` = both truncated page and running backfill. **Absent when `total_is_lower_bound` is false.**
  - `cold_tier_supplement_status` string, nullable: Tier 1 cold exact-name supplement for this page (GH #1462). `ok` = cold answered or confirmed no extra rows within the latency budget; `unavailable` = configured but unreachable (timeout, breaker, or misconfigured URL); `pending` = still in flight when the response was assembled. Omitted when cold was not consulted (filters on) or `TIER1_COLD_QUERY_URL` unset.

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