---
title: "Get a run"
method: GET
path: "/v1/runs/{id}"
tags: ["runs"]
---

# Get a run

`GET /v1/runs/{id}`

## Request

- Base URL: `https://api.hyperresearch.ai`
- URL: `https://api.hyperresearch.ai/v1/runs/{id}`
- Auth: one of:
  - HTTP bearer
  - API key in cookie `hr_session`

One run: live progress, counters, failure reason, and what it cost. `price_cents`, `billed`, `included`, `included_in_trial`, `trial_tier` and `charged_cents` carry the same values under the same names as the `GET /v1/runs` row for this run, so a detail screen never has to fetch the list to say "Included in trial" or print a price (E2E5-11).

## Path parameters

- `id` string, required

## Response `200`

Run

- RunStatus
  - `id` string, required
  - `status` 'clarifying' | 'queued' | 'running' | 'paused' | 'blocked' | 'done' | 'failed' | 'aborted', required
  - `tier` string, required
  - `profile` string, required
  - `project_id` string, nullable, required: The project this run is filed under, or null when it is unfiled.
  - `query` string, nullable, required: The verbatim research question this run was launched with, not truncated, with its line breaks kept (LF line endings; control characters, trailing spaces and runs of more than two blank lines removed). Null for legacy runs whose question cannot be recovered.
  - `step` object, nullable, required
    - `id` string, required
    - `name` string, required
    - `status` string, required
    - `chapter` string, nullable, required
    - `chapter_index` integer, nullable: Position of `chapter` in this run's chapter plan, 1-based. Null on a flat run and while the chapter is not yet registered. With `chapters_total` this is what lets a surface say "Chapter 2 of 4" rather than printing an internal chapter id.
    - `chapters_total` integer, nullable: How many chapters this run planned. Null on a flat run (Light).
    - `detail` string, nullable: The unit inside this step that is running now, in customer words: 'Chapter 2 of 4 · Depth investigation', 'Writing section 3 of 9'. Present only where a step is more than one act — a chaptered run's steps 2–10 and the sectioned synthesis — and null otherwise, because on a flat run the step name is the whole truth. Never an internal unit id (Wave C item 1).
  - `progress` object, required
    - `steps_done` integer, required
    - `steps_total` integer, required
    - `sources_fetched` integer, required: Sources this run KEPT — fetched successfully or served from the vault — which is exactly how many rows its Sources panel has. The single source count: `spend.sources_fetched`, the `counters` stream event and the list summary report the same number, and a reload does not change it. A failed fetch attempt is not a source (E2E4-02).
    - `notes_written` integer, required: Notes actually written to the vault; a source served from the vault writes none.
    - `candidates_seen` integer, nullable, required: Candidates this run CONSIDERED — every URL put to the fetch ladder, including the ones it could not read and the ones the budget refused. Always >= `sources_fetched`. Null while unknown (E2E4-08).
    - `fetch_attempts` integer, nullable, required: Physical fetch attempts across all rungs — requests that actually went out. A vault lookup is not one, so a run that served every source from the vault reports 0 here against a positive `candidates_seen` (E2E7-02). Null while unknown.
    - `sources_listed` integer, nullable, required: DEPRECATED spelling of `candidates_seen`, emitted in step with it so a console built against the old name never disagrees with one built against the new. Read `candidates_seen`.
  - `counters` object: The run’s counters as every surface prints them, identical in shape and value to the `counters` object on the `GET /v1/runs` row for this run. `sources_fetched` is null when the run predates the counter unification and its kept-source count cannot be verified — an attempt count is not a source count, and unknown is the honest answer.
    - `sources_fetched` integer, nullable, required
    - `notes_written` integer, nullable, required
    - `candidates_seen` integer, nullable, required
    - `fetch_attempts` integer, nullable, required
    - `sources_listed` integer, nullable, required
    - `steps_done` integer, nullable, required
    - `steps_total` integer, nullable, required
  - `failure_reason` string, nullable, required: Why a terminal run ended badly: the sanitised, single-line reason from the run log for a failed or aborted run, the gate for a blocked run, null otherwise. An aborted run reads "Stopped by you." for the person who requested the abort and "Stopped by a workspace member." for everyone else; the list row and this field always agree.
  - `aborted_by` string, nullable, required: The user id that asked for the abort, or null for a run nobody stopped, a run stopped by an unattributed API key, or one stopped before this field existed. Published so a client can say "Stopped by you." on its own terms rather than inferring it from the sentence; `failure_reason` is already narrowed for the caller.
  - `failure_detail` string: The raw platform/vendor text behind `failure_reason`. Present ONLY for a signed-in owner or admin of the workspace, and only when there is one — an API key never receives it. Support material, never customer copy.
  - `failure_request_id` string, nullable, required: Identifies that terminal event in the run log; quote it to support.
  - `blocked_on` string, nullable, required: One customer-safe sentence saying what a `blocked` run is waiting for and what to do about it; null on every other status. Branch on `blocked_code`, not on this string.
  - `blocked_code` string, nullable: The machine gate id behind `blocked_on` — `step:<id>:<check>`, `verify:<check>`, `fetch_budget`, `model_budget` or `provider:<name>`. Stable across copy changes.
  - `wait_reason` 'provider_billing' | 'provider_rate_limit' | 'provider_outage' | 'platform_capacity' | 'platform_transient' | 'vault_unavailable' | 'null', nullable: Why a `blocked` run is waiting on US rather than on a person: it is clearing the condition by itself and will carry on with no action from the caller, and nothing has been charged. Present only while a run is waiting; absent or null on every other run, including a block that does need the customer to act. Branch on its PRESENCE, not on the class.
  - `recovering` object, nullable: Present, non-null, while a `running`/`blocked` run is being RECOVERED: our infrastructure interrupted it and we are resuming it from where it stopped, on the same run id, at no extra charge (Deep automatic recovery). Absent or null on every other run. While set, `wait_reason` is null and `blocked_on` is not a gate: the run needs nothing from anyone. Branch on its PRESENCE.
    - `attempt` integer, nullable, required: Which automatic recovery this is (1, 2, 3), or null while a stalled run is being handed over and the attempt is not yet claimed.
  - `started_at` string, nullable, required
  - `updated_at` string, nullable, required
  - `finished_at` string, nullable, required
  - `duration_ms` integer, nullable, required: Wall time from start (or creation) to finish; null while the run is unfinished. The SAME number, from the same computation, that the list row serves as `duration_ms` — a client must not subtract its own timestamps, which is how one run read "15m 57s" here and "16m 0s" on the list (E2E7-11).
  - `spend` object, required
    - `price_cents` integer, required
    - `sources_fetched` integer, required
    - `notes_written` integer, required
    - `candidates_seen` integer, required
    - `fetch_attempts` integer, required
    - `model_calls` integer, required
  - `escalations` object, required
    - `queued` integer, required
    - `resolved` integer, required
  - `chapters_total` integer, nullable: How many chapters this run's plan has — the same field, the same name and the same value as the `GET /v1/runs` row, so a detail page can say "Deep · 4 chapters" without fetching the list. A property of the RUN, not of the step that is moving: `step.chapters_total` can only answer while a chapter-scoped step is running, which a finished Deep run has none of. Null means not chaptered (or settled before the column existed) — never zero.
  - `plan` object, nullable: This run's frozen plan: the steps it was going to run, in plan order, each with the completion state a rerun's prerequisite check applies, and the step it stopped on. Null for a run with no plan yet (queued, clarifying). Read it before offering `POST /v1/runs/{id}/rerun-from`: a tier runs a SUBSET of the pipeline, so a resume point guessed from the step numbering is refused — a console that defaulted to `8` sent it to a run whose plan does not include step 8 (fault 98).
    - `steps` RunPlanStep[], required: The steps this run planned, in plan order, deduplicated — a chaptered plan runs steps 2–10 once per chapter and the manifest keys each one once.
      - `id` string, required: The pipeline's own step id — `1`, `14.5`. Never an ordinal.
      - `name` string, required: The step's human name, the same one `step.name` carries.
      - `status` string, nullable, required: This step's status on the run manifest (`done`, `skipped`, `running`, `failed`, `pending`), or null for a step the run never reached.
      - `completed` boolean, required: The run is PAST this step: it ran, or it was skipped. A resume point whose predecessors are all `completed` cannot be refused for an unfinished prerequisite.
    - `failed_step` string, nullable, required: The step the run stopped on: the one whose status is `failed`, else the one still `running`/`blocked` when the run ended. Null for a healthy run. This is the resume point to default to — the run failed there and everything before it is done, so it is the one step the API is certain to accept.
  - `metadata` object, required
  - `reader_expertise` 'beginner' | 'intermediate' | 'expert' | 'null', nullable: The `levers.reader_expertise` this run was started with (`beginner`, `intermediate` or `expert`), read from its frozen launch packet. Null when the run did not set one, in which case the report is written for an informed non-specialist.
  - `levers` object: The levers this run was started with, from its frozen launch packet, limited to the ones `POST /v1/runs` acts on today (the deprecated `inference_depth` and `citation_style` are left out). Send them back to start the same run again. `{}` when the run set none.
    - `register` 'teach' | 'survey' | 'analyze' | 'advocate'
    - `inference_depth` 'surface' | 'standard' | 'deep': Deprecated. Still accepted and stored on the run, but the research planner sets its own depth for each question and no longer reads this value. Omit it.
    - `domain_notes` string
    - `response_format` string
    - `required_section_headings` string[]
    - `citation_style` 'numbered' | 'wikilink': Deprecated and ignored. Still accepted so existing callers keep working, but no longer stored on the run: every report uses the same citation markers whatever this says. Omit it.
    - `purpose` 'decide' | 'understand' | 'brief_someone': What the report is for. `decide` opens with the answer and what it means for the decision, sets out the trade-offs and closes with a recommendation; `understand` explains the subject and its mechanisms without pushing a recommendation; `brief_someone` makes the opening summary a stand-alone brief you can forward. It changes how the report is organised and summarised, never what it researches, claims or cites, and it does not change the price, the sources or the run time. Omitted, the report is organised as before.
    - `report_language` string: The language the report is written in, as a language tag (`en`, `zh`, `ja`, `ko`, `fr`, ...) or a bilingual pair (`zh+en`). Omitted, the report follows the language the question asks for, or the language it is written in. Headings and the labels the pipeline adds are localised for English, Chinese, Japanese and Korean, and are in English for other languages.
    - `reader_expertise` 'beginner' | 'intermediate' | 'expert': Who the report is written for. `beginner` defines technical terms and acronyms in plain words and explains the background a newcomer needs; `intermediate` uses the field’s standard terms and defines only specialist ones; `expert` uses the field’s terminology without definitions and spends the words on specifics. It changes how the report explains things, never what it researches, claims or cites, and it does not change the price, the sources or the run time. Omitted, the report is written for an informed non-specialist, as before.
  - `price_cents` integer, nullable, required: List price of the run at admission, in cents. Null for an `auto` run not yet classified.
  - `billed` boolean, required: Whether this run has been settled on the usage ledger.
  - `included` boolean, required: Server truth for "Included in trial": an allowance was claimed at admission. Not a guess from a zero price — a fixture run is also $0 and is not included.
  - `included_in_trial` boolean, required: Same value as `included`; both spellings are published so no client has to pick.
  - `trial_tier` string, nullable, required: Which included-run allowance was claimed, or null when the run was billable.
  - `complimentary` boolean: True when an operator grant comped this run (migration 0041): it ran for real, at its real `price_cents`, and its ledger row settles without reaching Stripe. Frozen on the run at admission, so it is unaffected by the grant being revoked or expiring later. Never inferred from a zero amount — a comped run’s ledger row carries the full price.
  - `charged_cents` integer, nullable, required: What the usage ledger actually metered for this run, refunds included. Null until a ledger row exists, which is NOT the same as zero. Exactly the field and the value `GET /v1/runs` publishes for this run.
  - `report_words` integer, nullable, required: Word count of the finished report; null while there is no report. The SAME field, name and value the `GET /v1/runs` row and the report cards publish — the run detail was the one screen that showed the report and would not say how long it was (E2E8-06).
  - `version_of` string, nullable, required: The newest run in this workspace asking the same question, or null when this run IS that run. Label a run “Earlier version” if and only if this is set. Grouped by normalised question text, or by an explicit rerun parent — never by project, which is a body of sources and says nothing about which report replaces which (E2E8-10).
  - `version_group` string: Stable key for the version group, so a client can bucket rows in one pass.
  - `dry_run` boolean, required: True when this run was a DRY RUN: the orchestration was real but the model provider and the fetch/search vendors were mocked, so the report is synthetic and the price is $0. Server truth (the run’s `dry_run` channel), never inferred from a zero price — a trial run and a fixture run are also $0 and are not dry. Always false in production, where dry runs cannot be created.
  - `receipt_summary` object, nullable: What this run’s verification receipt came to, in four counts — the same object and the same values the `GET /v1/runs` row publishes (E2E10-07). Null means no receipt is known for this run; null is NOT a pass.
    - `passed` integer, required: Checks that ran and met their bar. A warning is NOT a pass (E2E8-01).
    - `warning` integer, required: Checks that ran and recorded a finding below our own target. Informational; see `attention` for what a reader should act on.
    - `failed` integer, required: Checks that ran and did not meet their bar.
    - `skipped` integer, required: Checks with nothing to judge ("not applicable"). Counted apart from the three above.
    - `attention` integer: Findings a reader should act on before relying on the report: cited statements that say more than their sources support (above 3 and 3% of those checked), a chapter without its full research pass, a retracted source, or final checks that did not run. Absent on a summary cached before this field existed.
  - `clarification` object, nullable: The clarification interview, when this run had one: what was asked before anything was reserved, what the customer answered, the assumptions the run is proceeding on, and — while the status is `clarifying` — when its time-box runs out (`expires_at`) and what happens then (`on_timeout`). Null on a run whose question was specific enough to skip the interview.
    - `status` 'asking' | 'answered' | 'assumed', required
    - `round` integer, required
    - `score` integer
    - `questions` ClarificationQuestion[], required
      - `axis` string, required: The named dimension this question is about (`scope`, `time_window`, `geography`, …). Answer by axis, not by position.
      - `question` string, required
      - `assumption` string, required: What the run will do if this question is not answered. Shown behind "You decide" and in the skip-all confirmation, and printed in the report’s Assumptions paragraph.
      - `evidence` string, nullable, required: The span of the customer’s own question that prompted this one, when it could be quoted verbatim.
      - `suggested_answers` string[], required
    - `answers` ClarificationAnswer[], required
      - `axis` string, required
      - `question` string
      - `answer` string, required: The literal `you_decide` when the customer left this to us; the assumption then records the decision.
      - `you_decide` boolean
      - `answered_at` string
    - `assumptions` string[], required: Every default this run is proceeding on — verbatim the sentences its report prints.
    - `expires_at` string, nullable, required: When an unanswered interview times out; what happens then is `on_timeout`. Null once the interview is settled.
    - `on_timeout` 'start' | 'close': Present while `status` is `asking`: what happens at `expires_at` if nobody answers. `start` admits the run on its stated assumptions (or closes it without charge if it cannot be admitted then — spend cap, no payment method); `close` ends it without starting and without charge, which is what an interview that found nothing to research does.
    - `resolution` 'answered' | 'skipped' | 'timeout' | 'auto' | 'off' | 'unassessed' | 'null', nullable, required
    - `note` string: A sentence about the interview rather than about the question: that the ambiguity assessment could not be made (`resolution: "unassessed"`), that nothing has started while we ask what to research, or that the time-box closed the run without charge.

## Other responses

- `400`: Invalid request
- `401`: Authentication failed
- `403`: Insufficient scope or plan
- `404`: Not found
- `429`: Rate limited or over quota
- `500`: Internal error

---

[API](https://skmtc.dev/hyperresearch/apis/hyperresearch-cloud-api.md) · [All operations](https://skmtc.dev/hyperresearch/apis/hyperresearch-cloud-api/llms.txt) · [OpenAPI document](https://skmtc.dev/hyperresearch/apis/hyperresearch-cloud-api/revisions/726ef831ebf4?raw)
