Skip to the content

Document types

Define the fields you want read, the checks they must pass, and publish versions.

On this page

A document type says what you want read off a document and how sure ReceViz has to be. It has a key (such as payment_receipt), a name, and a definition: the fields, the rules the fields must satisfy together, and a few settings. Extraction always runs against a published version of it, never against a draft.

Start from a template#

Three built-in templates carry ReceViz's tuned rules for receipts and invoices. Create a document type from one in the console, or with template in POST /v1/document-types, then rename, drop, add or tighten fields as your own.

Built-in templates
TemplateReadsChecks
payment_receipt
Payment Terminal Receipt
amount, currency, transaction_date, transaction_time, receipt_number, rrn, auth_code, terminal_id, payment_method, card_scheme, card_last_four, approved, merchant, merchant_addressField patterns; the date not in the future and at most 400 days old
retail_receipt
Retail Receipt
merchant, merchant_address, merchant_phone, merchant_category, date, time, receipt_number, subtotal, tax, total, currency, trn, payment_method, card_scheme, card_last_four, and items (sku, description, type, quantity, unit_price, amount)subtotal + tax = total; the date not in the future and at most 730 days old
invoice
Invoice
invoice_number, invoice_date, due_date, supplier_name, supplier_trn, customer_name, subtotal, tax, total, currencysubtotal + tax = total; the due date on or after the invoice date

merchant_category is inferred from the merchant's name and what was bought, not read: its source is inferred. GET /v1/document-types/{key} returns any document type's exact definition.

Fields#

A field is an object in fields. Only key is required; everything else has a default.

keystringrequired

The machine name, and the field's name in data: a lowercase letter, then up to 63 of a-z, 0-9 and _. Unique among its siblings.

labelstring

What a person sees. Defaults to the key, humanized.

typestring

string (the default), integer, decimal, currency, date, datetime, boolean, enum, array or object. Money and decimals come back as JSON numbers, dates as YYYY-MM-DD, date-times as YYYY-MM-DDTHH:MM:SS.

requiredboolean

A required field that is not found makes the result need review (missing_required). The value is still null, never invented.

descriptionstring

Up to 500 characters. It appears in the JSON Schema of the document type, and the verifier reads it.

aliasesarray of strings

Labels the value may be printed under, such as TOTAL, GRAND TOTAL, AMOUNT DUE. Up to 25, each up to 60 characters.

exclude_aliasesarray of strings

Labels that mean something else and must not be taken for this field, such as SUBTOTAL for a total.

semanticstring

Read the field with ReceViz's tuned rules for that kind of fact (see below). The semantic must fit the field's type.

positionstring

any (the default), top or bottom: a hint that nudges rows in that part of the page.

validationobject

Checks the value must pass. See Validation.

enumarray

For enum fields, the values it may take: plain strings, or {"value", "label", "aliases"} where the aliases are what may be printed (VISA, MASTERCARD for card). Up to 60 values.

confidence_thresholdnumber, 0 to 1

Below it, the field's status is needs_review. The default is 0.7. See Confidence and verification.

verificationstring

When VERIFIED mode re-reads the field: never, when_uncertain (the default) or always.

fallbackstring

What happens when no value is found. null (the default) leaves it null. default uses default, with status default and confidence 0. verifier marks the field verification_recommended outside VERIFIED mode. error marks it field_unreadable, which makes the result need review even when the field is optional.

defaultany

The value for fallback: "default". It must fit the field's type.

fieldsarray

For object fields: the sub-fields. Rules reach them with dots, totals.tax.

items, max_itemsobject, integer

For array fields: the definition of one item (an object with its own fields, and no key), and the most items to return, 1 to 500 (default 100). An array is read as a table of line items: give it the line_items semantic, or item fields for a description and an amount (by the line_item_description and line_item_amount semantics, or keys such as description and amount). Other lists are not read, and a required one carries the issue array_needs_verification. Arrays of arrays are not supported.

A definition holds at most 120 fields counting nested ones, and objects and arrays nest at most three levels deep.

How a field is found#

For each field, in order, ReceViz:

  1. gathers candidates: the tuned rule for its semantic, if it has one; matches on its aliases; and, for a field with no semantic, matches on its own label and key, values that could only be its type, and tokens that fully match its validation pattern;
  2. merges candidates that agree on the value, so their evidence adds up;
  3. chooses the best value that can become the field's type; the others are kept as alternatives, and the runner-up sets how ambiguous the field is;
  4. records the value, the printed text, its position, its source and its confidence.

