Extracting Multi-Currency Line Items From an Invoice
10 min read · updated August 11, 2026
The dangerous failure in multi-currency extraction is not a missing value. It is a model that reads “$1,200.00” and “€1.100,00” on the same page, decides the document has one currency, and returns two numbers that now silently mean the same unit.
Two currencies on one lawful invoice
It is tempting to treat a second currency as a data-quality problem. It is not: the law requires it in a common case. Article 230 of Council Directive 2006/112/EC states that “the amounts which appear on the invoice may be expressed in any currency, provided that the amount of VAT payable or to be adjusted is expressed in the national currency” of the member state concerned. The UK applies the same idea in its own guidance: an invoice denominated in a foreign currency must still show the sterling equivalent of the VAT.
So a perfectly ordinary invoice reads USD throughout the line items and the net total, and then states the VAT amount in euros or sterling, with a conversion rate printed alongside. Article 91 of the same directive sets the rate to use — broadly, the rate recorded at the time the tax becomes chargeable on the most representative exchange market of the member state, with an ECB-rate option. A schema with one top-level currency string cannot represent this document at all, and the extractor will resolve the conflict by discarding information.
Other genuine multi-currency shapes: a dual-priced invoice showing both local and reporting currency for every line; a group invoice with lines from two subsidiaries; and a freight invoice with duties in the currency of the importing country and carriage in the carrier’s.
Symbols are ambiguous, codes are not
A currency symbol is not an identifier. $ is used for the US, Canadian, Australian, New Zealand, Singapore, Hong Kong and Mexican dollars among others; kr covers the Swedish, Norwegian, Danish and Icelandic kronor; ¥ is written for both the Japanese yen and the Chinese yuan. A model that sees $ and emits USD is guessing, and it will guess consistently and wrongly for an entire supplier.
The evidence that disambiguates is usually elsewhere on the page: the ISO 4217 alpha-3 code printed near the total, the supplier’s country and tax identifier, the IBAN’s country prefix, and the number formatting itself. Ask for the code and the evidence, not the symbol:
"currency": {
"code": "CAD", // ISO 4217 alpha-3
"as_printed": "$",
"evidence": "\"Total CAD\" in the summary block; supplier address Ontario"
}When no alpha-3 code appears anywhere, the honest output is the symbol plus a null code and a review flag — handling a missing required field rather than filling it. A pipeline that always produces a three-letter code has taught itself to be confidently wrong on the documents that matter most.
Minor units, and why money is an integer
ISO 4217 is maintained by SIX Financial Information as the standard body for currency codes, and the published list carries a minor unit — the number of decimal places — per currency alongside the alpha-3 and numeric codes. It is not two everywhere. The Japanese yen has none; several Gulf and North African currencies have three. An amount of 1.234 in a three-decimal currency is one thousand two hundred and thirty-four minor units, and truncating it to two decimals is a real financial error, not a rounding preference.
Two consequences for extraction. First, store money as an integer count of minor units plus the code, never as a float: 0.1 + 0.2 in IEEE-754 binary floating point is not 0.3, and an invoice with forty lines will fail its own total check for that reason alone. Second, the number of decimals you parse must come from the currency, not from the document — a yen invoice printing 1,200 is 1200 yen, and a pipeline that assumes two decimals and stores 120000 minor units is out by a factor of a hundred.
Decimal separators are the other half of this. Continental European invoices write 1.234,56 for what a US invoice writes 1,234.56; Swiss invoices use an apostrophe as a thousands separator; some systems print a space. The rule that actually works is positional: whichever of . or , appears last and is followed by exactly the currency’s minor-unit count of digits is the decimal separator, and everything else is grouping. Apply it after you know the currency, not before.
A schema that pins currency per amount
The fix is structural. Make currency a property of every amount rather than of the document, so that a single-currency invoice is simply the case where they all agree:
{
"document_currency": "USD",
"lines": [
{ "description": "Bearing housing, 40mm",
"quantity": 120,
"unit_price": { "minor": 1450, "currency": "USD", "as_printed": "$14.50" },
"line_net": { "minor": 174000,"currency": "USD", "as_printed": "$1,740.00" } }
],
"total_net": { "minor": 174000, "currency": "USD" },
"total_vat": { "minor": 33060, "currency": "EUR", "as_printed": "€330,60" },
"vat_rate_used": { "from": "USD", "to": "EUR", "rate": "0.92", "printed": true }
}Notice that total_vat is in a different currency from everything else and the schema does not complain. Notice also as_printed: keeping the literal string next to the parsed value is what makes a separator or minor-unit mistake findable later, and it costs a handful of output tokens per amount. Structured-output modes will happily enforce this shape — see structured output support across providers for how strictly each one holds a nested schema.
Never let the model convert
The single instruction that removes most of the damage is: extract what is printed, convert nothing. A language model asked for “the total in USD” on a euro invoice will produce a plausible USD figure using a rate from nowhere in particular. It will not say it did that. The number will be within a few per cent of correct, which is the worst possible error size — too small to notice, too large to ignore.
Conversion belongs downstream, with an explicit rate, an explicit rate date, and a record of both. If the invoice prints its own rate, extract it as a string and treat it as evidence to reconcile against, not as an instruction to apply. And check the direction: a printed rate of 0.92 could be EUR per USD or USD per EUR, and the only reliable way to tell is to multiply and see which result matches the printed VAT amount. That is another arithmetic check the document performs on itself, in the same family as reconciling a total against its line items.