Înapoi la Blog

Exchange Rates for Tax Reporting: A Developer's Guide to IRS, HMRC and EU VAT Conversion

V
Vlado Grigirov
August 20, 2026
Currency API Exchange Rates Historical Rates Tax Reporting VAT Accounting Finexly

Every multi-currency system eventually meets an accountant. They ask a question that sounds trivial and is not: "What rate did you use for this invoice?" If the honest answer is "whatever the API returned that afternoon, and we didn't keep it," you have a problem that no amount of clean code will fix at filing time.

Getting exchange rates for tax reporting right is less about picking the correct rate — most authorities are surprisingly relaxed about that — and much more about being able to prove, years later, which rate you used, where it came from, and that you have applied the same rule to every other transaction in the period. That is a data-modelling problem, and it is the part nobody writes about.

This guide covers what the IRS, HMRC and the EU VAT Directive actually require, the five conversion bugs that turn into restatements, and a rate-snapshot schema you can implement this week.

Nobody Agrees on "The" Exchange Rate — and That's the Point

The single most useful sentence in this entire area comes from the IRS itself:

"The Internal Revenue Service has no official exchange rate. Generally, it accepts any posted exchange rate that is used consistently."

Read that twice, because the same shape appears in almost every jurisdiction. The obligation is rarely use this specific number. It is use a defensible source, and use it consistently. Consistency is a property of your system, not of your rate provider. If your code silently falls back to a different source on weekends, you have broken the requirement without ever fetching a wrong number.

United States: Spot by Default, Yearly Average by Concession

The IRS baseline is the spot rate: "In general, use the exchange rate prevailing (i.e., the spot rate) when you receive, pay or accrue the item." Where income accrues steadily — salary, rent, ongoing business revenue — the IRS publishes a yearly average currency exchange rate table and instructs filers to "divide the foreign currency amount by the applicable yearly average exchange rate." As of that page's last update on 24 February 2026, the table covers tax years 2021 through 2025.

Note the direction of that operation. The IRS table is quoted as units of foreign currency per one US dollar, so you divide. Invert it by mistake and you are not slightly wrong — you are wrong by the square of the rate. On a yen figure, that is roughly four orders of magnitude. More on rate direction below, because it is the single most common integration bug in this domain.

For US federal agency reporting there is a second official series: the Treasury Reporting Rates of Exchange, published quarterly on FiscalData.Treasury.gov in CSV, JSON and XML. Treasury describes it as reflecting "exchange rates at which the U.S. government can acquire foreign currencies for official expenditures as reported by disbursing officers for each post on the last business day of the month prior to the date of the published report." If live rates move 10% or more away from a published rate, Treasury issues an amendment mid-quarter. The Fiscal Data API is open and requires no account or token — worth knowing if you need a government-sourced reference series to reconcile against.

United Kingdom: VAT Notice 700 §7.6 Has Force of Law

The UK is more prescriptive, and the relevant text carries statutory force under Schedule 6, paragraph 11 of the VAT Act 1994. VAT Notice 700 gives businesses three routes for converting foreign-currency supplies into sterling:

  1. The UK market selling rate at the time of the supply. This is the default. The Notice states that "the rates published in national newspapers will be acceptable as evidence of the rates at the relevant time."
  2. The HMRC period rate of exchange, published for customs purposes. You may adopt it "for all your supplies or for all supplies of a particular class or description." No advance notification is needed — but "having made such an option, you cannot then change it without first getting agreement by writing to the VAT Written Enquiries Team."
  3. A commercial rate or method of your own, which requires a written application. HMRC weighs whether the rate is "determined by reference to the UK currency market", whether it is "objectively verifiable", and how often it is updated. Crucially, "forward rates or methods deriving from forward rates are not acceptable" — a hard boundary worth understanding alongside the difference between spot and forward rates.

And the line that governs your caching layer: "Whatever rate or method you adopt, the appropriate rate for any supply is that current at the time of the supply." Time of supply, not time of invoicing, not time of payment, and certainly not time of your nightly batch job.

If you take route 2, the mechanics are pleasantly machine-friendly. HMRC publishes monthly rates on the penultimate Thursday of every month; they apply to the following calendar month and represent rates as of midday the day before publication. The files sit at a predictable URL — note that the month is not zero-padded:

