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 */ } ]
    }
  ]
}

Page

Each page reports what it matched, how much of the form carried values, then the extractions themselves.

FieldTypeWhat it means
statusstringok when the page was read. Anything else names the reason it was not.
page_numberint1-based page within the file.
template_form_namestringWhich document type this page was recognised as.
matching_ratiofloat0 to 1. How closely the page lines up with that document type.
num_ocr_wordsintWords read off the page.
text_fields_totalintFields defined for this document type.
text_fields_matchedintOf those, how many carried a value. The rest were blank on this form.
validation_appliedboolWhether your validation rules ran on this page.
kv_pairsarrayThe 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,
  "value_backfilled": false                        // true when the field was blank on the form
}

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_backfilled: true means nothing was written in that box on the document. You get the field with an empty value rather than a guess, and the location of the box it should have been in.

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)"
  "detection_method":  "fill",          // how the mark was read
  "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. Use it to split the work: accept above your threshold, send the rest to a person. Where you draw that line is yours to set, 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.