{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/arkrel-com/json-schema/arkrel-com-result-schema.json",
  "title": "Result",
  "x-generated": "2026-11-01",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/arkrel-com-openapi.yaml#/components/schemas/Result",
  "type": "object",
  "required": [
    "document_type",
    "fields",
    "tables",
    "checks",
    "pages",
    "warnings"
  ],
  "properties": {
    "status": {
      "allOf": [
        {
          "$ref": "#/$defs/DocumentStatus"
        }
      ],
      "description": "The document's terminal status, included here too (as well as\non GET /v1/documents/{id}) since `GET .../result?format=json`\nreturns the verifier's full VerificationResult object as-is\n(src/verify/types.ts) rather than a trimmed view, callers\nthat only ever fetch the result don't need a second request\njust to read this.\n"
    },
    "reasons": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/Reason"
      },
      "description": "Same reasons array as GET /v1/documents/{id}, see the note on `status` above for why it's repeated here."
    },
    "document_type": {
      "$ref": "#/$defs/DocumentType"
    },
    "fields": {
      "type": "object",
      "additionalProperties": {
        "$ref": "#/$defs/FieldValue"
      }
    },
    "tables": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/Table"
      }
    },
    "checks": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/Check"
      }
    },
    "pages": {
      "type": "object",
      "required": [
        "total",
        "processed"
      ],
      "properties": {
        "total": {
          "type": "integer"
        },
        "processed": {
          "type": "integer"
        },
        "sizes": {
          "type": "array",
          "description": "Actual per-page point dimensions, when the engine's layout\nmodel knows them. Present so a `FieldValue.source.bbox`\n(always absolute PDF points, top-left origin) can be drawn\naccurately without guessing page size. When absent, assume\nUS Letter (612x792 pt) as a last resort only.\n",
          "items": {
            "type": "object",
            "required": [
              "page",
              "width",
              "height"
            ],
            "properties": {
              "page": {
                "type": "integer"
              },
              "width": {
                "type": "number"
              },
              "height": {
                "type": "number"
              }
            }
          }
        }
      }
    },
    "warnings": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "engine": {
      "type": "object",
      "description": "Which engine actually processed this document. `kind` is derived\nserver-side from `name` (never sent by either engine itself):\n`primary` for our own self-hosted extraction servers, `fallback`\nfor the automated Cloudflare Workers AI path used when the\nprimary servers are briefly unavailable. See /docs/limits.\n",
      "properties": {
        "name": {
          "type": "string",
          "enum": [
            "homelab-python-engine",
            "workers-ai-fallback"
          ]
        },
        "kind": {
          "type": "string",
          "enum": [
            "primary",
            "fallback"
          ]
        },
        "model": {
          "type": "string"
        },
        "version": {
          "type": "string"
        },
        "duration_ms": {
          "type": "integer"
        }
      }
    }
  },
  "$defs": {
    "Check": {
      "type": "object",
      "required": [
        "id",
        "name",
        "passed"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "passed": {
          "type": "boolean"
        },
        "expected": {
          "description": "The value this check expected, when applicable (a check like `no_data_extracted` has none). Type varies per check, same reason as `FieldValue.value`.",
          "type": [
            "string",
            "number",
            "boolean",
            "object",
            "array",
            "null"
          ],
          "items": {}
        },
        "actual": {
          "description": "The value this check actually found, when applicable. Type varies per check, same reason as `FieldValue.value`.",
          "type": [
            "string",
            "number",
            "boolean",
            "object",
            "array",
            "null"
          ],
          "items": {}
        },
        "detail": {
          "type": "string"
        }
      }
    },
    "DocumentStatus": {
      "type": "string",
      "description": "A document always reaches one of the four terminal states below\nbefore its processing deadline. queued and processing are\ntransient. There is no state in which \"nothing came back\" is\nambiguous with \"extraction found nothing\": an empty extraction is\n`failed` with a NO_DATA_EXTRACTED reason, never silently `succeeded`.\n",
      "enum": [
        "queued",
        "processing",
        "succeeded",
        "partial",
        "needs_review",
        "failed"
      ]
    },
    "DocumentType": {
      "type": "string",
      "enum": [
        "invoice",
        "receipt",
        "purchase_order",
        "bank_statement",
        "delivery_note",
        "custom"
      ]
    },
    "FieldValue": {
      "type": "object",
      "required": [
        "value",
        "confidence",
        "source"
      ],
      "properties": {
        "value": {
          "description": "The extracted value. Any JSON scalar, array, or object, or `null` when the field genuinely has none. There is no single OpenAPI 3.0-style `nullable` here because the type itself varies per field (an amount is a number, a line-item table cell can be a string, etc).",
          "type": [
            "string",
            "number",
            "boolean",
            "object",
            "array",
            "null"
          ],
          "items": {}
        },
        "confidence": {
          "type": "number",
          "minimum": 0,
          "maximum": 1
        },
        "source": {
          "type": "object",
          "required": [
            "page"
          ],
          "properties": {
            "page": {
              "type": "integer"
            },
            "text_span": {
              "type": "array",
              "items": {
                "type": "integer"
              },
              "minItems": 2,
              "maxItems": 2
            },
            "bbox": {
              "type": "array",
              "description": "[x0, y0, x1, y1] in absolute PDF points (1/72 inch), never a\n0-1 fraction of page size. Top-left origin (x right, y down);\nthe engine normalizes this, so consumers never need to flip\nfor PDF's native bottom-left space. Pair with `Result.pages.sizes`\nfor the page's actual width/height; fall back to US Letter\n(612x792 pt) only when `pages.sizes` is entirely absent.\n",
              "items": {
                "type": "number"
              },
              "minItems": 4,
              "maxItems": 4
            }
          }
        }
      }
    },
    "Reason": {
      "type": "object",
      "required": [
        "code",
        "message"
      ],
      "properties": {
        "code": {
          "type": "string",
          "anyOf": [
            {
              "enum": [
                "MULTI_ACCOUNT_DOCUMENT",
                "PDF_PASSWORD_PROTECTED",
                "NO_PAGES_PROCESSED",
                "NO_DATA_EXTRACTED",
                "UNSUPPORTED_DOCUMENT",
                "ENGINE_ERROR",
                "DEADLINE_EXCEEDED",
                "QUOTA_EXCEEDED_AT_COMPLETION",
                "PAGES_INCOMPLETE",
                "LINE_ITEMS_SUBTOTAL_MISMATCH",
                "SUBTOTAL_TAX_TOTAL_MISMATCH",
                "BANK_BALANCE_MISMATCH",
                "RUNNING_BALANCE_DISCONTINUITY",
                "TABLE_ROW_COUNT_MISMATCH",
                "REQUIRED_FIELD_MISSING",
                "TAX_RATE_INCONSISTENT",
                "DATE_SANITY_FAILED",
                "CURRENCY_INCONSISTENT",
                "LOW_CONFIDENCE",
                "PAGE_LIMIT",
                "INBOUND_DAILY_CEILING_EXCEEDED"
              ]
            },
            {
              "type": "string",
              "description": "Additional ingestion or future reason codes."
            }
          ],
          "description": "Machine-readable, stable reason code. Never repurposed: a new\nfailure mode always gets a new code. See the full table of\ncodes, the status each one drives, and what it means at\nhttps://arkrel.com/docs/errors.\n"
        },
        "message": {
          "type": "string"
        },
        "page": {
          "type": "integer"
        },
        "field": {
          "type": "string"
        }
      }
    },
    "Table": {
      "type": "object",
      "required": [
        "name",
        "rows",
        "row_count_expected",
        "row_count_extracted",
        "row_count_detection_available"
      ],
      "properties": {
        "name": {
          "type": "string"
        },
        "rows": {
          "type": "array",
          "items": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "row_count_expected": {
          "type": "integer",
          "description": "Rows the engine detected in the raw table structure"
        },
        "row_count_extracted": {
          "type": "integer",
          "description": "Rows that made it into the structured `rows` array"
        },
        "row_count_detection_available": {
          "type": "boolean",
          "description": "False when the engine had no independent structural signal to\ndetect a row count from (e.g. a scanned table with no ruled\nlines to key off), so `row_count_expected` was set equal to\n`row_count_extracted` rather than invented. When false, the\n`table_row_count_reconciliation` check for this table always\npasses trivially and should not be read as a real cross-check;\nits `detail` field says so explicitly.\n"
        }
      }
    }
  }
}