Sign in

The document schema

View as Markdown

GET /public/v1/files/{file_id}/document and GET /public/v1/documents/{document_id} answer a document as ocost.document.v1, in the terms of EN 16931 (the e-invoice norm behind XRechnung, ZUGFeRD and Peppol UBL). Each value names its business term (BT-…) or group (BG-…), so it maps to UBL or CII.

The shape is stable: a breaking change comes as ocost.document.v2 under /public/v2, with v1 served 12 months more.

Conventions

  • Amounts and quantities: decimal strings as printed ("1270.92", "2.5"), never numbers. Use a decimal type.
  • Rates: percent strings ("19", "7", "0", "5.5").
  • Dates ISO 8601 (2026-11-03), currencies ISO 4217 (EUR), countries ISO 3166-1 alpha-2 (DE).
  • Units: UNECE Recommendation 20 (C62 one, H87 piece, HUR hour, KGM kilogram) and Recommendation 21 packages with their X (XCT carton, XPF pallet). Unknown is null, never C62.
  • Absent is null: not printed, or dropped because Ocost wasn't sure. Ocost never guesses.

The fields

FieldEN 16931Meaning
schemaAlways "ocost.document.v1".
id, file_idThe document and its file.
pagesIts pages in the file, from 1.
kindinvoice (every invoice to pay: partial, final, advance, collective, recurring; type_code says which), credit_note, invoice_correction, receipt, pro_forma, delivery_note, order, order_confirmation, quote, contract (leases, policies, agreements), statement (a factor's), payment_reminder, other. kind_detail is the exact kind, for information.
directionincoming (a supplier's bill), outgoing (your business issued it) or unsure. Worked out from the VAT ID, tax number and name in Settings › Company: keep them right.
type_codeBT-3UNCL 1001: 380 invoice, 381 credit note, 384 corrected, 326 partial, 386 advance payment, 389 self-billed, 325 pro forma.
number, issue_date, due_dateBT-1, BT-2, BT-9Number, issue date, due date.
delivery_dateBT-72Delivery date.
invoicing_period_start, invoicing_period_endBT-73, BT-74The period invoiced.
valid_untilA quote's.
currencyBT-5Currency.
buyer_referenceBT-10The buyer's reference.
order_reference, order_referencesBT-13The order, and every order it names.
preceding_invoiceBT-25What a credit note or correction refers to.
seller, buyerBG-4, BG-7Each: name (BT-27, BT-44), vat_id (BT-31, BT-48), tax_registration_id (BT-32, the Steuernummer), legal_registration_id (BT-30, BT-47, the register entry), address (street, postal_code, city, country), contact (email, phone), website.
payeeBG-10Who is paid when it isn't the seller (a factor).
deliver_toBG-15The delivery address.
other_parties[]Every other party named, each with its role (invoicee, service_recipient, payer, ship_to, carrier, debtor, landlord, tenant, contractor, …) and printed_role (as printed).
paymentBG-16means_code (BT-81, UNCL 4461: 58 SEPA credit transfer, 59 SEPA direct debit, 48 card), means (as a word, or already_paid), iban (BT-84), bic (BT-86), terms (BT-20 as printed), net_days (days to pay in full from the issue date), paid_on (the day it says it was paid).
early_payment_discounts[]BT-20Skonto (XRechnung's #SKONTO#): days, percent, base_amount (null: off the amount due), due_date (issue date plus days).
totalsBG-22net (BT-109), tax (BT-110), gross (BT-112), payable (BT-115, left to pay; null when it is the gross), prices_include_vat (as on a receipt).
vat_breakdown[]BG-23category (BT-118, UNCL 5305: S standard, Z zero, E exempt, AE reverse charge, K intra-EU, G export, O outside VAT), rate (BT-119), taxable_amount (BT-116), tax_amount (BT-117), exemption_reason (BT-120).
lines[]BG-25id (BT-126, its position), name (BT-153), seller_item_id (BT-155, article number), standard_item_id (BT-157, GTIN), quantity (BT-129), unit (BT-130), price (BT-146, net), base_quantity (BT-149, units price is for; null: one), net_amount (BT-131), discount_percent, vat_category (BT-151), vat_rate (BT-152); where printed, ordered_quantity, backordered_quantity, batch, pzn, attributes (colour, size, material, brand, …).
deductions[]The earlier invoices a final invoice takes off: number, issue_date, net, tax, gross.
cardFor a document without fields of its own (a test report, a letter): title, summary, facts (label, kind, date, amount, currency, number, text, page), deadlines first.
worked_out[]An amount Ocost worked out because the one read can't be right: field, printed, rule.
sourcesPer value, by path (number, totals.gross, seller.vat_id, …): page, quote (the words read), confidence, and why for a doubt.
checks[]Each check Ocost ran in code: name, passed, sums (one of the sums), fields (paths), at (the rate or line).
tax_checkAn incoming invoice against § 14 UStG: case (standard, small, reverse_charge, intra_eu, exempt) and missing[] (item, law, at_stake: missing, it costs the input VAT deduction). Null for kinds it doesn't check.
attention[]What a person should look at before booking: sums (a sum failed), outgoing, possible_duplicate, self_billed.
possible_duplicate_ofAn earlier document of the same kind, number and seller, else null.
read_withe_invoice (from its XML, exactly) or reader (from its pages).

confidence in sources:

  • checked: a check confirmed it, or it came from an e-invoice.
  • read: nothing speaks against it.
  • doubt: a check failed (why names it), or its words weren't found on its page.

The sums in checks are total, vat, vat_net, vat_tax, gross, payable, signs, skonto, deduction, deductions and quantity. Fill a bill without a person's look only when every check with sums: true passed.

An example

An incoming invoice with two lines at 19 %, 2 % Skonto within 14 days:

JSON
{
  "schema": "ocost.document.v1",
  "id": "01a1b08c-7aa1-7b52-9d4e-2f1f1c0c4f11",
  "file_id": "01a1b08c-6e3a-7562-8f4a-7924722667b1",
  "pages": [1],
  "kind": "invoice",
  "kind_detail": "invoice",
  "direction": "incoming",
  "type_code": "380",
  "number": "RE-2026-1187",
  "issue_date": "2026-11-03",
  "due_date": "2026-12-03",
  "delivery_date": "2026-10-30",
  "invoicing_period_start": null,
  "invoicing_period_end": null,
  "valid_until": null,
  "currency": "EUR",
  "buyer_reference": null,
  "order_reference": "PO-2316",
  "order_references": ["PO-2316"],
  "preceding_invoice": null,
  "seller": {
    "name": "Musterbüro Lieferant GmbH",
    "vat_id": "DE123456789",
    "tax_registration_id": null,
    "legal_registration_id": "HRB 12345",
    "address": { "street": "Beispielweg 4", "postal_code": "50667", "city": "Köln", "country": "DE" },
    "contact": { "email": "rechnung@lieferant.example", "phone": null },
    "website": null
  },
  "buyer": {
    "name": "Beispiel Handel GmbH",
    "vat_id": "DE987654321",
    "tax_registration_id": null,
    "legal_registration_id": null,
    "address": { "street": "Hauptstraße 1", "postal_code": "40213", "city": "Düsseldorf", "country": "DE" },
    "contact": null,
    "website": null
  },
  "payee": null,
  "deliver_to": null,
  "other_parties": [],
  "payment": {
    "means_code": "58",
    "means": "transfer",
    "iban": "DE02120300000000202051",
    "bic": null,
    "terms": "14 Tage 2 % Skonto, 30 Tage netto",
    "net_days": 30,
    "paid_on": null
  },
  "early_payment_discounts": [
    { "days": 14, "percent": "2", "base_amount": null, "due_date": "2026-11-17" }
  ],
  "totals": { "net": "1068.00", "tax": "202.92", "gross": "1270.92", "payable": null, "prices_include_vat": false },
  "vat_breakdown": [
    { "category": "S", "rate": "19", "taxable_amount": "1068.00", "tax_amount": "202.92", "exemption_reason": null }
  ],
  "lines": [
    {
      "id": "1", "name": "Bürostuhl ErgoPlus schwarz", "seller_item_id": "ES-200", "standard_item_id": null,
      "quantity": "4", "unit": "H87", "price": "189.00", "base_quantity": null, "net_amount": "756.00",
      "discount_percent": null, "vat_category": "S", "vat_rate": "19",
      "ordered_quantity": null, "backordered_quantity": null, "batch": null, "pzn": null,
      "attributes": { "colour": "schwarz" }
    },
    {
      "id": "2", "name": "Montage", "seller_item_id": null, "standard_item_id": null,
      "quantity": "4", "unit": "HUR", "price": "78.00", "base_quantity": null, "net_amount": "312.00",
      "discount_percent": null, "vat_category": "S", "vat_rate": "19",
      "ordered_quantity": null, "backordered_quantity": null, "batch": null, "pzn": null,
      "attributes": {}
    }
  ],
  "deductions": [],
  "card": null,
  "worked_out": [],
  "sources": {
    "number": { "page": 1, "quote": "RE-2026-1187", "confidence": "read", "why": null },
    "totals.gross": { "page": 1, "quote": "1.270,92 €", "confidence": "checked", "why": null },
    "seller.vat_id": { "page": 1, "quote": "DE123456789", "confidence": "checked", "why": null }
  },
  "checks": [
    { "name": "vat", "passed": true, "sums": true, "fields": ["totals.net", "totals.tax"], "at": "19" },
    { "name": "gross", "passed": true, "sums": true, "fields": ["totals.net", "totals.tax", "totals.gross"], "at": null }
  ],
  "tax_check": { "case": "standard", "missing": [] },
  "attention": [],
  "possible_duplicate_of": null,
  "read_with": "reader"
}

Reading it

from decimal import Decimal

document = session.get(f"{API}/files/{file_id}/document", timeout=15).json()
sums_hold = all(c["passed"] for c in document["checks"] if c["sums"])
gross = Decimal(document["totals"]["gross"]) if document["totals"]["gross"] else None
doubts = [path for path, s in document["sources"].items() if s["confidence"] == "doubt"]