A label is found the way a person finds it: the value is what follows the label on the same printed row, or, when the row holds only the label, the start of the row under it. An array of line items is read as a table.

Semantics#

Semantics
SemanticThe fact it readsField types
total_amountTotal amountcurrency, decimal
subtotalSubtotal, the amount before taxcurrency, decimal
tax_amountTax or VAT amountcurrency, decimal
currencyCurrency codestring, enum
document_dateDocument or transaction datedate, datetime, string
document_timeTransaction timestring
document_numberReceipt or invoice numberstring
trace_numberTrace number (STAN)string
rrnRetrieval reference numberstring
auth_codeAuthorisation codestring
terminal_idTerminal IDstring
merchant_idMerchant IDstring
batch_numberBatch numberstring
card_schemeCard schemestring, enum
card_last_fourLast four digits of the cardstring
payment_methodHow it was paidenum, string
approvedApproved or declinedboolean
merchant_nameMerchant or supplier namestring
merchant_addressMerchant or supplier addressstring
merchant_phoneMerchant or supplier phonestring
merchant_categoryKind of business, inferred rather than readstring, enum
tax_registration_numberTax registration number (TRN, VAT number)string
emailEmail addressstring
phonePhone numberstring
due_dateDue datedate, string
line_itemsLine itemsarray
line_item_descriptionLine item descriptionstring
line_item_quantityLine item quantitydecimal, integer
line_item_unit_priceLine item unit pricecurrency, decimal
line_item_amountLine item amountcurrency, decimal
line_item_skuLine item SKU or product codestring
line_item_typeLine item type: product, fee or discountenum, string

Validation#

validation checks a value once it is read, in STANDARD and VERIFIED modes. A value that fails a check keeps its value, gets the status invalid with an issue saying why, and its confidence is capped at 0.35.

patternregular expression

The value must match it in full (pattern_mismatch). Up to 200 characters; nested quantifiers such as (a+)+ are refused because they can take forever to match. On a number or a date, the pattern is checked against the value's canonical text, such as 150.00 or 2026-09-26.

min, maxnumber

For numbers and money (below_minimum, above_maximum).

min_length, max_lengthinteger

For strings (too_short, too_long).

not_futureboolean

For dates: not after the reference date (date_in_future). The reference date is today (UTC), or options.reference_date.

max_age_daysinteger

For dates: at most this many days before the reference date (date_too_old).

Rules#

rules check fields against each other, in STANDARD and VERIFIED modes. A failed rule never changes a value: it adds a rule_failed issue to the fields involved, sets them to needs_review, and appears in validation.rules with its message. Each rule may carry a message of your own. Up to 30 rules.

sum

{"type": "sum", "fields": ["subtotal", "tax"], "equals": "total", "tolerance": 0.02}. All numeric fields; at least two parts. allow_extras (default true) skips the rule when the document shows other charges or discounts that explain a gap.

compare

{"type": "compare", "left": "due_date", "op": ">=", "right": "invoice_date"}. Two numeric or two date fields; op is one of < <= == >= > !=.

required_one_of

{"type": "required_one_of", "fields": ["receipt_number", "rrn"]}. At least one of two or more fields must be found.

tax_rate

{"type": "tax_rate", "base": "subtotal", "tax": "tax", "rates": [5], "tolerance": 0.02}. The tax must be one of the rates (percent, above 0 and at most 50) of the base.

A rule is skipped when a value it needs is missing. Paths use dots for nested fields. Whenever a definition reads a total, ReceViz also checks the document's own arithmetic (base plus tax equals the total) without being asked; see the checks that run first.

Settings#

date_orderstring

DMY (the default) or MDY: how to read a date such as 03/04/2026. A request can override it with options.date_order.

default_currencystring

A three-letter currency code to assume when a document prints none: a field with the currency semantic that was not found takes it, marked as a default rather than read (status default, confidence 0). The application's setting and a request's options.default_currency come first.

capture.nounstring

Top level, beside settings: what the SDK scanners call this document in their guidance (“Point at an invoice”), up to 40 letters. Leave it out and ReceViz works it out from the key, then the name: payment_receipt is a receipt, bill_of_lading a bill of lading, emirates_id an ID card, anything unknown a document. GET /v1/document-types returns the noun in use as capture.noun.

A full example#

An invoice type with nested objects, line items, an enum with a default, and four rules. It passes the definition validator as it stands.

