dextract.
Sign in Book a demo

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.

The envelope below is the same for every document type. Field names are not: they come from the schema you define, so a claim, a loan file, and a tax form all arrive in this shape with their own field names. The examples here are from a health insurance claim.

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.

WhereSet whenWhat to do
page.needs_reviewThe 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_reviewThat 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 and confidence answer different questions. Confidence says how legibly the characters read. 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.

FieldTypeWhat it means
page_numberint1-based page within the file.
statusstringextracted, unreadable when the page carried no readable text, or not_recognized when it is not a document type you have set up.
text_fields_totalintFields defined for this document type.
text_fields_matchedintOf those, how many carried a value. The rest were blank on this form.
needs_reviewboolThe routing signal. True when this page should be seen by a person before anything downstream acts on it. See above.
page_dpiintThe resolution the page actually arrived at, measured rather than taken from the file header.
validation_appliedboolWhether your validation rules ran on this page.
fieldsarrayThe fields. See below.
tablesarrayThe tables. See below.
page_assetstringFilename of the page image in the .zip.
A page with many blank fields is normal. Most regulated forms carry sections that do not apply to a given claim or file, so 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.

A field that was blank on the document comes back with an empty 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.

Confidence is per field, not per document. One uncertain box on an otherwise clean claim should route one field to review, not the whole file.

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.