discovery

Poll a job's status.

get/mcp/jobs/{jobId}/status

Request

  • Base URL: https://api.prosp.cc
  • URL: https://api.prosp.cc/mcp/jobs/{jobId}/status
  • Auth: OAuth 2 (client credentials, token URL https://prosp-mcp-641053169627.auth.eu-central-1.amazoncognito.com/oauth2/token, scopes: mcp/execute)

Query parameters

cursorstring

Opaque progressive-polling cursor. Pass the previous response's next_cursor to receive only the prospect domains added since that poll. Omit for the full found[] accumulated so far.

Response

Current job status.

jobIdstring required

Id of this discovery.

icp_idstring

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_prospectsinteger

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_countinteger

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.

prospectsFoundinteger

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.

targetinteger nullable

Requested prospect target (maxResults); null for jobs predating target persistence.

foundstring[]

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_cursorstring 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_moreboolean

True while the job is non-terminal and more domains may still accumulate.

candidates_screenedinteger

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_countinteger

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_atstring 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_activityinteger

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_secondsinteger

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_secondsinteger

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_secondsinteger

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_secondsinteger

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_secondsinteger

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_yieldinteger

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_countinteger

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.

Example response

{
  "total_prospects": 3,
  "withheld_count": 1,
  "candidates_screened": 157,
  "evaluated_count": 195,
  "stage": "screening",
  "last_activity_at": "2026-08-09T11:38:12.474Z",
  "seconds_since_last_activity": 45,
  "poll_after_seconds": 20,
  "estimated_duration_seconds": 2900,
  "estimated_duration_p50_seconds": 1140,
  "estimated_duration_p90_seconds": 2400,
  "estimated_duration_p95_seconds": 2900,
  "estimated_yield": 4,
  "already_owned_count": 2
}

Changes

No recorded changes to this operation across all 1 revision of this API.