json
{
  "fields": [
    {
      "key": "invoice_number",
      "label": "Invoice number",
      "type": "string",
      "required": true,
      "semantic": "document_number",
      "aliases": [
        "INVOICE NO",
        "INV #",
        "BILL NO"
      ],
      "validation": {
        "pattern": "^[A-Z0-9-]{3,20}$"
      },
      "confidence_threshold": 0.85
    },
    {
      "key": "invoice_date",
      "label": "Invoice date",
      "type": "date",
      "required": true,
      "semantic": "document_date",
      "validation": {
        "not_future": true,
        "max_age_days": 365
      }
    },
    {
      "key": "due_date",
      "label": "Due date",
      "type": "date",
      "semantic": "due_date"
    },
    {
      "key": "supplier",
      "label": "Supplier",
      "type": "object",
      "required": true,
      "fields": [
        {
          "key": "name",
          "label": "Name",
          "type": "string",
          "semantic": "merchant_name"
        },
        {
          "key": "trn",
          "label": "TRN",
          "type": "string",
          "semantic": "tax_registration_number",
          "validation": {
            "pattern": "^[0-9]{15}$"
          }
        }
      ]
    },
    {
      "key": "po_number",
      "label": "Purchase order",
      "type": "string",
      "aliases": [
        "PO NO",
        "P.O. NUMBER"
      ],
      "verification": "never"
    },
    {
      "key": "payment_terms",
      "label": "Payment terms",
      "type": "enum",
      "enum": [
        {
          "value": "net_30",
          "label": "Net 30",
          "aliases": [
            "NET 30",
            "30 DAYS"
          ]
        },
        {
          "value": "on_receipt",
          "label": "Due on receipt",
          "aliases": [
            "DUE ON RECEIPT",
            "IMMEDIATE"
          ]
        }
      ],
      "fallback": "default",
      "default": "net_30"
    },
    {
      "key": "totals",
      "label": "Totals",
      "type": "object",
      "fields": [
        {
          "key": "subtotal",
          "label": "Subtotal",
          "type": "currency",
          "semantic": "subtotal"
        },
        {
          "key": "tax",
          "label": "VAT",
          "type": "currency",
          "semantic": "tax_amount",
          "exclude_aliases": [
            "TAX INVOICE"
          ]
        },
        {
          "key": "total",
          "label": "Total",
          "type": "currency",
          "semantic": "total_amount",
          "validation": {
            "min": 0
          },
          "verification": "always"
        }
      ]
    },
    {
      "key": "lines",
      "label": "Line items",
      "type": "array",
      "semantic": "line_items",
      "max_items": 200,
      "items": {
        "type": "object",
        "fields": [
          {
            "key": "description",
            "type": "string",
            "semantic": "line_item_description"
          },
          {
            "key": "quantity",
            "type": "decimal",
            "semantic": "line_item_quantity"
          },
          {
            "key": "amount",
            "type": "currency",
            "semantic": "line_item_amount"
          }
        ]
      }
    }
  ],
  "rules": [
    {
      "type": "sum",
      "fields": [
        "totals.subtotal",
        "totals.tax"
      ],
      "equals": "totals.total",
      "tolerance": 0.02
    },
    {
      "type": "tax_rate",
      "base": "totals.subtotal",
      "tax": "totals.tax",
      "rates": [
        5
      ]
    },
    {
      "type": "compare",
      "left": "due_date",
      "op": ">=",
      "right": "invoice_date",
      "message": "The due date is before the invoice date."
    },
    {
      "type": "required_one_of",
      "fields": [
        "invoice_number",
        "po_number"
      ]
    }
  ],
  "settings": {
    "date_order": "DMY"
  }
}

The validator is strict about structure and forgiving about wording: it returns every problem at once, each with a path, so an invalid definition answers 400 invalid_definition with details.problems such as {"path": "fields[2].type", "message": "…"}.

The shape of data#

Every published version carries the JSON Schema of data for that definition: what your code reads. Every field is present, and null when it was not found. This is the schema of the payment_receipt template:

