Output schema
What you get back for every document, field by field. This is the same JSON the dashboard renders and the same JSON in the .zip you download, so what you see on screen is what your systems receive.
Shape of a result
One result covers one job. A job holds the files you sent, each file holds its pages, and each page holds the fields and tables read off it.
{
"job_id": "16d85a86-94db-420d-83b3-a6801952db9b",
"created_at": "2026-08-16T00:42:34+00:00",
"files": [
{
"filename": "claim.pdf",
"error": null, // set only if the file could not be read
"pages": [ { /* one entry per page */ } ]
}
]
}
needs_review, the one to branch on
Of everything in a result, needs_review is the field your code should act on. It is how a document tells you it wants a person, and it is what turns extraction into a workload you can staff: the flagged share goes to your reviewers, the rest goes straight through.
It appears at two levels and they are independent.
| Where | Set when | What to do |
|---|---|---|
page.needs_review | The page arrived below 150 DPI, or something about the read applies to the whole page rather than one value. | Look at the page. Values can be clean and still sit on the wrong line when the print is too small to separate. |
field.needs_review | That value is uncertain on its own terms. On a checkbox group it means the marks were ambiguous. | Route that field, not the document. |
needs_review says whether the reading is worth checking. A value printed crisply but picked up from the row above scores high on the first and is exactly what the second exists to catch, so branching on confidence alone will let that one through.A page flag does not set the field flags, and a field flag does not raise the page. Check both: page.needs_review or any(f.needs_review for f in page.fields) is the whole-document question, and the per field flag is the routing decision.
Page
Each page reports what it matched, how much of the form carried values, then the extractions themselves.
| Field | Type | What it means |
|---|---|---|
| page_number | int | 1-based page within the file. |
| status | string | extracted, unreadable when the page carried no readable text, or not_recognized when it is not a document type you have set up. |
| text_fields_total | int | Fields defined for this document type. |
| text_fields_matched | int | Of those, how many carried a value. The rest were blank on this form. |
| needs_review | bool | The routing signal. True when this page should be seen by a person before anything downstream acts on it. See above. |
| page_dpi | int | The resolution the page actually arrived at, measured rather than taken from the file header. |
| validation_applied | bool | Whether your validation rules ran on this page. |
| fields | array | The fields. See below. |
| tables | array | The tables. See below. |
| page_asset | string | Filename of the page image in the .zip. |
text_fields_matched below text_fields_total tells you the form was partly empty, not that the read went wrong.Fields
Every field comes back with the label it was read against, the value, where both sit on the page, and a confidence.
{
"field_slug": "1a_insured_s_i_d_number", // stable key, safe to map on
"key_text": "1a. INSURED'S I.D. NUMBER", // the printed label
"value_text": "123-45-6789",
"value_type": "text", // text, date, address, phone, amount, checkbox
"value_components": {}, // parsed parts, e.g. a date split out
"key_bbox": { /* where the label sits */ },
"value_bbox": { /* where the value sits */ },
"confidence": 1.0,
"needs_review": false // worth a human check before it ships
}
field_slug is the key to build against. parent_field_slug links a field to its group when one applies, and regex_validator plus value_format carry the rule the value was checked against.
value_text rather than being dropped or guessed at, so your own code can tell "nothing was written here" apart from "we did not look".Checkboxes
Marked boxes come back as fields with value_type: "checkbox". The value is the option that was marked, not a raw true or false, so a group of four boxes resolves to the one that applies.
{
"field_slug": "6_patient_relationship_to_insured",
"value_type": "checkbox",
"value_text": "Self", // or "NO", or "(none marked)"
"needs_review": false, // true when the group was ambiguous
"confidence": 0.62
}
When no box in a group is marked you get (none marked) rather than a silent omission, so an unanswered question stays visible.
Tables
Repeating tables, service lines and the like, come back as columns plus rows of cells. Cells are keyed by column slug, so a row is a record you can read straight into your own model.
{
"columns": [
{ "slug": "24d_cpt_hcpcs", "header_text": "24 D. PROCEDURES", "value_type": "text" }
],
"rows": [
{
"row_index": 0,
"cells": {
"24d_cpt_hcpcs": {
"text": "76705",
"value_type": "text",
"bbox": { /* where this cell sits on the page */ }
}
}
}
],
"row_count": 6
}
Blank rows are dropped rather than returned as empty records, so row_count is the number of lines actually filled in. Every cell carries its own box, which is what lets the dashboard highlight a cell on the document when you hover it.
Coordinates
Every bbox is normalised to the page, so it holds up whatever resolution you render at.
{ "x": 0.6081, "y": 0.1236, "width": 0.1371, "height": 0.0082 }
// fractions of page width and height, origin at the top left
// pixels: x * page_width, y * page_height
These are what make a result checkable. Every value points back at the spot it came from, so a reviewer can confirm it against the document instead of taking it on trust.
Confidence
confidence runs 0 to 1 per field and comes from the model that read the value. It is not a setting: what you choose is the threshold you apply to it. Accept above your line, send the rest to a person, and the audit trail records which values went each way.
Use it alongside needs_review rather than instead of it. Confidence is a number you tune; needs_review is our judgement that something is worth a look, and the two catch different failures.