---
title: "Collect a draft review"
summary: "Poll a review that answered 202. Returns progress while it runs and the full result once it is done."
category: "developer"
tags: ["attest", "reference", "draft", "job", "polling"]
updated: "2026-09-14T04:25:15.632Z"
url: "https://support.quorumtech.ch/articles/attest-draft-job"
markdown: "https://support.quorumtech.ch/articles/attest-draft-job.md"
---

# Collect a draft review

`GET https://api.quorumtech.ch/attest/v1/attest/draft/jobs/{jobId}`

Collect a draft review that is still running.

Returns the review's progress while it runs and the full result once it is done. Collecting
the result is also what settles the bill, so a caller that only wants the counts should read
`.../progress` or `.../events` instead. A finished result stays for five minutes after it is
first collected, so a dropped poll can be repeated, and is then forgotten. Jobs are held in
memory and are not retrievable after the process restarts; a `404` means start the review
again.

While the review runs, `progress.extracted` grows as each passage is read, with
`passagesDone` of `passagesTotal` beside it, and `updatedAt` says when those counts last
changed.

While the review runs the answer carries `state: "running"` and `progress` with the claims extracted and checked so far and the phase (`extracting`, `checking`, `finished`). Once done it carries `state: "done"` and the full review, the same shape as a review that finished inside the request.

Poll every few seconds. Polls are free and do not count as claims. A finished result stays for five minutes after it is first collected, so a dropped poll can be repeated, and is then forgotten. Jobs are held in memory and are not retrievable after the service restarts: a 404 means start the review again. A 410 means the draft or the batch was deleted after the review started.

## Request

Path parameters:

| Parameter | Description |
| --- | --- |
| `jobId` | The `jobId` from a `202` response to `POST /attest/v1/attest/draft`. |

## Example request

```bash
curl -X GET "https://api.quorumtech.ch/attest/v1/attest/draft/jobs/<jobId>" \
  -H "Authorization: Bearer $QUORUM_API_KEY"
```

## Responses

### 200

Either progress (`state` is `running`) or the finished review (`state` is `done`).

Every shape below carries these fields:

| Field | Type | Description |
| --- | --- | --- |
| `jobId` | string |  |
| `draft` | DraftSummary | Which bytes were reviewed. |
| `draft.id` | string | The draft id to review by. `inline` when the text came in the request. |
| `draft.filename` | string | May be null. |
| `draft.fileHash` | string | SHA-256 of the draft as uploaded, or of the text as sent. |
| `draft.chars` | integer |  |
| `draft.createdAt` | string (date-time) |  |
| `draft.source` | `upload` or `inline` |  |

#### Draft job running

| Field | Type | Description |
| --- | --- | --- |
| `state` | `running` |  |
| `progress` | JobProgress |  |
| `progress.extracted` | integer | Claims found so far. It grows as each passage is read, and is final once `phase` leaves `extracting`. |
| `progress.checked` | integer | Claims checked so far |
| `progress.passagesDone` | integer | Passages read so far |
| `progress.passagesTotal` | integer |  |
| `progress.phase` | `extracting` or `checking` or `finished` |  |
| `progress.failedPassages` | FailedPassage[] |  |
| `progress.failedPassages[].chunk` | integer |  |
| `progress.failedPassages[].excerpt` | string |  |
| `progress.failedPassages[].reason` | string |  |
| `updatedAt` | string (date-time) | When the counts last changed. |

#### Draft job done