json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "amount": {
      "type": [
        "number",
        "null"
      ]
    },
    "currency": {
      "type": [
        "string",
        "null"
      ]
    },
    "transaction_date": {
      "type": [
        "string",
        "null"
      ],
      "format": "date"
    },
    "transaction_time": {
      "type": [
        "string",
        "null"
      ]
    },
    "receipt_number": {
      "type": [
        "string",
        "null"
      ]
    },
    "rrn": {
      "type": [
        "string",
        "null"
      ]
    },
    "auth_code": {
      "type": [
        "string",
        "null"
      ]
    },
    "terminal_id": {
      "type": [
        "string",
        "null"
      ]
    },
    "payment_method": {
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "card",
        "cash",
        "bank_transfer",
        "cheque",
        "wallet",
        "other",
        null
      ]
    },
    "card_scheme": {
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "visa",
        "mastercard",
        "amex",
        "maestro",
        "discover",
        "diners",
        "jcb",
        "unionpay",
        "mada",
        "rupay",
        null
      ]
    },
    "card_last_four": {
      "type": [
        "string",
        "null"
      ]
    },
    "approved": {
      "type": [
        "boolean",
        "null"
      ]
    },
    "merchant": {
      "type": [
        "string",
        "null"
      ]
    },
    "merchant_address": {
      "type": [
        "string",
        "null"
      ]
    }
  },
  "required": [
    "amount",
    "currency",
    "transaction_date",
    "transaction_time",
    "receipt_number",
    "rrn",
    "auth_code",
    "terminal_id",
    "payment_method",
    "card_scheme",
    "card_last_four",
    "approved",
    "merchant",
    "merchant_address"
  ],
  "additionalProperties": false
}

Read it from GET /v1/document-types/{key} (schema.json_schema), GET /v1/document-types/{key}/versions/{version} or GET /v1/schemas/{schema_id}. All three need schemas.read.

Drafts, versions and breaking changes#

Editing a document type writes its draft. Publishing validates the draft, compares it with the latest version, and adds a new version that never changes afterwards. Nothing you save is published until someone publishes it; in the console, drafts can be edited by developers and published by admins and owners.

What counts as a breaking change
BreakingNot breaking
A field removedAn optional field added
A field's type changedA required field made optional
An optional field made required, or a new required fieldLabels, descriptions, aliases, position or semantic changed
An enum value removedAn enum value added
An array's item type changedValidation, thresholds, verification, fallback or default changed
Rules or settings changed

A new required field is breaking on purpose: documents that used to come back complete would start to need review. Publishing a breaking draft has to be confirmed deliberately; without that, it answers 409 breaking_change with the breaking changes in details.changes. A draft that is the same as the latest version answers 409 nothing_changed.

Which version a request uses#

An extraction uses, in this order:

  1. the schema_version sent with the request;
  2. the version the application is pinned to for that document type;
  3. the latest published version.

Pin a version per document type on an application in the console to keep an integration on a schema while you publish new ones. Every result says which it used, as schema_version and schema_id.

Document types and applications#

In the console, an application can be given the list of document types it may use, one of them as its default. Then:

  • a request for any other type answers 403 document_type_not_assigned;
  • a request without document_type uses the default, or the only assigned type; otherwise it answers 400 document_type_required.

An application with no list may use every active document type of the organization, and every request must name one. Using one of your own document types, rather than one created from a template, needs the schemas.execute capability. Archiving a document type stops it being used for new extractions.

Describe a document in words#

In the console you can describe a document in a sentence or two, for example: “Extract the receipt number, transaction date, total amount and last four digits of the card. Receipt number may appear as RRN, REF NO, Receipt No or Reference.” ReceViz answers with a proposed definition, built by a language model when your organization's data policy allows one, or otherwise from a vocabulary of the facts documents carry. Either way the proposal is run through the definition validator, and says which builder made it.

A proposal, never a change

Nothing is saved or published by describing a document. You review the proposal, edit it, save it as a draft, and publish it as a separate, deliberate step. Your description is treated only as a description; the builder sees no documents.

An organization can ask for up to 30 proposals an hour.

Create a document type through the API#

POST /v1/document-types creates one from your server. It needs a secret key with schemas.write (an opt-in capability), and a custom definition also needs schemas.execute.

keystringrequired

2 to 64 characters of a-z, 0-9 and _, starting with a letter. 409 key_taken if the organization already has it.

namestring

Up to 120 characters.

descriptionstring

Up to 1000 characters.

templatestring

payment_receipt, retail_receipt or invoice: start from a built-in.

definitionobject

Your own definition, as above.

publishboolean

Publish version 1 at once. The default is true.

RECEVIZ_API="https://accounthouse-backend-793493499887.me-central1.run.app/api/receviz/v1"

# From a built-in template, published as version 1.
curl "$RECEVIZ_API/document-types" \
  -H "Authorization: Bearer $RECEVIZ_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key": "shop_receipt", "name": "Shop receipt", "template": "retail_receipt"}'

The answer is 201 with the document type and its current schema. If the calling application has a list of document types, the new one is added to it. An organization has at most 50 active document types unless ReceViz set another limit.