# Extract fields from a document

`POST https://api.paxalabs.com/v1/extract`

POST /v1/extract reads a PDF or image and fills in a schema of typed fields, each value the span printed in the document or null with the reason. Charged in credits per page at a rate the schema's leaf count selects, before reading, refunded on failure.

Send the file as base64 in `document` and the fields as `schema`. An image counts as one page. The [Document Extraction guide](https://paxalabs.com/docs/extraction) covers the schema dialect, every leaf type, the leaf-counting rule, and how to read `missing`, `unverified`, `truncated`, and `assumed`. A schema outside the dialect answers [400 schema_invalid](https://paxalabs.com/docs/errors#schema_invalid) with `path` and `reason`, uncharged.

## Headers

- `authorization` (Bearer pxa_..., required): Authorization: Bearer pxa_... (recommended).
- `x-api-key` (pxa_..., optional): Alternative to the Authorization header. Ignored when Authorization is present.
- `idempotency-key` (string, optional, 1 to 200 characters, pattern ^[!-~]+$): Makes a retry safe. Two requests carrying the same key charge once and synthesize the same content. One request per key runs at a time. A concurrent duplicate answers 409 idempotency_in_flight. Reuse a key only to retry an identical request. A changed payload is rejected with 422. Accepts up to 200 printable ASCII characters.

See authentication at https://paxalabs.com/docs/authentication.

## Body

- `document` (string, required, 1 to 41,000,000 characters): The document to extract from, as the base64 encoding of a PDF, PNG, JPEG, or WebP file. An image counts as one page. Cost is 13 credits per page for a schema of up to 50 leaf fields and 19.5 credits per page beyond that, charged before extraction and refunded automatically when extraction fails. A PDF may carry up to 20 pages, and more answers 400 too_many_pages. The decoded file may be up to 10,485,760 bytes, and larger answers 413 document_too_large. A file that cannot be read as one of the four formats answers 400 document_invalid, and a PDF that needs a password to open answers 400 document_password_required. Send one document per request. A file holding several documents gets one object, with no promise which document it describes. GET /v1/models reports the ceilings as max_pages, max_bytes, and max_leaves.
- `model` (string, required, 1 to 100 characters): Extraction model id, for example paxa-doc-extract-v1. The id is the contract version: the dialect, the response shape, and the leaf-counting rule are fixed under it, and a change ships as a new id served beside it. GET /v1/models lists the served catalog.
- `schema` (object, required): The fields to extract, as an object with one key, fields, mapping each field name to its definition. A definition carries a type, an optional required flag, an optional description of up to 200 characters the model reads, and the keys its type offers. Leaf types: "string", "integer", "number", "date", "time", "enum", "id", "thai_id", "email", "phone", "postal_code", "province", "bank", "insurer", "card_scheme", "payment_method", "legal_form", "currency", "unit", "amount_words". Containers: "object" with its own fields, and "array" with items (a leaf or an object) and a required max_items of 1 to 200. Names are 1 to 64 characters of letters, digits, and underscores. At most 3 containers sit below the root. An array holds leaves or objects, and an array inside an array is refused. The schema's leaf count is every leaf once plus every leaf inside an array once per element the array is sized for, and it must stay within the model's max_leaves. A schema outside the dialect answers 400 schema_invalid with the offending path and a reason, uncharged. A serialized schema over 131,072 bytes answers the same code with reason too_large. The Document Extraction guide is the full reference for every type, its keys, and its delivered form.
- `include_pages` (boolean, optional, default false): true adds a pages array to the response: the text read from each page as GitHub-flavored Markdown, the reading the fields were extracted from, at no extra charge. Omitted, the response carries the fields alone.
- `include_evidence` (boolean, optional, default false): true adds an evidence object to the response, mapping each field path to the printed span its value was read from, whether or not a value could be read from it. Costs nothing extra. Omitted, the response carries no spans.

## Response

- `status` ("complete" or "incomplete", required): "complete" when every required field carries a value, "incomplete" otherwise. Computed from the fields alone. A file that is not what the schema describes comes back "incomplete" with most required fields in missing, which is the wrong-document signal.
- `fields` (object, required): Your schema's tree with plain values. A string field carries the printed text and a number or integer field a number. A date is ISO YYYY-MM-DD and a time is 24-hour HH:MM, with seconds when printed. An enum carries one listed value, and a closed-set field the form its format asked for. Null wherever nothing was read. Every value is a span printed in the document, or null. Nothing is inferred, computed, or looked up.
- `missing` (array of strings, required): Every required field delivered as null, for any reason.
- `unverified` (array of objects, required): Every field delivered as null because its span could not be read as the field's type, each with its reason. Empty when every value stood.
  - `unverified[].path` (string, required): The field delivered as null. Paths are in dot form, array elements indexed: "items[2].amount".
  - `unverified[].reason` (string, required): Why the span could not stand as the field's value: one of not_in_readout, not_as_printed, no_number, ambiguous_number, not_an_integer, no_date, ambiguous_date, no_time, ambiguous_time, not_an_enum_value, ambiguous_match, not_13_digits, bad_checksum, wrong_digit_count, not_an_email, not_a_phone, not_a_postal_code, no_amount_words, below_min, above_max, too_many_decimals, too_short, too_long, pattern_mismatch, before_min, after_max, in_the_future. The Document Extraction reference states each one.
- `truncated` (array of strings, required): Every array whose document carried more elements than it was sized for. The first max_items elements are in fields, in reading order. Raise max_items to read the rest. Empty when every array fit.
- `assumed` (array of objects, required): Every date whose year was printed with two digits, with the digits and the year delivered. Always present and empty when nothing was assumed. A caller who would rather decide the era reads it here.
  - `assumed[].path` (string, required): The date field. Paths are in dot form, array elements indexed: "items[2].amount".
  - `assumed[].printed` (string, required): The two digits of the year as printed, for example "69".
  - `assumed[].read_as` (integer, required): The four-digit year delivered in the field, in the Common Era.
- `evidence` (object, optional): Field path to the printed span the value was read from, for every field whose span the reading carries. Present when the request set include_evidence to true.
- `pages` (array of objects, optional): One entry per page of the document, in page order. The fields were extracted from this reading. Present when the request set include_pages to true.
  - `pages[].page` (integer, required): Page number, starting at 1. An image request has exactly one page.
  - `pages[].markdown` (string, required): The page's content as GitHub-flavored Markdown, in reading order. The fields were read from this text.
- `usage` (object, required): What the request was billed for.
  - `usage.pages` (integer, required): Pages this request was billed for.
  - `usage.leaves` (integer, required): The schema's leaf count, which decides the per-page rate: up to 50 bills at credits_per_page, more at large_schema_credits_per_page.
  - `usage.credits` (number, required): What this delivery cost, in credits, exact to a hundredth. An idempotent replay reports the ORIGINAL request's charge, since that one charge is what paid for this delivery too; your balance moves only once.

## Errors

- `validation` (400): The request body or headers failed validation against the endpoint schema.
- `unknown_model` (400): The model field does not name a served model.
- `schema_invalid` (400): The extraction schema is outside the dialect. The problem body carries two extra fields: path, the offending field in dot form (empty for the schema as a whole), and reason, one stable code from the schema reasons list. Nothing was charged.
- `document_invalid` (400): The document field could not be read as a PDF, PNG, JPEG, or WebP file. A damaged or truncated PDF answers this code. Nothing was charged.
- `document_password_required` (400): The PDF needs a password to open. A PDF that carries permissions-only encryption, the kind that opens without being asked for a password, is read normally. Nothing was charged.
- `document_too_large` (413): The decoded document exceeds the model's size ceiling. Nothing was charged.
- `too_many_pages` (400): The document has more pages than the model's per-request ceiling. Nothing was charged.
- `unauthorized` (401): The request carried no API key, or the key is invalid or disabled.
- `insufficient_credits` (402): The account does not have enough credits for this request. Nothing was charged.
- `key_limit` (403): This API key reached its spending cap. Nothing was charged.
- `idempotency_in_flight` (409): Another request with this Idempotency-Key is in flight right now.
- `idempotency_refunded` (409): The original request under this Idempotency-Key failed and was refunded.
- `idempotency_mismatch` (422): This Idempotency-Key was already used for a different request.
- `content_blocked` (422): The upstream safety system declined to process this content. The charge was refunded.
- `rate_limited` (429): Requests per minute for the plan are exhausted. One window covers the whole account, across every product and every key.
- `concurrency_limited` (429): The account holds the plan's full count of concurrent requests for this product. Nothing was charged. Each product is limited separately, and an open live connection holds one speech slot.
- `internal` (500): Request state was inconsistent on the server.
- `provider_error` (502): Model inference failed after the request was charged.
- `provider_unavailable` (503): The model behind this endpoint is not available right now. Nothing was charged.

## Example

curl:

```bash
# Encode without line wrapping: wrapped base64 breaks the JSON string.
DOC=$(base64 < invoice.pdf | tr -d '\n')
# --max-time covers a multi-page document; curl defaults to no limit.
curl -X POST https://api.paxalabs.com/v1/extract \
  --max-time 300 \
  -H "Authorization: Bearer $PAXA_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<EOF
{
  "document": "$DOC",
  "model": "paxa-doc-extract-v1",
  "schema": {
    "fields": {
      "seller": {
        "type": "string",
        "required": true,
        "description": "The shop name as printed at the top"
      },
      "total": {
        "type": "number",
        "required": true
      },
      "issued_on": {
        "type": "date"
      },
      "items": {
        "type": "array",
        "max_items": 5,
        "items": {
          "type": "object",
          "fields": {
            "name": {
              "type": "string"
            },
            "amount": {
              "type": "number"
            }
          }
        }
      }
    }
  }
}
EOF
```

TypeScript:

```typescript
import { readFile } from "node:fs/promises";

const document = (await readFile("invoice.pdf")).toString("base64");
const schema = {
  "fields": {
    "seller": {
      "type": "string",
      "required": true,
      "description": "The shop name as printed at the top"
    },
    "total": {
      "type": "number",
      "required": true
    },
    "issued_on": {
      "type": "date"
    },
    "items": {
      "type": "array",
      "max_items": 5,
      "items": {
        "type": "object",
        "fields": {
          "name": {
            "type": "string"
          },
          "amount": {
            "type": "number"
          }
        }
      }
    }
  }
};

const response = await fetch("https://api.paxalabs.com/v1/extract", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PAXA_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ document, model: "paxa-doc-extract-v1", schema }),
  // A multi-page document can run for minutes; give it room.
  signal: AbortSignal.timeout(300_000),
});

if (!response.ok) throw new Error(`Extraction failed: ${response.status}`);
const { status, fields, missing, usage } = await response.json();
console.log(status, fields.seller, fields.total, missing);
console.log(`${usage.leaves} leaves, ${usage.credits} credits`);
```

Python:

```python
import base64
import os

import requests

with open("invoice.pdf", "rb") as file:
    document = base64.b64encode(file.read()).decode()

schema = {
    "fields": {
        "seller": {
            "type": "string",
            "required": True,
            "description": "The shop name as printed at the top",
        },
        "total": {
            "type": "number",
            "required": True,
        },
        "issued_on": {
            "type": "date",
        },
        "items": {
            "type": "array",
            "max_items": 5,
            "items": {
                "type": "object",
                "fields": {
                    "name": {
                        "type": "string",
                    },
                    "amount": {
                        "type": "number",
                    },
                },
            },
        },
    },
}

response = requests.post(
    "https://api.paxalabs.com/v1/extract",
    headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"},
    json={"document": document, "model": "paxa-doc-extract-v1", "schema": schema},
    # A multi-page document can run for minutes; give it room.
    timeout=300,
)
response.raise_for_status()
body = response.json()
print(body["status"], body["fields"]["seller"], body["fields"]["total"])
print(body["usage"]["leaves"], "leaves,", body["usage"]["credits"], "credits")
```

Response:

```json
{
  "status": "complete",
  "fields": {
    "seller": "ร้านข้าวแกงบ้านสวน สาขาสีลม",
    "total": 70,
    "issued_on": null,
    "items": [
      {
        "name": "ข้าวแกงเขียวหวานไก่",
        "amount": 60
      },
      {
        "name": "น้ำเปล่า",
        "amount": 10
      }
    ]
  },
  "missing": [],
  "unverified": [],
  "truncated": [],
  "assumed": [],
  "usage": {
    "pages": 1,
    "leaves": 13,
    "credits": 13
  }
}
```
