Extract fields from documents
Document Extraction reads a PDF or image and fills in a schema of typed fields you define, each value the span printed in the document or null with the reason. Billing is per page at a rate the schema's size selects, charged before reading and refunded automatically on failure.
POST /v1/extract reads a PDF, PNG, JPEG, or WebP file and fills in the schema you send. Send the file as base64 in document, the model id paxa-doc-extract-v1, and a schema of typed fields. Every value delivered is text printed in the document, read as the field's type, or null with the reason. Nothing is inferred or looked up. Like every endpoint, the request is charged before inference and refunded automatically when inference fails (Credits).
# 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"
}
}
}
}
}
}
}
EOFimport { 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`);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"){
"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
}
}Billing#
A page costs 13 credits when the schema counts up to 50 leaf fields, and 19.5 credits when it counts 51 to 100. That is two and three times the OCR rate of 6.5. The reading is included. A page is one PDF page or one image file. The page count is read from the document and the leaf count from the schema, both before the charge. The cost is known before any reading starts. Evidence and the reading change no charge.
The leaf count is every leaf field once, plus every leaf inside an array once per element the array is sized for. A schema with fifteen header fields and three fields per line item over ten items counts 45 leaves. The same schema over twenty items counts 75. The response reports the count in usage.leaves beside usage.pages and usage.credits.
The charge is reported only in the body's usage.credits. This endpoint sends no x-credits-charged header. When extraction fails after the charge, the refund is automatic and the response is 502 provider_error. A document that carries none of the fields is a delivery, with the fields null, and is charged.
Write a schema#
schema is 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, and the keys its type offers. The description is the one piece of your prose the model reads. It is where a field's meaning goes, for example "Seller name as printed at the top". Names are 1 to 64 characters of letters, digits, and underscores.
Two containers nest fields. An object carries its own fields. An array carries items, a leaf or an object, and a required max_items of 1 to 200. At most 3 containers sit below the root. items[].tax.rate is allowed and a fourth level is refused. An array holds leaves or objects. An array inside an array is refused.
| Type | Keys | Value delivered |
|---|---|---|
| string | pattern, min_length, max_length | The printed text, copied. Use it for identifiers that may start with a zero. |
| integer | min, max | The whole number printed in the span. |
| number | min, max, decimals | The amount printed in the span. |
| date | min, max, not_future, year | An ISO date, YYYY-MM-DD, read from the span under Thai document conventions: day first, either era. |
| time | A 24-hour time, HH:MM, with seconds when printed. | |
| enum | values (1 to 50), aliases, strict | One of values, resolved from the printed span or chosen when nothing prints it. |
| id | digits, check | The digits, with the count and the check digit verified. |
| thai_id | format | The 13 digits of a personal, juristic, or tax identifier, check digit verified. |
| The address, domain lower-cased. | ||
| phone | format | A Thai number as E.164 or national. |
| postal_code | A five-digit code in use in Thailand. | |
| province | format | One of Thailand's 77 provinces. |
| bank | format | One of the banks in Thailand. |
| insurer | format | One of the insurers licensed in Thailand. |
| card_scheme | format | A card scheme, such as Visa, Mastercard, JCB, or UnionPay. |
| payment_method | format | Cash, card, transfer, PromptPay, or a wallet. |
| legal_form | format | A Thai legal form, such as บจก., บมจ., หจก., or ร้าน. |
| currency | format | An ISO 4217 code from a symbol or a name. |
| unit | format | A unit of measure from a line item. |
| amount_words | An amount written in words, in Thai or another language, delivered as a number. | |
| object | fields | An object of its fields. |
| array | items, max_items | The first max_items printed elements, in reading order. |
Constraints#
A constraint says what the field is. A printed value that fails one is delivered null with the reason, and the span stays in evidence when you asked for it. Bound a number with min, max, and decimals. Shape a string with pattern, a regular expression of up to 200 characters matched against the whole value, and with min_length and max_length. Bound a date with min and max as ISO dates and with not_future, judged against today in Thailand. An id takes digits as a count or a [min, max] pair from 1 to 64, and check as none, thai_mod11, or luhn. A check detects a wrong digit and never corrects it.
year says how a date reads a two-digit year. The default, recent, takes whichever era puts the date in the recent past. A bare 69 on a receipt is then BE 2569. strict delivers a year only when one era fits. be and ce name the era you know. Whichever rule applies, every two-digit year read is listed in the response's assumed with the digits as printed.
Enums and built-in sets#
An enum lists its values and may carry aliases, a map from a value to up to 20 other spellings, for example "bangkok" to กรุงเทพมหานคร, กทม., and BKK. The printed span is resolved against the set. An exact match, case folded, wins first. Then a span containing exactly one value's spellings of four characters or more. Then a span one edit from exactly one value, with no other value within two edits. A tie resolves to nothing and the field is null with ambiguous_match. Set strict to true for exact matches only. An enum is also the one field that may be a judgment: a classification nothing prints, such as the kind of document, is chosen from the listed values.
The closed-set types, province, bank, insurer, postal_code, card_scheme, payment_method, legal_form, currency, and unit, are enums whose tables the service ships, including the abbreviations documents print. A former bank name resolves to its successor. A bare currency sign such as $ names several currencies and is delivered as the sign. format picks the delivered form, always a lossless one:
| Type | Forms, default first |
|---|---|
| province | name_th, name_en, code (10), iso (TH-10) |
| bank | short (KBANK), code (004), name_th, name_en |
| insurer | short, code (2037), name_th, name_en |
| thai_id | digits (1234567890121), grouped (1-2345-67890-12-1) |
| phone | e164 (+66812345678), national (0812345678) |
| card_scheme | code (MASTERCARD), name |
| payment_method, legal_form, currency, unit | code, name_th, name_en |
Schema refusals#
A schema outside the dialect answers 400 schema_invalid before the document is read, uncharged. The problem body carries two fields beyond the minimum shape. path names the offending field in dot form, empty for the root. reason is one of the values below. A schema over 128 KB as JSON answers the same code with reason too_large.
| reason | Meaning |
|---|---|
| not_an_object | The schema, or the field definition at this path, is not a JSON object. |
| unknown_key | A key at this path is not part of the dialect. A field takes type, required, description, and the keys its type lists. |
| missing_fields | The object at this path has no fields key. |
| invalid_fields | The fields key at this path is not an object with at least one field. |
| invalid_name | The field name at this path is not 1 to 64 characters of letters, digits, and underscores. |
| missing_type | The field at this path has no type. |
| unknown_type | The type at this path is not one of the dialect's types. |
| too_deep | The field at this path sits under more than 3 containers. |
| missing_items | The array at this path has no items definition. |
| nested_array | The array at this path holds arrays. An array holds leaves or objects. |
| invalid_max_items | The array at this path has no max_items, or one outside 1 to 200. |
| too_many_leaves | The schema counts more leaf fields than the model's ceiling, arrays counted once per element they are sized for. |
| invalid_flag | The required, strict, or not_future key at this path is not a boolean. |
| invalid_format | The format at this path is not one the type offers. |
| invalid_bound | A min or max at this path is not a number, or not an ISO date on a date field. |
| min_exceeds_max | The min at this path is greater than its max. |
| invalid_count | A decimals, min_length, or max_length at this path is not a whole number in range. |
| invalid_pattern | The pattern at this path is not a valid regular expression of up to 200 characters. |
| invalid_digits | The digits at this path is not a count from 1 to 64, or a [min, max] pair in that range. |
| invalid_check | The check at this path is not none, thai_mod11, or luhn. |
| invalid_year | The year at this path is not recent, strict, be, or ce. |
| invalid_description | The description at this path is not a string of up to 200 characters. |
| invalid_enum_values | The values at this path is not a list of 1 to 50 strings. |
| duplicate_enum_value | Two values at this path are the same once case is folded. |
| invalid_aliases | The aliases at this path is not a map from a listed value to up to 20 spellings. |
| conflicting_alias | An alias at this path spells a listed value, or could name two of them. |
| too_large | The schema is larger than 131,072 bytes as JSON. |
Read the response#
fields is your schema's tree with plain values, null wherever nothing was read. status is "complete" when every required field carries a value and "incomplete" otherwise. It is 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 how to detect the wrong document. There is no confidence score. A faithful copy of a misread digit is invisible to any score, and the types that carry a check digit are how such a misread is caught.
missinglists every required field delivered as null, for any reason.unverifiedlists every field whose span could not be read as the field's type, or whose value the reading did not carry, each with areasonfrom the table below. Those fields are null.truncatedlists every array whose document carried more elements than it was sized for. The firstmax_itemselements are infields, in reading order. Raisemax_itemsto read the rest.assumedlists every date whose year was printed with two digits, withprintedandread_as. It is always present and empty when nothing was assumed.evidence, wheninclude_evidenceis true, maps each field path to the printed span its value was read from, whether or not a value could be read from it.pages, wheninclude_pagesis true, is the text read from each page as Markdown, the same reading POST /v1/ocr returns. The fields were extracted from it.
Paths are in dot form with array elements indexed, for example "items[2].amount". The unverified reasons:
| reason | Meaning |
|---|---|
| not_in_readout | The value does not appear in the text read from the document. |
| not_as_printed | The value differs from the printed span it was read from. |
| no_number | The span carries no number. |
| ambiguous_number | The span carries more than one number. |
| not_an_integer | The number in the span is not whole. |
| no_date | The span carries no date. |
| ambiguous_date | The span's date reads two ways, or its two-digit year fits no era under the strict rule. |
| no_time | The span carries no time of day. |
| ambiguous_time | The span carries more than one time. |
| not_an_enum_value | The span resolves to none of the listed values. |
| ambiguous_match | The span resolves to two listed values equally well. |
| not_13_digits | The identifier does not carry exactly thirteen digits. |
| bad_checksum | The identifier's check digit does not hold. |
| wrong_digit_count | The identifier's digit count is outside the field's digits. |
| not_an_email | The span is not an email address. |
| not_a_phone | The span is not a Thai telephone number. |
| not_a_postal_code | The span is not a postal code in use in Thailand. |
| no_amount_words | The span is not an amount written in words. |
| below_min | The number is below the field's min. |
| above_max | The number is above the field's max. |
| too_many_decimals | The number carries more decimals than the field allows. |
| too_short | The text is shorter than the field's min_length. |
| too_long | The text is longer than the field's max_length. |
| pattern_mismatch | The text does not match the field's pattern. |
| before_min | The date is before the field's min. |
| after_max | The date is after the field's max. |
| in_the_future | The date is after today in Thailand, and the field set not_future. |
Note
There is no boolean type. A checkbox's label is on the page and its tick is not. Model a checkbox as an enum over its printed labels.
Page, size, and schema limits#
One request reads at most 20 pages, and a longer document answers 400 too_many_pages. The decoded file may be at most 10 MiB, and a larger one answers 413 document_too_large. A schema counts at most 100 leaves and is at most 128 KB as JSON. The document rules of the OCR guide apply unchanged: the format is detected from the bytes, an image is one page, and an unreadable or password-protected file is refused before the charge. One document per request. A file holding several documents gets one set of fields.
Reliability and retries#
The response is one buffered JSON body. A multi-page document can take minutes. Set the client timeout generously. The samples above allow 300 seconds.
A request that runs long enough writes its response headers before the outcome is known. Intermediate proxies abandon a connection that has been silent too long, and the early headers prevent that. Such a response always has status 200. It reports a failure in the body, as the same problem document a failed request returns: a title and a status where fields would be. Treat a body carrying title as the error it describes.
Retries are safe with an Idempotency-Key. A replay of a delivered request serves the same fields again without a second charge. When extraction fails, the charge is refunded and a fresh request needs a new key. The model id is the contract version. The dialect, the response shape, and the leaf-counting rule are fixed under paxa-doc-extract-v1, and a change ships as a new id served beside it.