---
title: "List the caller's discoveries."
method: GET
path: "/api/v1/discoveries"
tags: ["discoveries"]
---

# List the caller's discoveries.

`GET /api/v1/discoveries`

## Request

- Base URL: `https://api.prosp.cc`
- URL: `https://api.prosp.cc/api/v1/discoveries`
- Auth: OAuth 2 (client credentials, token URL `https://prosp-mcp-641053169627.auth.eu-central-1.amazoncognito.com/oauth2/token`, scopes: `mcp/execute`)

Newest-first page of the caller's discoveries. Page with `limit` (default 50, max 200)
and `offset`. A `limit` outside 1–200, a negative or non-integer `limit`/`offset`, or any
other query parameter, is rejected with `400 invalid_query`.

## Query parameters

- `limit` integer
- `offset` integer

## Response `200`

The caller's discoveries, with the canonical pagination envelope (`total`, `count`, `offset`, `has_more`) shared with the credit-ledger surface so a client pages every list surface the same way.

- object
  - `customer_id` string, required: The authenticated customer these discoveries belong to.
  - `jobs` JobStatus[], required
    - `jobId` string, required: Id of this discovery.
    - `icp_id` string: The saved ICP this discovery was started from (`POST /api/v1/discoveries {"icpId": ...}`), for billing attribution — the same value the matching credit-ledger rows carry, so a charge reconciles back to the ICP that caused it. Served on EVERY job surface (the progressive status, the merged discovery view, the finished result, and each entry of the discoveries list) from one reader, so a customer can attribute a run to its ICP without waiting for the run to finish or cross-referencing the ledger. ABSENT (field omitted, never an empty string) when the run was ad-hoc from inline criteria. Ids are scoped to the calling customer — the same namespace `GET /api/v1/icps` returns.
    - `status` 'queued' | 'running' | 'processing' | 'completed' | 'failed' | 'exhausted' | 'cancelled', required: Discovery lifecycle state. `queued` = accepted, not yet started; `running` and `processing` = discovery in progress; `completed` = finished and reached its target; `failed` = ended with an error; `exhausted` = finished but ran out of candidates before hitting the target; `cancelled` = stopped by the caller via `POST /mcp/jobs/{jobId}/cancel`. The last four are terminal.
    - `total_prospects` integer: Canonical count of validated prospects this discovery has delivered so far — the SAME per-discovery delivery metric the finalized `JobResult.total_prospects` carries, read here as a LIVE snapshot that grows monotonically while the job runs and equals the terminal `JobResult.total_prospects` once the job is done. Key on this ONE field name to group/count a discovery's delivery across the status and result surfaces. This is a single-job delivery count — NOT the pagination `total` on `ProspectList`/`ProspectSummaryList`, which counts a LIST/SEARCH result set across your whole inbox.
    - `withheld_count` integer: How many companies this discovery DELIVERED and then RETRACTED, so `total_prospects` above is the SERVABLE set rather than the originally-announced one. A prospect is retracted when a check that can only complete after delivery decides it does not belong to your search — most often a headquarters re-check placing it outside your target region. Retracted companies are removed from `prospects[]`/`found[]` and never reach your CRM. ABSENT (never `0`) when nothing was retracted: absence is the signal.
    - `withheld` object[]: The retracted companies, one entry per row removed from `prospects[]`/`found[]`. Present only alongside a non-zero `withheld_count`.
      - `domain` string: The retracted company's domain, as it was originally delivered.
      - `reason` 'sub_region' | 'kb_seed_dedup' | 'exclusion' | 'business_model' | 'delivery_cap' | 'unspecified': Bounded reason the company was retracted. `sub_region` = its headquarters resolved outside your target region; `exclusion` / `business_model` = it matched a filter you set; `kb_seed_dedup` = you already own it from an earlier search; `delivery_cap` = it arrived after the run had already met your target.
    - `prospectsFound` integer: DEPRECATED camelCase alias of `total_prospects` (identical value); kept during a back-compat transition so existing clients keying on `prospectsFound` do not break. New integrations MUST read `total_prospects` (the canonical, snake_case name shared with `JobResult`). This alias will be removed in a future major version once no live consumer depends on it.
    - `target` integer, nullable: Requested prospect target (maxResults); null for jobs predating target persistence.
    - `found` string[]: Prospect domains discovered so far, in discovery order. With a `cursor`, only the domains added since that cursor; without one, the full list so far. Call `GET /api/v1/prospects/{domain}` (or read the result once terminal) for full detail on any domain.
    - `next_cursor` string, nullable: Opaque cursor to pass as the `cursor` query param on the next poll to fetch only newer domains. Null once the job is terminal (nothing more will accumulate).
    - `has_more` boolean: True while the job is non-terminal and more domains may still accumulate.
    - `candidates_screened` integer: Number of candidate companies this discovery began SCREENING (search/candidate-dispatch phase). Increases monotonically during screening so a client sees live progress even before the first prospect domain lands; 0 before screening has started. It legitimately PLATEAUS once search has exhausted the niche and the run moves into its crawl/validation tail — that is not a stall. For liveness during that tail, read `evaluated_count`. On the finished result it is the frozen final value, so the same number explains a run after it ends.
    - `evaluated_count` integer: Number of screened candidates that have finished crawl + validation (a running verdict count = accepted + rejected). Complements `candidates_screened`: it keeps climbing through the crawl/validation tail — after screening plateaus — so a polling client sees liveness for the WHOLE run. Retains its final total once the discovery is terminal, where together with `candidates_screened` it is the funnel that explains the delivered count.
    - `stage` 'searching' | 'screening' | 'complete': Coarse progress phase, a companion to `candidates_screened`. `searching` = still issuing discovery queries, no candidates evaluated yet; `screening` = evaluating candidate companies; `complete` = the job is terminal (finished, whatever its `terminal_reason`).
    - `last_activity_at` string, date-time: RFC-3339 UTC timestamp (…Z) of the last per-candidate verdict (accept/reject) — the SAME timestamp format as every other date-time the API surfaces (`discovered_at`, `employee_count_as_of`, `facts[].extracted_at`, credit-ledger timestamps). Present only while the job is RUNNING and at least one candidate has been evaluated; omitted on terminal jobs and before the first verdict. With `seconds_since_last_activity` it distinguishes a working-but-slow job from a stalled one when `candidates_screened`/`evaluated_count` look static during the slow evaluation tail.
    - `seconds_since_last_activity` integer: Seconds since `last_activity_at`, computed fresh each poll (resets toward 0 on every new verdict, grows only during genuine silence). Present only while running with at least one verdict — the read-time-fresh liveness signal a polling client uses to tell working-but-slow from stuck.
    - `poll_after_seconds` integer: Recommended number of seconds to wait before polling this discovery's status again. A cadence hint for polite back-off — poll no more often than this rather than hammering the API.
    - `estimated_duration_seconds` integer: Expected wall-clock duration for this discovery, in seconds — the single planning-safe number, always present. Equals the p95 of recent OBSERVED job runtimes (the industry-standard user-facing UPPER BOUND so a slow-but-healthy run is not mistaken for a stuck one, and a client planning timeouts is not misled low): it is identical to `estimated_duration_p95_seconds` once history exists, and falls back to a static coarse default until enough jobs have finished. For the full percentile trio read `estimated_duration_p50_seconds` / `estimated_duration_p90_seconds` / `estimated_duration_p95_seconds`. An estimate, not a guarantee.
    - `estimated_duration_p50_seconds` integer: Median (p50) of recent OBSERVED job runtimes, in seconds — the TYPICAL duration. The lowest member of the explicit p50/p90/p95 trio; `estimated_duration_p95_seconds` is the planning-safe upper bound. Omitted (together with the p90 and p95 fields) until enough jobs have finished to derive it.
    - `estimated_duration_p90_seconds` integer: 90th-percentile (p90) of recent OBSERVED job runtimes, in seconds — a monitoring-baseline percentile and the middle member of the explicit p50/p90/p95 trio. Omitted (together with the p50 and p95 fields) until enough jobs have finished to derive it.
    - `estimated_duration_p95_seconds` integer: 95th-percentile (p95) of recent OBSERVED job runtimes, in seconds — the planning-safe user-facing UPPER BOUND, and the top member of the explicit p50/p90/p95 trio. This is the same value carried by `estimated_duration_seconds` whenever history exists (that field is the always-present single number; this one is the labelled member of the trio). Omitted (together with the p50 and p90 fields) until enough jobs have finished to derive it.
    - `estimated_yield` integer: Expectation-setting hint: the TOTAL number of prospects this discovery is likely to DELIVER — i.e. total deliverable, NOT net-new. It is projected from YOUR own historical delivery ratio (prospects delivered ÷ target requested, accumulated across your completed discoveries) applied to this run's requested target, and it is never projected above the request. "Total deliverable" means the count is GROSS: it includes domains you may already own from a prior discovery (see `already_owned_count` for how many of the delivered set were already yours); it is not a promise of that many BRAND-NEW companies. It derives from the SAME frozen value returned when the discovery was accepted (surfaced here on every poll so a client that did not capture the accept response can still read it), but because the projection is deliberately CONSERVATIVE and a fast-converging run can overtake it, the value shown here is floored at the live `prospectsFound` — i.e. `max(frozen_estimate, prospectsFound)` — so this hint is NEVER less than the count already delivered in the same response. Omitted entirely (never null) when the job carried no create-time projection (insufficient history). An estimate, not a guarantee.
    - `terminal_reason` 'candidates_exhausted' | 'trailing_candidates_forgiven' | 'candidate_cap_reached' | 'candidate_search_failed' | 'time_limit_reached' | 'stalled_no_progress' | 'fan_in_incomplete' | 'target_reached' | 'saturated_known_set' | 'cancelled_by_customer' | 'null', nullable: Why the discovery stopped, once terminal; null while still running, and null on any job terminal before this field's writers shipped. Carried on BOTH the progressive status and the finalized result — they read the same durable value, so the two surfaces cannot disagree about why a run ended. `candidates_exhausted` = the market had no more matching candidates to evaluate, and every candidate it did dispatch was evaluated; `trailing_candidates_forgiven` = the run stopped while a small remainder of its own candidates (up to 5%) was still being evaluated, so the shortfall was forgiven rather than exhausted — reported separately from `candidates_exhausted` because the two call for OPPOSITE responses: a genuinely dry market wants a wider profile, whereas a forgiven remainder means the run was cut short and re-running the SAME profile may deliver; `candidate_cap_reached` = when a run's candidates ran out before your target, it fetched further batches of new candidates, and it stopped because it had screened the maximum number of candidates one run may evaluate — the sources were still returning new candidates, so the shortfall reflects the per-run screening limit, not an exhausted market; `candidate_search_failed` = the run's candidates ran out before your target and the search for further candidates failed repeatedly, so the market was never shown to be exhausted — re-running the SAME profile later may deliver; `time_limit_reached` = the run hit its time budget while still producing; `stalled_no_progress` = the run stopped producing results and was terminated early rather than hitting its budget (a different situation from either of the two above, and reported separately so it is not mistaken for a thin market or a short budget); `fan_in_incomplete` = a search step never finished, so the run was never able to ask for more candidates; `target_reached` = the run delivered everything you asked for; `saturated_known_set` = every delivered domain was already yours from an earlier search (refunded); `cancelled_by_customer` = you cancelled it via `POST /mcp/jobs/{jobId}/cancel`. When credits are refunded, the refund reason matches this.
    - `already_owned_count` integer: How many of this discovery's delivered domains you ALREADY owned from a PRIOR discovery (any earlier search, not just this one's filters) — i.e. cross-search overlap with your existing prospects. 0 means every delivered domain is new to you. Domains already owned under this exact search's filters are de-duplicated out before delivery and are not counted here. Served on both the progressive status and the finalized result from one durable value. See `already_owned_withheld_count` for how many WERE de-duplicated out.
    - `suggestions` Suggestion[]: The SAME data-driven next-step entries `JobResult.suggestions` carries, served here so a client that only ever polls status is not dead-ended on an underperforming run. Read through the one shared reader, so this array can never disagree with the finalized result about which advisories are still true. It has two lives across a discovery's lifetime: while the run is still going it carries AT MOST ONE early `BROADEN_QUERY` entry, emitted only once the observed counters imply the requested target is unreachable, so you can act at minute 5 instead of at the terminal; once the run is terminal it carries that run's frozen advice and disclosures — the underperformance recommendation and any mandatory `FILTER_RELAXED` / `REGION_PREFERENCE_APPLIED` note. ABSENT (never `[]`) when there is nothing to say — absence is the signal, and unlike `JobResult.suggestions` this property has no `[]` default. See `Suggestion` for the per-entry shape and `JobResult.suggestions` for the full terminal vocabulary.
      - `code` 'WIDEN_LOCATION' | 'ALLOW_LARGER_SIZE' | 'ALLOW_SMALLER_SIZE' | 'BROADEN_QUERY' | 'LIMITED_MARKET_DATA' | 'LOWER_TARGET' | 'icp_realignment' | 'TRANSIENT_INTERRUPTION' | 'PIPELINE_LIMITED' | 'ALREADY_DELIVERED' | 'FILTER_RELAXED' | 'REGION_PREFERENCE_APPLIED', required: Bounded code (NOT a rejection reason). The first seven values are advice codes; `FILTER_RELAXED` is a mandatory relaxation disclosure, `TRANSIENT_INTERRUPTION` advises a plain re-run after an infra stall, `PIPELINE_LIMITED` and `ALREADY_DELIVERED` are honest non-actionable outcomes that deliberately carry NO broadening advice, and `REGION_PREFERENCE_APPLIED` is an informational note that a `sub_country_regions` preference shaped this run.
      - `message` string, required: Customer-safe, human-readable recommendation or disclosure. Contains no internal rejection reasons or funnel counts.
      - `suggested_parameters` SuggestedCriteria: A partial `DiscoveryCriteria` delta. Merge it over your original request (the delta wins) and start a new discovery. Every field is optional; only the changed values are present. An empty array (e.g. `sub_country_regions: []`) explicitly clears that field. When the run under-delivered, an LLM improvement pass may also rewrite the free-text `query` itself — when present, `query` REPLACES your original `query` verbatim (it is not merged token-wise); apply it alongside the other fields to re-run the improved ICP.
        - `query` string: LLM-rewritten replacement for the free-text `query`, refined from this run's outcome to surface more matching companies. Present only when the improvement pass produced a better query; use it as-is in place of your original `query` on the re-run.
        - `locations` string[]: Suggested replacement for `locations` (e.g. a wider set of countries).
        - `company_sizes` string[]: Suggested replacement for `company_sizes`.
        - `sub_country_regions` string[]: Suggested replacement for `sub_country_regions`. An empty array clears the field.
        - `industries` string[]: Suggested replacement for `industries`.
        - `keywords` string[]: Suggested replacement for `keywords`.
        - `maxResults` integer: Suggested replacement for `maxResults`.
  - `count` integer, required: Number of discoveries in THIS page (never exceeds the requested `limit`).
  - `total` integer, required: Total discoveries for the caller (across all pages).
  - `offset` integer, required: The starting index this page was served from (echoes the request `offset`).
  - `has_more` boolean, required: True when this page left discoveries unreturned — fetch the next with `offset += count`.

## Other responses

- `401`: Missing/invalid/expired Bearer token.
- `405`: HTTP method not allowed on this endpoint (rejected before any credit debit).
- `429`: Rate limit exceeded (100 requests / 60s per customer).

---

[API](https://skmtc.dev/prosp/apis/prosp-discovery-api.md) · [All operations](https://skmtc.dev/prosp/apis/prosp-discovery-api/llms.txt) · [OpenAPI document](https://skmtc.dev/prosp/apis/prosp-discovery-api/revisions/1f2e0c55da22?raw)
