---
title: "Poll a sweep job; paginate results when done"
method: GET
path: "/v1/sweeps/{job_id}"
tags: ["sweeps"]
---

# Poll a sweep job; paginate results when done

`GET /v1/sweeps/{job_id}`

## Request

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

Poll a submitted sweep. While `queued`/`running`, returns status
only; when `done`, returns a page of results (same row shape as
`/v1/certificates/search`, plus per-row `match_class` and `match_reason`
annotations: `match_class` ∈ `first_party` / `lookalike_candidate` /
`third_party_integration` / `substring_collision` / `shared_hosting`;
`match_reason` names the rule and token that fired); when `failed`,
returns the structured error (e.g. `sweep_timeout` → narrow
`cn`/`san_contains` or add `issuer`/time windows and resubmit — the
post-match attribution filters will not help). A `done` result with
`truncated: true` also carries `next_cursor` — submit a new sweep with
the same filters plus `resume_token: <next_cursor>` to page past the
result cap (keyset continuation, no gaps or duplicates).

`status` is terminal (`done`/`failed`) if and only if `finished_at` is
set — the two can never disagree, so a response is never `running` with
a finish timestamp already recorded. The `timing` block breaks the
latency down into `queued_ms` (broker wait, usually dominant),
`query_ms` (the engine scan alone), `execution_ms` (worker wall clock)
and `total_ms` (submit → terminal, what a polling caller waits).

## Path parameters

- `job_id` string, uuid, required

## Query parameters

- `page` integer
- `limit` integer
- `cursor` string, nullable: NOT supported here — accepted only so a mistaken cursor walk fails loudly (422) instead of being silently served as page 1. Sweep results are an immutable materialized set, so page/limit paging is already gap-free; and `next_cursor` on this route is a sweep RESUME token for a new submission, not a page cursor.
- `match_class` string, nullable: Filter results to rows with this match_class only (post-materialization view filter — does not change result_count or next_cursor). One of: 'first_party', 'lookalike_candidate', 'third_party_integration', 'substring_collision', 'shared_hosting'. Null-class rows (persisted before the taxonomy existed) are excluded when this filter is set.
- `match_class_filter` string, nullable: Comma-separated view filter applied before page/limit pagination. Allowed values: first_party, lookalike_candidate, third_party_integration, substring_collision, shared_hosting. Defaults to the normalized filter persisted at submit time.

## Cookies

- `ctix_session` string, nullable

## Response `200`

Successful Response

- SweepStatusResponse
  - `job_id` string, uuid, required
  - `status` string, required: queued | running | done | failed. Terminal (done/failed) if and only if finished_at is non-null — the two can never disagree (GH #267).
  - `filters` object, required
  - `created_at` string, date-time, required
  - `started_at` string, date-time, nullable
  - `finished_at` string, date-time, nullable: Set if and only if status is terminal (done/failed). A response never carries a finish timestamp while reporting queued/running.
  - `error` object, nullable
  - `result_count` integer, nullable
  - `truncated` boolean
  - `result_cap` integer, required
  - `timing` SweepTiming: Where a sweep's wall clock actually went (GH #267). Published engine time alone was misleading: a sweep whose bounded SELECT ran in 10 ms could still take ~90 s to become pollable, so no caller could ever observe the advertised figure. These four numbers decompose the latency a caller genuinely experiences.
    - `queued_ms` integer, nullable: Milliseconds the job waited in the broker queue before a worker claimed it (created_at → started_at). Present once the job has been picked up. This is usually the dominant term.
    - `query_ms` integer, nullable: Milliseconds spent in the bounded sweep SELECT itself — the engine cost, excluding queue wait, attribution classification and result persistence. Recorded only for jobs that reached the query (absent on jobs that failed before it).
    - `execution_ms` integer, nullable: Milliseconds of worker wall clock, from claim to terminal write (started_at → finished_at): the query plus classification, result persistence and dispatch overhead. Present once terminal.
    - `total_ms` integer, nullable: End-to-end milliseconds from submit to terminal state (created_at → finished_at) — what a polling caller actually waits. Present once terminal.
  - `next_cursor` string, nullable: Present only when status='done' AND truncated=true: an opaque keyset continuation token. Submit a NEW sweep with the same filters plus `resume_token: <next_cursor>` to fetch the next (up to) result_cap rows with no gaps or duplicates. Repeat until a sweep finishes with truncated=false.
  - `page` integer, nullable
  - `limit` integer, nullable
  - `has_more` boolean, nullable
  - `results` SweepResultRow[], nullable
    - `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` 'first_party' | 'lookalike_candidate' | 'third_party_integration' | 'substring_collision' | 'shared_hosting' | 'null', nullable: Brand-attribution class of this row: 'first_party' — the matched token is a whole label of the name's registrable domain, or the subject organization names the brand; 'lookalike_candidate' — the token sits on a label/token boundary of the registrable domain itself (the typosquat / brand-protection signal); 'third_party_integration' — the token is an exact label (or a label plus a short affix, e.g. datadog2.everestlabs.dev) on a subdomain of an unrelated registrable domain, characteristic of a third-party deployment/integration (GH #281, GH #302); 'substring_collision' — the token only appears embedded inside a larger token (an unrelated brand) or a named non-brand subject organization was present. null only on rows persisted before this field existed.
    - `match_reason` string, nullable: Semicolon-separated key=value string naming the rule and brand token that determined match_class, e.g. 'exact_label_on_third_party;token=datadog' or 'boundary_match_in_rd;token=stripe;rd=stripe-login.com'. Useful for auditing classifier decisions without reverse-engineering the heuristics. The 'first_party' basis is always distinguishable: 'first_party_allowlist;rd=...' (you asserted the domain via first_party_domains), 'subject_org_match;token=...' / 'first_party_org_match;org=...' (the certificate's organization proves it), or 'first_party_label;...' / 'first_party_anchor;basis=...;rd=...' (INFERRED from the hostname or from in-window evidence — only possible when no first_party_domains allowlist was supplied). null on rows persisted before this field existed.
    - `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.

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