https://www.trade-tariff.service.gov.uk/exchange_rates/view/files/monthly_csv_2026-9.csv
https://www.trade-tariff.service.gov.uk/exchange_rates/view/files/monthly_xml_2026-9.xml

One monthly fetch, cached for the month, and every UK VAT conversion in that period is reproducible from a file you can hand to an inspector.

European Union: Article 91 of the VAT Directive

For intra-EU supplies, Article 91(2) of Council Directive 2006/112/EC sets the rule as "the latest selling rate recorded, at the time VAT becomes chargeable, on the most representative exchange market or markets of the Member State concerned, or a rate determined by reference to that or those markets."

That would be difficult to implement across 27 member states, so the Directive adds a practical escape hatch: Member States "shall accept instead the use of the latest exchange rate published by the European Central Bank at the time the tax becomes chargeable." Conversion between two non-euro currencies is done "by using the euro exchange rate of each currency" — in other words, cross via EUR rather than quoting the pair directly. Member States may require notification that you are exercising this option.

For imports, Article 91(1) defers instead to the customs rules on calculating value for customs purposes — a genuinely different rate, on a genuinely different date, in the same ledger. If your system treats "EU VAT" as one conversion rule, it is already wrong.

Underneath All of It: IAS 21

Tax rules sit on top of your accounting policy, and for IFRS reporters that policy is IAS 21. Four provisions do most of the work:

  • A foreign currency transaction is initially recognised at the spot rate at the date of the transaction (IAS 21.21).
  • An average rate is permitted as a simplification, but only "as long as exchange rates don't fluctuate significantly" (IAS 21.22). This is a condition, not a default — and it is the clause that quietly fails in a volatile quarter.
  • Monetary items are retranslated at the closing rate at the reporting date (IAS 21.23).
  • Non-monetary items measured at historical cost stay at the transaction-date rate and are not retranslated.

US GAAP reaches broadly similar conclusions under ASC 830. The practical consequence for a developer is that a single transaction can legitimately require two or three different rates over its life — one at recognition, one at period close, one at settlement — and your schema needs room for all of them. Our guide to currency risk management for businesses covers what the resulting gains and losses mean commercially.

Five Conversion Bugs That Turn Into Restatements

1. Rate Direction

base=USD&symbols=EUR returns euros per dollar. base=EUR&symbols=USD returns dollars per euro. The IRS yearly table is foreign-per-USD, so you divide; a Finexly response with base=EUR is USD-per-EUR, so you multiply. Both are correct; mixing them is not.

The fix is boring and effective: never name a column rate. Name it quote_per_base, and make the direction unambiguous in the schema rather than in a comment.

2. Re-Querying Instead of Replaying

An audit in 2029 asks about a transaction from 2026. If your reporting code calls a live endpoint at report-generation time, two runs of the same report produce two different numbers. The rate applied to a transaction is a fact about that transaction, not a lookup — persist it at the moment of conversion. Historical endpoints exist to backfill and reconcile, not to substitute for storage; see our historical exchange rates API guide for the backfill patterns.

3. Average Where Spot Is Required

Monthly averages are convenient and often permitted, but IAS 21.22 attaches a condition and one-off transactions generally require the transaction-date rate under IRS guidance. Store the method alongside the rate so you can answer "why this number?" without archaeology.

4. Missing Days

Weekends, national holidays and non-TARGET days have no published rate. Every system needs an explicit rule — usually "last published rate on or before the date" — and it needs to record which rule fired. A silent fallback is indistinguishable from a bug six months later. This overlaps directly with caching and error-handling practice.

5. Precision and Rounding

Store more decimal places than you display, and round exactly once, at the final presentation or posting step. Rounding at each intermediate step across a few thousand invoices produces a reconciliation gap that is tedious to explain and impossible to reverse. We covered the arithmetic in detail in currency rounding and decimal places.

Design the Rate Snapshot, Not the Rate Lookup

The whole problem collapses if you stop thinking of conversion as a function call and start thinking of it as an immutable record. Here is a minimal schema that satisfies every requirement discussed above:

