space ocr
GuidesArticlesPricingDocs
developer

Extract invoice line items to JSON — the exact request and response shape

A copy-paste-ready JSON API contract for invoice line items using fields, values, path-keyed cells, review.flagged, and normalized numeric and date values.

8 min read· 2026-08-31

If you want to extract invoice line items to JSON, the part that matters is the contract: how rows are declared, where business values are returned, and how a review item maps back to the page.

space ocr models line items as a field of type: "array" whose children describe one row. Extracted data is returned in data.values. Coordinates and verification metadata are separate in data.cells[path]; for example, line_items[0].amount. The human-review queue is data.review.flagged, and deterministic date and number parsing is returned in data.normalized without changing the printed strings in values.

For a higher-level walkthrough, see extract line items from invoices. For production ingestion, see the invoice data extraction API.

The request: line items as a type:"array" field

You call POST /ocr/fields with one image and a fields array. A scalar field (invoice number, total) is a { name, type } pair. Line items are one field whose type is "array" and whose children describe the shape of a single row — the engine then returns as many rows as it finds on the page.

Field names and params are camelCase (the snake_case aliases like image_type still work but are deprecated). The engine takes raster images — JPEG, PNG, GIF, BMP, TIFF, WebP — sent as a URL or as pure base64; there's no PDF parsing at the engine layer (the web app converts PDF pages to PNG before OCR). Language is auto-detected, so there's no language parameter to set.

request body — POST /ocr/fields
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
  "image": "https://example.com/invoice.jpg",
  "imageType": "url",
  "fields": [
    { "name": "invoice_no", "type": "string", "required": true,
      "pattern": "^[A-Z0-9-]+$" },
    { "name": "invoice_date", "type": "date", "required": true },
    { "name": "total", "type": "number", "required": true, "min": 0 },
    {
      "name": "line_items",
      "type": "array",
      "children": [
        { "name": "description", "type": "string", "required": true },
        { "name": "quantity", "type": "integer", "min": 0 },
        { "name": "unit_price", "type": "number", "min": 0 },
        { "name": "amount", "type": "number", "min": 0 }
      ]
    }
  ]
}

Declare the fields you need, as above. If you are exploring an unfamiliar document, autoFields: true is an alternative; for a production integration, an explicit schema makes validation and downstream mappings predictable.

curl — one image in, structured JSON out
1
2
3
4
curl -s https://api.space-ocr.com/ocr/fields \
  -H "Authorization: Bearer $SPACE_OCR_API_KEY" \
  -H "Content-Type: application/json" \
  -d @request.json

The response: values and evidence are separate

The response is { "status": "success", "data": { ... } }. Read extracted fields from data.values. Read a row's union coordinates from a path such as data.cells["line_items[0]"], and a child cell from data.cells["line_items[0].amount"].

  • box is an axis-aligned { xmin, ymin, xmax, ymax } rectangle on a 0–1000 normalized grid.
  • quad is the four-point polygon that follows page rotation.
  • verified and review describe the current verification verdict. A review object can also reflect declared rules such as required, pattern, or range checks.
  • evidence contains diagnostic signals such as printed text and match ratio. Match ratio is supporting evidence, not a product-level confidence threshold.
  • data.review.flagged is the review queue. Each item has a path that directly addresses data.cells[path].
  • data.normalized contains deterministic parsed representations for date, number, and integer fields while data.values preserves the printed form.

Convert coordinates to pixels with box.xmin / 1000 * data.image.width and the corresponding height formula. Use data.image, not the original upload dimensions, because it reflects the frame used by the response.

response shape (representative)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
  "status": "success",
  "data": {
    "values": {
      "invoice_no": "INV-2049",
      "invoice_date": "2026/08/31",
      "total": "4,286",
      "line_items": [
        { "description": "96K調製豆乳", "quantity": "2", "unit_price": "316", "amount": "632" }
      ]
    },
    "normalized": {
      "invoice_date": "2026-08-31",
      "total": 4286,
      "line_items": [
        { "quantity": 2, "unit_price": 316, "amount": 632 }
      ]
    },
    "cells": {
      "line_items[0]": {
        "box": { "xmin": 92, "ymin": 405, "xmax": 944, "ymax": 448 },
        "quad": [{ "x": 92, "y": 405 }, { "x": 944, "y": 405 }, { "x": 944, "y": 448 }, { "x": 92, "y": 448 }],
        "verified": true,
        "review": null,
        "evidence": { "source": "vision_symbol_match" }
      },
      "line_items[0].amount": {
        "box": { "xmin": 860, "ymin": 412, "xmax": 944, "ymax": 446 },
        "quad": [{ "x": 860, "y": 412 }, { "x": 944, "y": 412 }, { "x": 944, "y": 446 }, { "x": 860, "y": 446 }],
        "verified": false,
        "review": { "reasons": ["text_mismatch"] },
        "evidence": { "text_match": false, "printed_text": "632", "match_ratio": 0.94 }
      }
    },
    "review": {
      "flagged": [{ "path": "line_items[0].amount", "reasons": ["text_mismatch"] }]
    },
    "image": { "width": 1600, "height": 2200 }
  }
}
✓ Verified

