Skip to main content
This is the public contract. The models, serializers, API docs, and app all build against it. The response carries what you need to make a decision: the verdict, the reasoning behind it, and enough of the document’s identity to match it against your own records. Rendered revisions, highlighted diffs, and the raw structural analysis stay in the dashboard, where they can be displayed properly.

document_check

Flags and reasons

A flag is a category of finding. The set of flag codes is closed and stable. Branch on flag.code in your integration and it will keep meaning the same thing. A reason is one concrete observation that produced the flag. A flag always has at least one, and often several: two independent routes to the same conclusion is stronger evidence than one, so we return both rather than collapsing them.
reason.code is an open set. New reasons appear as detectors improve, without a version bump and without a new flag code. Display reason.summary; branch on flag.code.
flag.description is a one-line headline. flag.pages is the union of the pages named by its reasons.

status

While processing, verdict fields (decision, risk_score, summary, flags, …) are null/empty but the object shape is the same, so pollers never branch on which keys exist. That holds for nested objects too: every extracted field is present and null before extraction has run, rather than the block being empty.

decision

Assigned by the analysis engine (with score-band realignment after rule deltas):

risk_score

An integer from 0 to 100. Higher means riskier, in the same direction as risk_level. It is null until the check completes. decision is cut from that number using the workspace’s outcome thresholds (snapshotted onto each check). Defaults preserve the historical bands: Because the bands are contiguous, decision is always recoverable from risk_score + the snapshotted thresholds. The two can never tell you different things.

Setting your own boundaries

Tune the three cut lines via GET/PATCH /api/v1/thresholds/ or the Rules page: pending_below / suspicious_below / fraudulent_below (defaults 40 / 60 / 80). Those values are snapshotted onto each check so later edits do not rewrite history.
Within a band, the score reflects how much evidence there was. Findings combine by saturation rather than addition, so ten weak signals never outrank one decisive one, and a finding supported by several independent observations sits higher than the same finding seen once. A single HIGH finding on a fraudulent document scores near the top of the fraudulent band. Triggered workspace rules also move the dial by their configured score_delta (positive = riskier). A binding outcome_guidance (REJECTED / PENDING / ACCEPTED) floors or caps the score into the matching decision band. After that, decision is re-aligned to the score band so the two stay consistent. Triggered rules are shown on the document viewer.
A clear document is still scored. Signals that were not strong enough to publish as flags (a rebuilt PDF, a named editing tool) still move it within 0–39, which is what gives you something to tune against across the documents that pass. This is why a clear result can score 18 while showing an empty flags array.
The score is comparable within an engine.version, not across versions. Retuning is what a version bump is for, so store engine.version alongside any threshold you set, and re-check your line when it changes.If a reviewer overrides a verdict in the dashboard, decision reflects the override while risk_score continues to report what the engine concluded. That is the only case where the two diverge, and it is deliberate.

What the API does not return

The original file, each stored PDF revision, the highlighted diff between revisions, and the raw structural dump are not part of the response. They are material for a human to look at, and that review happens in the dashboard, where every finding is rendered against the page it came from. You always keep the file you uploaded, and document.sha256 lets you tie a result back to it.