EU VAT determination for a sales ledger, 200 lines per call

One VAT determination per line — place of supply, mechanism, rate, compulsory mentions — for up to 200 transactions in a single paid call, each sourced article by article.

By Matthias Begot · · Updated

VAT is decided transaction by transaction, but it is worked on ledger by ledger. An agent closing a month of sales, re-judging an order book, or checking a supplier journal before it is booked needs one determination per line — who charges the tax, under which mechanism, where it is taxed, at which rate, and what the invoice must say. One paid call — POST /invoice/vat-treatment/batch — judges up to 200 transactions under Directive 2006/112/EC, each sourced article by article, settled as one x402 payment for the whole call.

The problem: an EN 16931 validator judges the document, not the transaction

An invoice can satisfy every normative BR-* rule of EN 16931 and still be refused, because the rules that make it wrong are not in the document schema. An intra-EU B2B dispatch invoiced without its reverse-charge mention is structurally perfect and substantively broken. That is the split between the two endpoints: /invoice/validate judges the document, /invoice/vat-treatment judges the transaction.

The judgement is also line-local. Two rows of the same ledger, same supplier, same month, differ on one declared field — a customer VAT number known valid versus unknown — and land on opposite mechanisms: exempt with the customer self-accounting, or French VAT charged at the standard rate. There is no ledger-level verdict to compute; there are N verdicts to collect.

Why this is a 2026 problem and not a 2030 one

The regulatory clock is what turns this from a spreadsheet chore into an agent’s job.

  • France: from 1 September 2026, every VAT-registered business established in France must be able to receive electronic invoices, and large and intermediate-sized enterprises must issue them; micro, small and medium enterprises follow by 1 September 2027 (impots.gouv.fr).
  • European Union: the VAT in the Digital Age package — Council Directive (EU) 2025/516 of 11 March 2025, in force since 14 April 2025 — makes e-invoicing the default and imposes digital reporting of cross-border B2B supplies from 1 July 2030, with domestic reporting systems to be aligned by 1 January 2035 (European Commission).

Structured invoices in, structured reporting out. What stops being tolerable in that pipeline is a mechanism guessed by a template.

The call: one array, up to 200 transactions

The body carries a single field, items, holding 1 to 200 entries. Each entry is exactly the body of the single call — same required fields, same closed enumerations, same optional customer_vat_status, amount and currency.

POST /invoice/vat-treatment/batch
Content-Type: application/json
{
  "items": [
    { "supplier_country": "FR", "customer_country": "IT", "transaction_type": "services", "customer_type": "business", "customer_vat_status": "valid", "supply_kind": "services_b2b_general", "date": "2026-07-15", "amount": "3200.00", "currency": "EUR" },
    { "supplier_country": "FR", "customer_country": "ES", "transaction_type": "goods", "customer_type": "business", "customer_vat_status": "unknown", "supply_kind": "intra_eu_dispatch", "date": "2026-07-15", "amount": "4000.00", "currency": "EUR" },
    { "supplier_country": "FR", "customer_country": "CH", "transaction_type": "goods", "customer_type": "business", "supply_kind": "export_outside_eu", "date": "2026-07-15", "amount": "9000.00", "currency": "EUR" }
  ]
}

supply_kind is never inferred: it is one of eight declared values (domestic, intra-EU dispatch, export, import, B2B services, B2C services, land-related services, electronic B2C services). The date of each line selects the rate in force on that line — a July 2019 row is rated with the 2019 rate, not today’s.

What comes back: one verdict per line, one snapshot for the ledger

The response is the standard UnifiedResponse. Four properties make it usable as a month-close primitive:

  • results[] is in input order, each element carrying its own index, so verdicts re-pair with ledger rows without bookkeeping.
  • Each verdict is the unit determination field for field: rule_id, mechanism, liable, place_of_supply with its cited article, rate, required_invoice_mentions, plus computed when the line carried an amount and rationale when there is something to explain.
  • shared deduplicates the legal work. Over 200 lines mixing five mechanisms, the distinct sets of legal bases and caveats can be counted on one hand; they are served once, and each result points at its set through legal_basis_ref and caveats_ref.
  • One provenance covers the whole calldirective-2006-112 + tedb, with freshness.as_of dating the committed rate snapshot. Every line of the ledger was judged against the same stated state of the rates.

Here is the three-line ledger above, abridged from a real response:

{
  "data": {
    "count": 3,
    "results": [
      { "index": 0, "status": "ok", "legal_basis_ref": 0, "caveats_ref": 0,
        "verdict": { "rule_id": "INV-VAT-S02", "mechanism": "reverse_charge", "liable": "customer",
          "place_of_supply": { "country": "IT", "basis": { "celex": "32006L0112", "article": "44", "title": "Place of supply of services to a taxable person: where the customer is established", "query": "/legal/eu-act?celex=32006L0112&article=44" } },
          "rate": { "country": "IT", "kind": "standard", "percentage": "22" },
          "required_invoice_mentions": [ { "text": "Reverse charge", "language": "en" }, { "text": "Autoliquidation", "language": "fr", "national_basis": { "jurisdiction": "FR", "reference": "CGI, art. 283-2 (mention obligatoire, art. 242 nonies A ann. II)" } } ],
          "computed": { "taxable_amount": "3200.00", "rate_percentage": "0", "vat_amount": "0.00", "total_amount": "3200.00" } } },
      { "index": 1, "status": "ok", "legal_basis_ref": 1, "caveats_ref": 1,
        "verdict": { "rule_id": "INV-VAT-D02", "mechanism": "standard", "liable": "supplier",
          "place_of_supply": { "country": "FR" },
          "rate": { "country": "FR", "kind": "standard", "percentage": "20" },
          "rationale": "A VAT identification number of the customer, issued by another Member State and valid at the time of the supply, is a substantive condition of the exemption (Art. 138(1)(b)). It is declared as invalid or unknown here, so the exemption is not granted and the supply is taxed in the Member State of dispatch.",
          "computed": { "taxable_amount": "4000.00", "rate_percentage": "20", "vat_amount": "800.00", "total_amount": "4800.00" } } },
      { "index": 2, "status": "ok", "legal_basis_ref": 2, "caveats_ref": 2,
        "verdict": { "rule_id": "INV-VAT-X01", "mechanism": "exempt_with_credit", "liable": "supplier" } }
    ],
    "summary": { "items": 3, "ok": 3, "errors": 0,
      "mechanisms": { "standard": 1, "reverse_charge": 1, "exempt_with_credit": 1, "out_of_scope": 0, "undetermined": 0 } }
  },
  "provenance": { "source": "directive-2006-112 + tedb", "freshness": { "kind": "snapshot", "as_of": "2026-07-27T00:00:00Z" } }
}

Two rates are in play on line 0 and they say different things: rate.percentage is what the Italian customer self-assesses, computed.rate_percentage is what the French supplier charges — zero. An agent filling the invoice reads computed; an agent booking the acquisition reads rate.

summary is the ledger-level read

summary.mechanisms carries all five keys, zeros included — an absent key would read as an omission rather than a count of zero. It is what an agent scans first: how many lines carry a compulsory mention, how many left the Union’s territorial scope, and how many came back undetermined and therefore need a human or a different source.

The x402 golden rule, applied to a ledger

The agent pays for the answer to its question, and “197 lines judged, 2 need your VAT numbers verified, 1 row is malformed” is a successful answer. A well-formed batch returns 200 whatever the mix inside it:

  • A malformed item does not sink the call. It comes back at its position with status: "error" and the exact code / message / issues[] a single call would have produced — an unknown supply_kind, for instance, is refused by name with the admitted values listed back.
  • An undetermined line is ok, not an error. It is a judged line whose judgement is a motivated refusal, counted as such in summary. An uncovered regime answered with a plausible default would be worse than no answer.
  • Only the envelope can fail: an unreadable body, an absent or empty items, or more than 200 transactions — a 400 invalid_batch_size checked before any judging, and not billed.

The batch settles as one x402 payment and is priced per transaction by the gateway, so the cost tracks the size of the ledger rather than the number of round-trips. The authoritative price and the accepted rails are served live by the catalog — never hardcoded here. The payment loop is the usual discover → 402 → pay → replay cycle; the Quickstart walks it with runnable snippets and For agents covers the discovery surfaces.

A concrete shape: the month-close loop

  1. Project the sales journal onto the item shape: two countries, goods or services, business or consumer, the declared supply_kind, the chargeability date, and the amount when you want VAT computed.
  2. Judge the ledger: one POST /invoice/vat-treatment/batch, one 402, one signature, one replay, up to 200 lines.
  3. Split on summary. Lines with mechanism: "undetermined" and lines with status: "error" leave the automated path; the rest carry their mentions.
  4. Verify what was taken as declared. Every customer_vat_check block whose status is not_checked points at GET /company/vat, the live VIES lookup — a valid customer VAT number is a substantive condition of the intra-EU exemption (Art. 138(1)(b)), so line 1 above flips from standard to reverse_charge only once the number checks out. Re-judge the corrected lines.
  5. Emit. Pass each determination to POST /invoice/generate through its vat_treatment field and the compulsory mention is written into the invoice for you.
  6. Log provenance.freshness.as_of next to the closed period — the audit answer to “judged against which rates?”.

Steps 4 and 5 are why the batch is a pre-emission gate rather than a report: the correction happens before the invoice exists, not after a tax authority reads it.

What this batch will not do

The scope is exactly the single call’s scope, and the six sentences that state it travel in data.limits with every answer:

  • OSS and IOSS, and the distance-selling thresholds of Art. 33 and 59c, are not applied — a line falling under them comes back undetermined.
  • Import VAT and customs procedures (Art. 60, 201) are not determined: the person liable is designated by the Member State of importation.
  • Margin schemes, investment gold, chain and triangular transactions beyond the two declared parties: not judged, no transport ascribed.
  • No product classification. The rate served is the standard rate of the place of supply; reduced rates come back as candidates only, and has_regional_rates flags States with territory-specific rates outside the snapshot.
  • The verdict rests on what the request declares. The runtime performs no registry or VIES lookup — that is /company/vat and /company/resolve.

It is a determination, not tax advice, and it files nothing: no VAT return, no recapitulative statement, no Intrastat. The articles it cites are readable at a date through GET /legal/eu-act, and the rates come from a committed extract of the Commission’s Taxes in Europe Database covering 2016 onwards — a date before it is refused, never extrapolated.

Used for what it is — one settled call that renders a sourced verdict on every line of a ledger against one dated snapshot — the batch makes VAT determination cheap enough to run before the invoices are cut. For the request contract, the field tables and the error codes, see the POST /invoice/vat-treatment/batch documentation.