CREATE TABLE fx_rate_snapshot (
    id                BIGSERIAL PRIMARY KEY,
    transaction_id    BIGINT      NOT NULL,
    -- direction is in the name, not in a comment
    base_currency     CHAR(3)     NOT NULL,   -- e.g. 'EUR'
    quote_currency    CHAR(3)     NOT NULL,   -- e.g. 'USD'
    quote_per_base    NUMERIC(20,10) NOT NULL,

    rate_date         DATE        NOT NULL,   -- the date the rate APPLIES to
    method            TEXT        NOT NULL,   -- 'spot' | 'monthly_period' | 'yearly_average'
    fallback_applied  TEXT,                   -- 'previous_business_day' | NULL

    source            TEXT        NOT NULL,   -- 'finexly' | 'hmrc_monthly' | 'irs_yearly'
    source_reference  TEXT,                   -- file name, request id, or table year
    retrieved_at      TIMESTAMPTZ NOT NULL,   -- when WE obtained it

    amount_base       NUMERIC(20,4) NOT NULL,
    amount_quote      NUMERIC(20,4) NOT NULL,

    created_at        TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- one authoritative conversion per transaction per purpose
CREATE UNIQUE INDEX ux_fx_snapshot_txn_method
    ON fx_rate_snapshot (transaction_id, method, rate_date);

Three columns carry most of the audit weight. rate_date is the date the rate applies to and is deliberately separate from retrieved_at, the moment you obtained it — a rate for 13 March fetched during a September backfill is perfectly legitimate, and the pair of timestamps says so honestly. fallback_applied turns your weekend rule from invisible behaviour into recorded evidence.

Note that rows are never updated. If a rate is corrected, insert a new row and supersede the old one. An audit trail you can edit is not an audit trail.

Fetching a Defensible Historical Rate

For a transaction-date rate, request the specific date rather than the current one:

curl "https://api.finexly.com/v1/historical?date=2026-03-13&base=EUR&symbols=USD" \
  -H "Authorization: Bearer YOUR_API_KEY"
{
  "base": "EUR",
  "date": "2026-03-13",
  "rates": {
    "USD": 1.0842
  }
}

With base=EUR, the value is dollars per euro — so a €10,000 invoice converts to $10,842.00 by multiplication. Here is the snapshot-on-write pattern in Python, including the missing-day fallback and the fields the schema above expects:

import requests
from datetime import date, timedelta
from decimal import Decimal, ROUND_HALF_UP

API = "https://api.finexly.com/v1/historical"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}


def fetch_rate(base: str, quote: str, on: date, max_lookback: int = 5):
    """Return (quote_per_base, rate_date, fallback) for a given date.

    Walks back to the most recent published rate if `on` is a weekend
    or a market holiday, and reports which day it actually landed on.
    """
    for offset in range(max_lookback + 1):
        d = on - timedelta(days=offset)
        r = requests.get(
            API,
            params={"date": d.isoformat(), "base": base, "symbols": quote},
            headers=HEADERS,
            timeout=10,
        )
        r.raise_for_status()
        rates = r.json().get("rates", {})
        if quote in rates:
            fallback = "previous_business_day" if offset else None
            return Decimal(str(rates[quote])), d, fallback

    raise LookupError(f"No {base}/{quote} rate within {max_lookback} days of {on}")