| Field | Type | Description |
| --- | --- | --- |
| `state` | `done` |  |
| `claims` | DraftClaimCheck[] |  |
| `claims[].index` | integer |  |
| `claims[].claim` | string | The claim as extracted from the draft. |
| `claims[].source` | string | The passage it was extracted from. |
| `claims[].ok` | boolean |  |
| `claims[].duplicateOf` | integer | Set when an earlier claim in this review has the same text. The result is that claim's, checked and charged once. |
| `claims[].kind` | `assertion` or `synthesis` or `judgement` |  |
| `claims[].coverage` | `in_scope` or `out_of_scope` | Whether the batch could speak to this claim at all. Out-of-scope attention is not a finding against the draft. |
| `claims[].missingTerms` | string[] | Terms from the claim that appear nowhere in the batch, when out of scope. |
| `claims[].result` | EvidenceModeResult or BatchModeResult |  |
| `claims[].error` | object |  |
| `claims[].error.code` | string |  |
| `claims[].error.message` | string |  |
| `summary` | object |  |
| `summary.total` | integer |  |
| `summary.supported` | integer |  |
| `summary.needsAttention` | integer |  |
| `summary.failed` | integer | Claims whose check could not run. Not the same as `failedChunks` |
| `summary.extracted` | integer | Claims found in the draft. |
| `summary.chunks` | integer | Passages the draft was read in |
| `summary.extractedChunks` | integer | Passages actually read. Fewer than `chunks` when the claim ceiling stopped extraction early. |
| `summary.failedChunks` | integer | Passages that could not be read. |
| `summary.skippedChunks` | integer | Passages left unchecked because the claim ceiling was reached. |
| `summary.truncated` | boolean | True when the claim ceiling was reached and part of the draft went unchecked. |
| `summary.nextCursor` | string | Where to continue when `truncated`: pass it back as `config.cursor` with the same draft and batch. Null when the whole draft was read. May be null. |
| `summary.nextOffset` | integer | The character offset in the draft where the next review starts. Null when the whole draft was read. May be null. |
| `summary.duplicates` | integer | Claims whose text repeated an earlier claim |
| `summary.cached` | integer | Claims answered from a measurement already made |
| `summary.usage` | Usage | What this response cost at the model provider. Zeros for a replay. |
| `summary.usage.calls` | integer | Completions made. |
| `summary.usage.promptTokens` | integer | Tokens sent. |
| `summary.usage.completionTokens` | integer | Tokens returned. |
| `summary.usage.cachedTokens` | integer | Prompt tokens served from the provider's cache. |
| `summary.usage.latencyMs` | integer | Wall time waiting on the provider. |
| `summary.usage.fellBack` | integer | Calls the preferred tier shed to the fallback. |
| `summary.attentionInScope` | integer | Flagged claims the batch could speak to. |
| `summary.attentionOutOfScope` | integer | Flagged claims the batch could not speak to. |
| `summary.attentionByKind` | object |  |
| `summary.attentionByKind.assertion` | integer |  |
| `summary.attentionByKind.synthesis` | integer |  |
| `summary.attentionByKind.judgement` | integer |  |
| `failedPassages` | FailedPassage[] |  |
| `failedPassages[].chunk` | integer |  |
| `failedPassages[].excerpt` | string |  |
| `failedPassages[].reason` | string |  |
| `skippedPassages` | FailedPassage[] | The passages the claim ceiling left unchecked. Nothing went wrong with them; they are where the next review starts. |
| `skippedPassages[].chunk` | integer |  |
| `skippedPassages[].excerpt` | string |  |
| `skippedPassages[].reason` | string |  |

## Errors

The body is `{ "error": { "code", "message" } }`; the full catalogue with what to do about each code is on the Errors page.

| Status | Codes | When |
| --- | --- | --- |
| 401 |  | The key is missing, not recognised, or revoked. |
| 402 | `payment_required`, `credits_exhausted`, `remaining`, `needed` | The workspace has no active API subscription (`payment_required`), or is prepaid and has no credits left for this request (`credits_exhausted`, with `remaining` and `needed`). Manage billing and add credits at https://developer.quorumtech.ch/billing. |
| 403 |  | The key is not scoped to Attest. |
| 404 |  | No such job. |
| 410 | `inputs_deleted` | The draft or the batch was deleted after the review started (`inputs_deleted`). Kept for a few minutes, then a `404`. Upload again and start a new review. |
| 429 | `rate_limited`, `claims_rate_limited`, `claim_budget_exhausted`, `monthly_cap_reached`, `cap` | Over the key's requests-per-minute limit (`rate_limited`), its claims-per-minute ceiling (`claims_rate_limited`), its claims-per-day budget (`claim_budget_exhausted`), or, for a postpaid workspace, its monthly credit cap (`monthly_cap_reached`, with `cap` and `resetsAt`). `Retry-After` says when to try again; the daily budget resets at midnight UTC and the cap on the first of the month. |
| 502 |  | The review failed at the model provider. |
| 503 |  | The review failed because no part of the draft could be read. |

## Cost

Free to poll. The review's credits settle on the poll that collects the finished result, and are reported in that response's X-Credits-Charged header.
