Review a draft

Check every claim in a draft against a loaded batch. Attest extracts the claims itself, splits compound sentences, and returns one result per claim in draft order.

Last updated 12 September 2026

Review a draft

POST https://api.quorumtech.ch/attest/v1/attest/draft

Review every claim in a draft.

Takes a whole draft, finds the claims in it, and checks each against a loaded batch. Coverage is uniform: the claims a writer omits from a list are the ones they did not perceive as claims, and those are where the errors are.

Name the draft by the draftId from POST /attest/v1/drafts (preferred) or send its text as draft. A short review finishes inside the request and returns 200 with the full result. A long one returns 202 with a jobId to poll at GET /attest/v1/attest/draft/jobs/{jobId}; config.waitMs sets how long the request is held before that happens.

A draft review reserves its ceiling of claims up front, against both the daily budget and the workspace's credits: the request's config.maxClaims or 200, bounded by the draft's length when its text is in the request. The reserve shrinks to the claims found as soon as extraction ends, and settles to the number actually checked once the result is returned or collected. Set maxClaims to reserve less.

A review the day or the balance cannot cover in full but can cover part of is clamped rather than refused: the ceiling becomes what is left and X-Claims-Clamped says so. Only a review nothing is left for is refused. Whenever the ceiling stops a review short, summary.truncated is true, skippedPassages lists what was left, and summary.nextCursor passed back as config.cursor resumes it where it stopped, checking nothing twice.

This is the call for reviewing a document rather than a sentence. Load the sources as a batch, upload the draft, and name both. Attest reads the draft in passages, extracts every assertion (counts, durations, sequence, comparisons, attributions, negations, and characterisations of what a document says), splits compound sentences, checks each claim against the batch, and returns the results in draft order, each carrying the passage it came from so you can find it in your text.

config.waitMs holds the request up to 120,000 milliseconds before a long review becomes a job; poll that job until state is done.

Coverage is reported, never assumed: summary.failedChunks and failedPassages name stretches nothing could read, summary.truncated says whether maxClaims cut the review short, and each claim's coverage says whether the batch could speak to it at all. Out-of-scope attention is not a finding against the draft.

Prefer draftId from an upload over draft text, so the review runs against the bytes you hold and the draft.fileHash in the result proves it.

{
  "draftId": "dft_14f917371a7547dc8bd6",
  "batchId": "bat_8153abc164334cd38273",
  "config": { "maxClaims": 50, "waitMs": 20000 }
}

Replace the ids with the ones your uploads returned. The playground offers the most recent ones it has seen.

Request

Provide draftId (preferred) or draft.

A JSON body with these fields:

| Field | Type | Required | Description | | --- | --- | --- | --- | | draftId | string | no | The id from POST /attest/v1/drafts. | | draft | string | no | The draft's text, only for a caller that cannot upload a file. 1 to 200,000 characters. | | batchId | string | yes | The batch to check the draft's claims against. | | glossary | object of string | no | | | config | object | no | | | config.maxClaims | integer | no | The most claims to extract and check, and what the review reserves against the daily budget and the workspace's credits before it runs. When the draft's text is sent as draft, the reserve is also bounded by its length, and that smaller figure is the ceiling the review actually runs under. Extraction stops as soon as the passages read fill the ceiling, so nothing is extracted only to be discarded. Default 200. 1 to 500. | | config.cursor | string | no | summary.nextCursor from a previous review of this same draft. The review resumes at the passage where that one stopped, drops the claims of it already checked, and checks nothing twice. Loop until nextCursor is null. At most 40 characters. | | config.waitMs | integer | no | How long to hold the request before answering 202 with a job to poll. 0 to 120,000. |

Example request

curl -X POST "https://api.quorumtech.ch/attest/v1/attest/draft" \
  -H "Authorization: Bearer $QUORUM_API_KEY" \
  -H "content-type: application/json" \
  -d '{
  "draftId": "dft_14f917371a7547dc8bd6",
  "batchId": "bat_8153abc164334cd38273",
  "config": {
    "maxClaims": 50,
    "waitMs": 20000
  }
}'

Responses

200

The review finished inside the request.

| Field | Type | Description | | --- | --- | --- | | 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 | | | 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 | |

202

The review is still running. Poll the job.

| Field | Type | Description | | --- | --- | --- | | jobId | string | | | 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. Progress that has not moved for minutes is a long passage or a dead job, and this says which. | | 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 | | | poll | string | Where to collect the result, as a full URL through the gateway. | | events | string | Where to watch the review as it runs, as a full URL through the gateway. |

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 | | --- | --- | --- | | 400 | | The request does not match the schema. error.issues names each failing field. | | 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 | batch_not_found, draft_not_found | The batch (batch_not_found) or the draft (draft_not_found) named in the request is not loaded. Both age out; upload again. | | 410 | inputs_deleted | The draft or the batch was deleted while the review was running (inputs_deleted). Upload again and start a new review. | | 429 | rate_limited, claim_budget_exhausted, too_many_running | Over a limit (rate_limited, claim_budget_exhausted), or the server has no room for another review right now (too_many_running); Retry-After says when to try again. | | 502 | | The model provider behind Attest failed. Retry. | | 503 | extraction_failed, not_configured | No part of the draft could be read (extraction_failed), or the evaluator is not configured (not_configured). |

Cost

Reserves config.maxClaims (default 200, at most 500) at two credits ($0.02) each when the request is accepted, and settles to two credits per claim actually checked when the result is returned or collected. The difference goes back. Set maxClaims to reserve less.

Still need help?

Ask Quincy in the chat bubble below, or write to support@quorumtech.ch and we will help you directly.