@fin.cx/fxrates
Euro exchange rates for German bookkeeping. It reads the BMF's monthly VAT conversion rates (§ 16 Abs. 6 UStG), fetches the Bundesbank's monthly averages those rates are taken from, and fetches the ECB's daily reference rates. It converts amounts exactly, in integer cents. Rates stay decimal strings with exactly the digits published, never floats. The package keeps nothing beyond memory; storing the rates a return used is the application's part.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Install
pnpm install @fin.cx/fxrates
What the law says
- The rule: the BMF's monthly average. § 16 Abs. 6 Satz 1 UStG: "Werte in fremder Währung sind zur Berechnung der Steuer und der abziehbaren Vorsteuerbeträge auf Euro nach den Durchschnittskursen umzurechnen, die das Bundesministerium der Finanzen für den Monat öffentlich bekanntgibt, in dem die Leistung ausgeführt oder das Entgelt oder ein Teil des Entgelts vor Ausführung der Leistung (§ 13 Abs. 1 Nr. 1 Buchstabe a Satz 4) vereinnahmt wird."
- Ist-Versteuerung: under § 20, the month in which the Entgelte are received (Satz 2).
- Advances: an advance in a foreign currency keeps the average of its month of receipt, even when the supply falls into a month with another average (UStAE 13.5 Abs. 7).
- Later rate changes: changes between the supply and the receipt of the Entgelt are disregarded (UStAE 16.4 Abs. 1 Satz 2).
- The daily rate, by permission. "Das Finanzamt kann die Umrechnung nach dem Tageskurs, der durch Bankmitteilung oder Kurszettel nachzuweisen ist, gestatten" (§ 16 Abs. 6 Satz 3; UStAE 16.4 Abs. 2 Satz 1).
- The previous month's average, by permission. As a simplification, the Finanzamt can allow the average of the month before the month of the supply or the receipt (UStAE 16.4 Abs. 2 Satz 2).
- OSS and IOSS (§ 18 Abs. 4c, 4e, §§ 18i–18k) convert at the ECB rate of the last day of the tax period, or of the next day if none was fixed (§ 16 Abs. 6 Sätze 4–5).
EcbReferenceRatesgives those rates. This package has no OSS logic. - Balance-sheet valuation is not VAT. § 256a HGB converts foreign-currency assets and liabilities at the "Devisenkassamittelkurs am Abschlussstichtag". Whether the ECB reference rate serves as that rate is for the preparer to decide; the law does not name a source.
Sources as read on 2026-09-27: § 16 UStG and § 256a HGB on gesetze-im-internet.de; the Umsatzsteuer-Anwendungserlass in its consolidated version "Stand 2. Juni 2026".
vatRateMonth(method, eventDate) and VatRateBook.resolveForEvent apply these rules. Which day counts as the event (the supply, an advance's receipt, a receipt under Ist) is the caller's knowledge.
Sources and their terms
| Source | What | Terms |
|---|---|---|
| BMF Datenportal, Umsatzsteuer-Umrechnungskurse seit 2010 | Yearly tables since 2010 and a monthly updated table of the current year, as CSV. The rates are "1 Euro = x" per currency and month, and from 2018 each month names the BMF-Schreiben that published it | Datenlizenz Deutschland – Namensnennung – 2.0: any use, commercial included, when the provider (Bundesministerium der Finanzen), the licence and the dataset are named. Changed data must be marked as changed |
Deutsche Bundesbank, series BBEX3.M.<currency>.EUR.BB.AC.A02 |
The monthly averages of the ECB reference rates, computed by the Bundesbank. The BMF receives them on the first working day of the following month and publishes them that day | The figures originate with the ECB (below). The Bundesbank's own terms for its API were not found and are not stated here |
| ECB, euro foreign exchange reference rates | Daily rates, from the concertation around 14:10 CET, published around 16:00 CET each TARGET working day | Reproduction is allowed when the ECB is cited as the source. The ECB publishes the rates "for information purposes only. Using the rates for transaction purposes is strongly discouraged" |
The BMF cannot be fetched automatically. bundesfinanzministerium.de answers automated requests with a captcha. This package therefore does not download BMF tables: parseBmfVatRateCsv reads a table someone downloaded.
The Bundesbank's averages are the BMF's figures. All BMF table cells from January 2010 to March 2024 were compared with the Bundesbank series (5 323 cells). They are equal in value in all but four. Each of the four is a one-digit slip in the BMF table:
| Month | BMF | Bundesbank |
|---|---|---|
| TRY 2015-07 | 311,53 | 2.9705 |
| MXN 2017-08 | 20,0333 | 21.0333 |
| THB 2018-07 | 37,894 | 38.894 |
| ZAR 2019-07 | 15,7512 | 15.7412 |
Whether the BMF-Schreiben of those months carry the same slips was not verified. The Schreiben, not the Datenportal table, is the publication § 16 Abs. 6 refers to. VatRateBook therefore reports any disagreement as a conflict for a person to settle, and never picks a side.
Usage
The VAT rate of a month
import * as fxrates from '@fin.cx/fxrates';
const book = new fxrates.VatRateBook({
bundesbank: new fxrates.BundesbankMonthlyAverages(),
});
// optional: a BMF table someone downloaded, for the BMF-Schreiben references and the cross-check
book.addBmfTable(fxrates.parseBmfVatRateCsv(bytesOfTheCsv));
const rate = await book.resolve('USD', '2026-08');
switch (rate.state) {
case 'published':
// rate.rate: the decimal as published; rate.source is 'bmf' (with rate.bmf.publication) or 'bundesbank'
break;
case 'conflict': // the BMF table and the Bundesbank disagree: rate.bmf, rate.average
case 'not_yet_published': // the month is not over in Germany
case 'not_available': // over, but no source lists it yet (the BMF publishes on the first working day after)
case 'no_rate': // the month's figures list no rate in this currency (for example the rouble since 2 March 2022)
break;
}
// the rate a supply of 15 February 2026 converts at, where the Finanzamt allowed the previous month's average
await book.resolveForEvent('bmf_previous_month', 'USD', '2026-02-15');
A BMF table
const table = fxrates.parseBmfVatRateCsv(bytes); // Windows-1252 as downloaded, or text
table.rates; // [{ currency: 'USD', month: '2023-01', rate: '1.0769', country: 'USA', printed: '1,0769 USD' }, …]
table.publications; // [{ month: '2023-02', letterDate: '2023-03-01', reference: 'III C 3 – S 7329/19/10001 :005 (2023/0211507)', text: … }, …]
table.anomalies; // what the table gets wrong, never silently repaired
The parser reads both layouts the dataset uses. It reports these defects found in the published files:
- cells without the space between number and code (
7,4455DKK); - a wrong code (
1.345,06 RWin the KRW row); - a footnote dating January 2023's letter "1. Februar 2022".
Daily reference rates
const ecb = new fxrates.EcbReferenceRates();
await ecb.getDay('2026-09-25'); // every currency's rate of that day; [] on a weekend or TARGET holiday
await ecb.getLatestOnOrBefore('2026-09-27', 'USD'); // { currency: 'USD', date: '2026-09-25', rate: '1.1403' }
Whether the rate of an earlier day may stand for a day without a fixing is the caller's decision. The rate carries its own day.
Converting
fxrates.eurCentsOf(100000, '1.0769'); // 1 000.00 USD → 92 859 (928.59 EUR)
fxrates.foreignCentsOf(10000, '0.86828'); // 100.00 EUR → 8 683 (86.83 GBP)
Amounts are hundredths of the unit for every currency, as @fin.cx/calculation counts them: 1 234 JPY are 123 400. The result is rounded once, half away from zero (kaufmännisches Runden). Neither § 16 Abs. 6 UStG nor UStAE 16.4 states a rounding rule; this is the rounding @fin.cx/calculation uses for every other amount.
Runtime
The package uses the standard fetch (injectable through the options) and Intl.DateTimeFormat with the Europe/Berlin time zone. It decodes Windows-1252 by the WHATWG Encoding Standard's index itself, with an internal table, not with the runtime's TextDecoder. It runs in Node.js and in browsers, though the Bundesbank and ECB APIs may refuse cross-origin requests from a browser.
Why the package decodes Windows-1252 itself. Some Node.js versions decode Windows-1252 as Latin-1 (nodejs/node#60888): the fast path of nodejs/node#55275 came with 22.13.0, and the fix nodejs/node#60893 with 22.22.1, 24.13.1 and 25.4.0. On 22.13.0 to 22.22.0, on 23.x, on 24.0.0 to 24.13.0 and on 25.0.0 to 25.3.x every character from 0x80 to 0x9F would come out as a control character, the en dashes of the BMF references among them. parseBmfVatRateCsv reads a table the same on every runtime.
Nothing is written to disk. The clients keep the answers for past periods in memory and ask again for a period that may still change: the Bundesbank a month without figures yet, the ECB a range until more than ECB_SETTLED_AFTER_DAYS (7) days have passed since its last day in Berlin, since its data API can lag behind the publication and an answer for recent days may still lack a fixing. A source that does not answer within timeoutMs (default 30 000, DEFAULT_TIMEOUT_MS) is given up: the request is aborted and the call rejects, naming the source.
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license.md file.
The test fixtures under test/fixtures/ are unchanged copies of data by the Bundesministerium der Finanzen and the European Central Bank, plus synthetic files in the layout of the Deutsche Bundesbank's API. See test/fixtures/readme.md. They are not part of the published package.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.