def convert_for_filing(amount_base, base, quote, transaction_date):
    """Convert once, and return everything an auditor will ask for."""
    rate, rate_date, fallback = fetch_rate(base, quote, transaction_date)
    amount = Decimal(str(amount_base))
    converted = (amount * rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

    return {
        "base_currency": base,
        "quote_currency": quote,
        "quote_per_base": rate,
        "rate_date": rate_date.isoformat(),
        "method": "spot",
        "fallback_applied": fallback,
        "source": "finexly",
        "amount_base": amount,
        "amount_quote": converted,
    }


snapshot = convert_for_filing(10000, "EUR", "USD", date(2026, 3, 13))
print(snapshot["amount_quote"], snapshot["rate_date"], snapshot["fallback_applied"])
# -> 10842.00 2026-03-13 None

The function converts and documents in the same breath. Whatever your persistence layer is, write that dictionary to it before anything downstream sees the number. The same discipline pays off across multi-currency invoicing, SaaS billing and cross-border payroll — all of which end up in the same tax return.

Reconciling Against the Official Published Rate

For UK VAT under route 2, pull HMRC's monthly file once per month and store it as the source of truth for that period:

import csv, io, requests
from datetime import date

def hmrc_monthly_rates(year: int, month: int) -> dict:
    """HMRC monthly rates for VAT/customs. Note: month is NOT zero-padded."""
    url = (
        "https://www.trade-tariff.service.gov.uk/exchange_rates/view/files/"
        f"monthly_csv_{year}-{month}.csv"
    )
    resp = requests.get(url, timeout=15)
    resp.raise_for_status()

    reader = csv.DictReader(io.StringIO(resp.text))
    # Don't hard-code the header text: find the ISO-4217 code column by shape.
    code_col = next(
        c for c in reader.fieldnames if "code" in c.strip().lower()
    )
    return {
        row[code_col].strip(): row
        for row in reader
        if row.get(code_col) and len(row[code_col].strip()) == 3
    }

Then run a periodic drift check: compare each stored snapshot against the official rate for its period and flag anything outside a tolerance you have chosen deliberately. Small divergences between a market rate and a published administrative rate are expected and usually acceptable — but you want to know the size of the gap before an inspector calculates it for you. If you are wiring this into a ledger, our notes on accounting software integration and on where exchange rate APIs get their data cover the provenance questions that come next.

A Pre-Filing Checklist for Multi-Currency Data

  1. Every converted amount has a stored rate. No report recomputes a historical conversion at run time.
  2. Rate direction is unambiguous in column names, not in documentation.
  3. rate_date and retrieved_at are separate fields, and both are populated.
  4. The missing-day rule is explicit and recorded, not an implicit retry.
  5. One method per class of transaction, applied consistently across the whole period — the actual legal requirement in both the US and UK.
  6. Rounding happens once, at the final step, with a documented mode.
  7. Snapshot rows are append-only. Corrections supersede; they never overwrite.

Work through those seven and the accountant's question stops being frightening. The answer becomes a query.

Frequently Asked Questions

Which exchange rate does the IRS require for tax reporting? None specifically. The IRS states plainly that it "has no official exchange rate" and "generally accepts any posted exchange rate that is used consistently." The default is the spot rate prevailing when you receive, pay or accrue the item; for steadily accruing income, the IRS publishes a yearly average table and tells filers to divide the foreign amount by the listed rate.

Can I use a currency API instead of HMRC's published rates for VAT? Yes, within limits. VAT Notice 700 §7.6 makes the UK market selling rate at the time of supply the default, so a market-based API rate fits that route provided you apply it consistently. HMRC's own period rate is an explicit alternative you can adopt without prior notification — but once adopted you cannot switch back without written agreement. A rate or method that falls outside both routes requires a written application, and forward rates are not accepted.

Do I need to store the exchange rate, or can I look it up again later? Store it. A stored rate makes a conversion reproducible; a re-query makes it a fresh calculation that may not match the return you already filed. Storing the rate, its date, its source and the moment you retrieved it is what turns a number into evidence.

What rate should I use for a weekend or holiday transaction date? There is no published rate for a non-trading day, so you need a stated rule — most commonly the last published rate on or before the transaction date. What matters more than which rule you pick is that it is documented, applied uniformly, and recorded on each affected row.

How many decimal places should I store for tax purposes? Store the full precision your provider returns — six to ten decimal places is a sensible column width — and round only when posting or displaying. Rounding early and repeatedly is the usual cause of reconciliation gaps that nobody can trace.

Does the ECB rate satisfy EU VAT requirements? Article 91(2) of the VAT Directive requires Member States to accept the latest ECB rate published at the time the tax becomes chargeable, with cross-currency conversions routed through each currency's euro rate. Some Member States require notification that you are using this option, and imports follow the customs valuation rules instead.

Get Your Rates Audit-Ready

Ready to integrate real-time and historical exchange rates into your project? Get your free Finexly API key — no credit card required. Start with 1,000 free requests per month and upgrade as you grow. Check the API documentation for the full historical endpoint reference, try the currency converter for one-off checks, compare currency APIs if you are evaluating providers, and review pricing plans when your reporting volume grows.

This article is a technical guide for developers building multi-currency systems. It is not tax advice — confirm your conversion policy with a qualified adviser in each jurisdiction where you file.

Vlado Grigirov

Senior Currency Markets Analyst & Financial Strategist

Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...

View full profile →

Distribuie acest articol