Treat coordinates and OCR comparison as evidence, not proof. The response keeps the model's extracted value separate from the OCR pass's printed text and location. When signals or declared validation rules disagree, the path appears in data.review.flagged. Two systems can still agree on the same misread, so downstream arithmetic and business-rule checks remain useful.

Parsing it in code

Read rows from data["values"]["line_items"]. For a row index and child name, build a path such as line_items[0].amount and look it up in data["cells"]. Build the human-review list from data["review"]["flagged"]; do not invent a fixed match-ratio threshold.

parse_line_items.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import json, os, urllib.request

request_body = json.loads(open("request.json", encoding="utf-8").read())
req = urllib.request.Request(
    "https://api.space-ocr.com/ocr/fields",
    data=json.dumps(request_body).encode(),
    headers={"Authorization": f"Bearer {os.environ['SPACE_OCR_API_KEY']}",
             "Content-Type": "application/json"},
)
payload = json.load(urllib.request.urlopen(req))["data"]
values = payload.get("values", {})
cells = payload.get("cells", {})
normalized = payload.get("normalized", {})

flagged = {item["path"]: item.get("reasons", [])
           for item in payload.get("review", {}).get("flagged", [])}

for i, row in enumerate(values.get("line_items", [])):
    row_review = {}
    for name in row:
        path = f"line_items[{i}].{name}"
        if path in flagged:
            row_review[name] = {"reasons": flagged[path], "cell": cells.get(path)}
    print({"values": row,
           "normalized": normalized.get("line_items", [{}])[i],
           "review": row_review})

JavaScript uses the same paths: iterate data.values.line_items, form line_items[${i}].${name}, and look that path up in data.cells and data.review.flagged.

lineitems_to_csv.py — unfold the array, one CSV row per item
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import csv

rows = values.get("line_items", [])
typed_rows = normalized.get("line_items", [])
invoice_no = values.get("invoice_no", "")
cols = ["description", "quantity", "unit_price", "amount"]

with open("line_items.csv", "w", encoding="utf-8-sig", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["invoice_no", *cols, "review_required"])
    for i, row in enumerate(rows):
        typed = typed_rows[i] if i < len(typed_rows) else {}
        output = [row.get("description", ""), typed.get("quantity"),
                  typed.get("unit_price"), typed.get("amount")]
        review_required = any(p.startswith(f"line_items[{i}]") for p in flagged)
        writer.writerow([invoice_no, *output, review_required])

Using utf-8-sig lets Excel recognize CJK text. Keep values for the printed representation and use normalized for arithmetic or database columns. Before export, compare quantity × unit price with amount and route any mismatch alongside the API's review queue. See scanned documents to CSV for stored-sheet workflows and the API docs for the complete response contract.

Array line items unfold to one CSV row per item, UTF-8 BOM so Excel reads CJK and currency text correctly.

How to extract invoice line items to JSON

  1. Get an API key
    Create an API key and authenticate with Authorization: Bearer. Check the current pricing page for quota and export policy.
  2. Declare the row schema
    Use fields with a line_items array and typed children. Add required, pattern, min, or other documented validation rules where the document contract supports them.
  3. Read values and normalized data
    Read printed business values from data.values and typed number, integer, or date representations from data.normalized.
  4. Process the review queue
    Iterate data.review.flagged and use each path to inspect the corresponding data.cells entry, including box, quad, review reasons, and evidence.
  5. Validate and export
    Run arithmetic and business-rule checks, then write one UTF-8-BOM CSV row per line item. Use the current pricing page for CSV export charges.
How do I request invoice line items as JSON?
Send POST /ocr/fields with a fields array. Define one field of type array and use children for description, quantity, unit_price, and amount. Use autoFields only when exploring an unfamiliar layout.
What is the response shape?
Business data is in data.values. Coordinates and verification metadata use path-keyed data.cells entries such as line_items[0].amount. The review queue is data.review.flagged, and typed date and numeric representations are in data.normalized.
How do coordinates map to pixels?
box and quad use a 0–1000 normalized grid. Convert them with data.image.width and data.image.height, which describe the response coordinate frame after image orientation and server processing.
Which values need human review?
Iterate data.review.flagged and use each item's path to retrieve data.cells[path]. Match ratio may appear under cell evidence, but it is not a fixed confidence gate.
How should I export line items to CSV?
Write one row per data.values.line_items item, use data.normalized for numeric calculations, preserve printed strings from values where needed, and use UTF-8 with a BOM for Excel and CJK text.
Related