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.

View as Markdown

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"
            }
          }
        }
      }
    }
  }
}
EOF
Response
{
  "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.

TypeKeysValue delivered
stringpattern, min_length, max_lengthThe printed text, copied. Use it for identifiers that may start with a zero.
integermin, maxThe whole number printed in the span.
numbermin, max, decimalsThe amount printed in the span.
datemin, max, not_future, yearAn ISO date, YYYY-MM-DD, read from the span under Thai document conventions: day first, either era.
timeA 24-hour time, HH:MM, with seconds when printed.
enumvalues (1 to 50), aliases, strictOne of values, resolved from the printed span or chosen when nothing prints it.
iddigits, checkThe digits, with the count and the check digit verified.
thai_idformatThe 13 digits of a personal, juristic, or tax identifier, check digit verified.
emailThe address, domain lower-cased.
phoneformatA Thai number as E.164 or national.
postal_codeA five-digit code in use in Thailand.
provinceformatOne of Thailand's 77 provinces.
bankformatOne of the banks in Thailand.
insurerformatOne of the insurers licensed in Thailand.
card_schemeformatA card scheme, such as Visa, Mastercard, JCB, or UnionPay.
payment_methodformatCash, card, transfer, PromptPay, or a wallet.
legal_formformatA Thai legal form, such as บจก., บมจ., หจก., or ร้าน.
currencyformatAn ISO 4217 code from a symbol or a name.
unitformatA unit of measure from a line item.
amount_wordsAn amount written in words, in Thai or another language, delivered as a number.
objectfieldsAn object of its fields.
arrayitems, max_itemsThe 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:

TypeForms, default first
provincename_th, name_en, code (10), iso (TH-10)
bankshort (KBANK), code (004), name_th, name_en
insurershort, code (2037), name_th, name_en
thai_iddigits (1234567890121), grouped (1-2345-67890-12-1)
phonee164 (+66812345678), national (0812345678)
card_schemecode (MASTERCARD), name
payment_method, legal_form, currency, unitcode, 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.

reasonMeaning
not_an_objectThe schema, or the field definition at this path, is not a JSON object.
unknown_keyA key at this path is not part of the dialect. A field takes type, required, description, and the keys its type lists.
missing_fieldsThe object at this path has no fields key.
invalid_fieldsThe fields key at this path is not an object with at least one field.
invalid_nameThe field name at this path is not 1 to 64 characters of letters, digits, and underscores.
missing_typeThe field at this path has no type.
unknown_typeThe type at this path is not one of the dialect's types.
too_deepThe field at this path sits under more than 3 containers.
missing_itemsThe array at this path has no items definition.
nested_arrayThe array at this path holds arrays. An array holds leaves or objects.
invalid_max_itemsThe array at this path has no max_items, or one outside 1 to 200.
too_many_leavesThe schema counts more leaf fields than the model's ceiling, arrays counted once per element they are sized for.
invalid_flagThe required, strict, or not_future key at this path is not a boolean.
invalid_formatThe format at this path is not one the type offers.
invalid_boundA min or max at this path is not a number, or not an ISO date on a date field.
min_exceeds_maxThe min at this path is greater than its max.
invalid_countA decimals, min_length, or max_length at this path is not a whole number in range.
invalid_patternThe pattern at this path is not a valid regular expression of up to 200 characters.
invalid_digitsThe digits at this path is not a count from 1 to 64, or a [min, max] pair in that range.
invalid_checkThe check at this path is not none, thai_mod11, or luhn.
invalid_yearThe year at this path is not recent, strict, be, or ce.
invalid_descriptionThe description at this path is not a string of up to 200 characters.
invalid_enum_valuesThe values at this path is not a list of 1 to 50 strings.
duplicate_enum_valueTwo values at this path are the same once case is folded.
invalid_aliasesThe aliases at this path is not a map from a listed value to up to 20 spellings.
conflicting_aliasAn alias at this path spells a listed value, or could name two of them.
too_largeThe 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.

  • missing lists every required field delivered as null, for any reason.
  • unverified lists every field whose span could not be read as the field's type, or whose value the reading did not carry, each with a reason from the table below. Those fields are null.
  • truncated lists 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.
  • assumed lists every date whose year was printed with two digits, with printed and read_as. It is always present and empty when nothing was assumed.
  • evidence, when include_evidence is 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, when include_pages is 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:

reasonMeaning
not_in_readoutThe value does not appear in the text read from the document.
not_as_printedThe value differs from the printed span it was read from.
no_numberThe span carries no number.
ambiguous_numberThe span carries more than one number.
not_an_integerThe number in the span is not whole.
no_dateThe span carries no date.
ambiguous_dateThe span's date reads two ways, or its two-digit year fits no era under the strict rule.
no_timeThe span carries no time of day.
ambiguous_timeThe span carries more than one time.
not_an_enum_valueThe span resolves to none of the listed values.
ambiguous_matchThe span resolves to two listed values equally well.
not_13_digitsThe identifier does not carry exactly thirteen digits.
bad_checksumThe identifier's check digit does not hold.
wrong_digit_countThe identifier's digit count is outside the field's digits.
not_an_emailThe span is not an email address.
not_a_phoneThe span is not a Thai telephone number.
not_a_postal_codeThe span is not a postal code in use in Thailand.
no_amount_wordsThe span is not an amount written in words.
below_minThe number is below the field's min.
above_maxThe number is above the field's max.
too_many_decimalsThe number carries more decimals than the field allows.
too_shortThe text is shorter than the field's min_length.
too_longThe text is longer than the field's max_length.
pattern_mismatchThe text does not match the field's pattern.
before_minThe date is before the field's min.
after_maxThe date is after the field's max.
in_the_futureThe 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.