@fin.cx/opendata
v4.10.0
Published
Open business and financial data for TypeScript: register lookups on the German register portal (handelsregister.de) without storage, the statutory public holidays of the German federal states, the base rate of § 247 BGB, the German consumer price index w
Maintainers
Keywords
Readme
@fin.cx/opendata
@fin.cx/opendata is a TypeScript toolbox for open business and market data: stock and crypto prices, SEC fundamentals, lookups in the German registers through the register portal of the federal states, the statutory public holidays of the German federal states, the base rate of § 247 BGB, the German consumer price index for index clauses, and the figures of published financial statements. It is built for programmers who want scriptable APIs and real workflows instead of a thin wrapper around one remote endpoint.
It gives you nine practical lanes in one package:
StockPriceServicefor stock and crypto prices with caching, retries, failover, historical ranges, and intraday supportFundamentalsServiceandStockDataServicefor SEC fundamentals and combined price + fundamentals payloadsHandelsregisterClient, from@fin.cx/opendata/handelsregister, for single lookups in the commercial, cooperative, partnership, association and civil-law partnership registers over plain HTTP, without any storage- the holiday calendar, from
@fin.cx/opendata/holidays, for the statutory public holidays of each German federal state, the working days of § 193 BGB and the bank working days the social insurance counts its due dates in, computed per year without data files or requests - the base rate, from
@fin.cx/opendata/baserate, for the Basiszinssatz of § 247 BGB per half-year as the Deutsche Bundesbank publishes it, for default interest under § 288 BGB - VAT ID confirmation, from
@fin.cx/opendata/vatid, over the BZSt's eVatR REST API (the confirmation of § 18e UStG, simple or qualified) and the European Commission's VIES REST API, without any storage - the consumer price index, from
@fin.cx/opendata/cpi, for the Verbraucherpreisindex für Deutschland of the Statistisches Bundesamt as the Deutsche Bundesbank republishes it, with the arithmetic of index clauses (Wertsicherungsklauseln, § 557b BGB), without any storage - published financial statements, from
@fin.cx/opendata/financialstatements, for the balance sheet and the income statement under the HGB from the HTML or the PDF the Unternehmensregister shows, and the facts of an ESEF report, read from a document the caller obtained, without any request or storage - GLEIF Level 1 data, from
@fin.cx/opendata/gleif, for streamed Golden Copy full/delta imports, single-LEI lookup, and German registration-authority codes mapped to XJustiz register courts
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.
Installation
pnpm add @fin.cx/opendataWhat You Get
Market Data
StockPriceServiceFetches current, batch, historical, and intraday price data.FundamentalsServiceFetches SEC EDGAR fundamentals for US-listed companies.StockDataServiceCombines price + fundamentals and enriches fundamentals with metrics likemarketCap,priceToEarnings, andpriceToBook.MarketstackProviderAuthenticated stock market provider with current, batch, historical, and intraday support.CoinGeckoProviderCrypto market provider behind the same stock-price interface.SecEdgarProviderFundamentals provider backed by SEC EDGAR company facts.
German Registers
Everything here comes from @fin.cx/opendata/handelsregister. That entry point imports nothing else of the package, needs no database, no browser and no download folder, and keeps nothing between calls.
HandelsregisterClientSearches the register portal (handelsregister.de) by name or register number and retrieves one register sheet with its structured register content (SI) parsed into a typed result.SlidingWindowGateHolds the portal's limit of 60 searches and retrievals per hour. The client asks it before every counted request.parseStructuredContent()andparseSearchResults()The parsers on their own, for an SI file or a result page someone saved from the portal. They make no request.minimiseStructuredContent()A copy of SI XML without natural-person birth details or private addresses, with its own SHA-256. It makes no request and preserves the remaining XML bytes.registerCourts,findRegisterCourt(),parseRegisterReferenceText(),formatRegisterReference()The 150 register courts with their XJustiz codes, and helpers for register references as businesses print them (Amtsgericht Bremen HRB 35230 HB).
German Public Holidays
Everything here comes from @fin.cx/opendata/holidays. That entry point imports nothing else of the package, has no dependencies, and makes no request.
getPublicHolidays(year, state),getPublicHolidaysOn(date, state)The statutory public holidays of a federal state in a year or on a date, each with its legal basis.isPublicHoliday(),isWorkingDay(),nextWorkingDay(),workingDayOnOrAfter()Holidays and working days in the sense of § 193 BGB: neither a Saturday, nor a Sunday, nor a statutory public holiday.isBankWorkingDay(),nthLastBankWorkingDay()Bank working days (Bankarbeitstage), which also leave out 24 and 31 December, when the Deutsche Bundesbank is closed: the days German social insurance due dates are counted in.easterSunday(year),germanStates,findGermanState()
The Base Rate Of § 247 BGB
Everything here comes from @fin.cx/opendata/baserate. That entry point imports nothing else of the package, has no dependencies, and makes no request.
getBaseRate(date)The base rate in force on a day: its half-year period with the rate in percent a year.splitByBaseRate(from, to)A span of days split where the rate changes, as interest under § 288 BGB is computed.baseRatePeriods,baseRateFirstDay,baseRatesReadOn,baseRateSource
VAT ID Confirmation
Everything here comes from @fin.cx/opendata/vatid. That entry point imports nothing else of the package and has no dependencies. It keeps nothing: each answer goes to the caller, with the answer as it arrived (answer: the HTTP status and the body, byte for byte), so the caller can keep it as evidence where the law asks for it. For an intra-Community supply that is the books' record of the buyer's foreign VAT ID (§ 17d Abs. 1 UStDV); a confirmation also shows the care of a prudent businessman on which the exemption rests in good faith (§ 6a Abs. 4 UStG).
EvatrClient#confirm({ requesterVatId, vatId, trader? })The BZSt's confirmation of a foreign VAT ID (§ 18e Nr. 1 UStG). The requester is a business with a German VAT ID. Withtrader(name and city, optionally street and postal code) it is a qualified confirmation. Where the VAT ID is valid the answer carries the member state's result for each field (qualified.answered: true;Amatches,Bdoes not match,Cnot asked,Dnot given), or, forevatr-2008, that the comparison has a particularity the BZSt explains on request (qualified.answered: false). A valid answer to a qualified request without a documented result for each field sent is refused asunexpected_answer, never passed on as a simple confirmation.ViesClient#check({ vatId, requesterVatId?, trader? })The European Commission's VIES check, with the registered name and address where the member state gives them, a comparison of the trader data (VALID,INVALID,NOT_PROCESSED), and VIES's request identifier where the requester is named. The name is VIES'sname, or, where it gives none, itstraderName: the contract liststraderNamein the answer but does not say whether it is the registered name or the one asked about; the test service answers a comparison with the registered name there. Everything else VIES answers is inanswer.splitVatId(value),vatIdCountryCodes,evatrStatusCodesVatIdErrorwith a code for every reason no answer about validity arrived, and the answer as it arrived where one did (answer)
Both clients take a fetch (for example createPublicOnlyFetch of @push.rocks/smartrequest), a baseUrl for a test server, a timeoutMs (30 seconds by default; its timer ends with the request) and a caller's signal. Nothing is retried.
import { EvatrClient, ViesClient, VatIdError } from '@fin.cx/opendata/vatid';
const evatr = new EvatrClient();
const confirmation = await evatr.confirm({
requesterVatId: 'DE123456789',
vatId: 'ATU12345678',
trader: { name: 'Musterhaus GmbH & Co KG', street: 'Musterstrasse 22', postalCode: '12345', city: 'musterort' },
});
// { service: 'bzst-evatr', requestId, requestedAt, status: 'evatr-0000', valid: true,
// qualified: { answered: true, name: 'A', city: 'A', ... }, answer: { httpStatus: 200, text: '{"id":…}' } }
try {
await new ViesClient().check({ vatId: 'ATU12345678', requesterVatId: 'DE123456789' });
} catch (error) {
if (error instanceof VatIdError) {
console.log(error.code, error.serviceCode); // for example 'member_state_unavailable', 'MS_UNAVAILABLE'
}
}The Consumer Price Index
Everything here comes from @fin.cx/opendata/cpi. That entry point imports nothing else of the package and has no dependencies. It keeps nothing: each answer goes to the caller with its source note and the answer as it arrived (answer: the HTTP status and the body), which the caller keeps as the evidence of an index adjustment.
BundesbankCpiClient#getMonthly({ from, to? }),#getAnnual({ from, to? }),#getMonth(month)The Verbraucherpreisindex für Deutschland (VPI) of the Statistisches Bundesamt, overall index, unadjusted, monthly and as annual averages, as the Deutsche Bundesbank republishes it in its time series API. No registration is needed. Values are decimal strings with the digits published ('113.5'), never floats. Each series names the base it is stated on (base: '2020=100'), read from the answer.cpiChangePercent(old, new),cpiChangePoints(old, new)The change in percent as the Statistisches Bundesamt computes it, ((new / old) × 100) − 100, rounded to one decimal; and the change in index points.cpiThresholds(reference, percent),cpiFirstReaching(values, reference, percent, { direction, reach })The thresholds of a percent rule (reference × (1 ± percent / 100), rounded to the reference's digits), and the first month after the reference month that reaches one. Whether the threshold itself counts (reached) or only a value beyond it (exceeded) is the clause's to say.cpiFirstReachingPoints(values, reference, points, { direction, reach })The same for a points rule: the first month after the reference month whose change in index points reaches the threshold, at or above reference + points (up), at or below reference − points (down), or whichever comes first (either). The points are a decimal from 0 upward ('5','2.5'); the comparison is exact and nothing is rounded.cpiScaleAmount(amountCents, old, new, { basis? })An amount in cents scaled from one index value to another: by the index ratio (amount × new / old, the default) or, withbasis: 'rounded_percent', by the change in percent rounded to one decimal.CpiErrorwith the codesinvalid_input,before_first_period,not_published,base_mismatch,series_withdrawn,busy,unavailable,unreachable,timeout,abortedandunexpected_answer
The client takes a fetch (for example createPublicOnlyFetch of @push.rocks/smartrequest), a baseUrl for a test server, a timeoutMs (30 seconds by default; its timer ends with the request), a userAgent and a caller's signal. Nothing is retried.
import { BundesbankCpiClient, CpiError, cpiChangePercent, cpiFirstReaching, cpiFirstReachingPoints, cpiScaleAmount } from '@fin.cx/opendata/cpi';
const client = new BundesbankCpiClient();
const series = await client.getMonthly({ from: '2020-01' });
// { index: 'de-vpi', frequency: 'monthly', base: '2020=100', baseYear: 2020,
// values: [{ period: '2020-01', value: '99.8', status: 'final', ... }, ...],
// estimates: [{ period: '2026-09', value: '126.6', status: 'estimated', ... }],
// source: { attribution: 'Quelle: Deutsche Bundesbank, ...', ... }, answer: { httpStatus: 200, text: '...' } }
const may2020 = series.values.find((value) => value.period === '2020-05')!; // 100.4
const october2022 = series.values.find((value) => value.period === '2022-10')!; // 113.5
cpiChangePercent(may2020, october2022); // '13.0'
cpiScaleAmount(100_000, may2020, october2022); // 113048 cents
cpiFirstReaching(series.values, series.values.find((value) => value.period === '2020-08')!, '5', {
direction: 'up',
reach: 'reached',
}); // the value of 2021-12 (104.7)
cpiFirstReachingPoints(series.values, october2022, '5', { direction: 'up', reach: 'reached' }); // the value of 2024-03 (118.6); 5 % first in 2024-04
try {
await client.getMonth('2026-09');
} catch (error) {
if (error instanceof CpiError && error.code === 'not_published') {
console.log(error.estimate?.value); // '126.6', the Bundesbank's estimate, not a published value
}
}Published values and estimates
The Statistisches Bundesamt publishes a flash estimate of a month one or two working days before the month ends, and the final value about two weeks after it. For the latest month the Bundesbank's series at times carries its own estimate, marked E. The client keeps such values apart (estimates, status: 'estimated'): values holds only published ones, getMonth() of a month with only an estimate ends in not_published with the estimate in the error, and every calculation refuses an estimate with not_published. A range or a month with no value yet ends in not_published too.
Base years
The Statistisches Bundesamt moves the index to a new base year about every five years (2020=100 since the results for January 2023). It recomputes the values from January of the new base year and converts the earlier ones to the new base, so that a value read on one base differs from the same month read on another. It publishes no factors to convert between bases, and has not done so since 2003. So:
- every value carries its base, and the calculations refuse values of different bases, indices or frequencies with
base_mismatch; nothing is converted; - for a percent rule the Statistisches Bundesamt calculates with the values of the current base for both months, even where the contract names an earlier base, so read both again on the current base;
- points depend on the base.
cpiChangePoints()andcpiFirstReachingPoints()give them within one base only; a points rule on an earlier base is the contract's matter; - the client leaves the base open in its request, and the Bundesbank answers with the series of the current base. The key of the former base 2015=100 answers 404 today; an answer of 404 ends in
series_withdrawn, and an answer that holds two bases inunexpected_answer.
Attribution
The figures are the Statistisches Bundesamt's, under the Datenlizenz Deutschland – Namensnennung – Version 2.0 (https://www.govdata.de/dl-de/by-2-0), delivered by the Deutsche Bundesbank. Wherever you show an index value, show its source note (source.attribution), which names:
- "Quelle: Deutsche Bundesbank" with the series key;
- the Statistisches Bundesamt (Destatis) as the provider of the data, with the index and its base;
- the licence "Datenlizenz Deutschland – Namensnennung – Version 2.0" with its link;
- the request the values were read from.
A change in percent, a threshold or a scaled amount is not a published figure. Mark it as such, for example "eigene Berechnung", next to the source note.
Published Financial Statements
Everything here comes from @fin.cx/opendata/financialstatements. That entry point imports nothing else of the package, makes no request and keeps nothing. It reads a statement from the bytes the caller obtained, such as a statement saved from the Unternehmensregister, and returns what the caller stores: the filing, the document byte for byte, and what could be read from it. Its types hold no storage of their own, so any store can keep them.
readFinancialStatement({ filing, bytes, mediaType, now? })A statement as a caller keeps it (IFinancialStatement): the filing as the caller states it (filing, anIFiling), the document with its media type and SHA-256 (raw), the statement read from it (parsed), why none was (parseFailure), when it was obtained (obtainedAt) and the publisher's origin note (notice). An ESEF report, on its own or in its package, goes to the ESEF reader, and so does HTML that holds inline XBRL; any other HTML or PDF goes toparseStatementDocument.parseStatementDocument(bytes, mediaType)A statement under the HGB from the HTML or the PDF a register shows (IHgbStatement): the balance sheet (§ 266 HGB) and, where one is published, the income statement (§ 275 HGB, by the total cost or the cost of sales method), each printed line with its outline code, label, kind and the amounts of both years, the unit (EUR, TEUR or Mio. EUR), the periods, and the key figures: balance sheet total, equity, net income and revenue. A PDF is read from its text layer.parseEsefPackage(zipBytes),parseEsefReport(xhtmlBytes, reportPath?)A listed issuer's annual financial report in the European Single Electronic Format (IEsefStatement): the LEI, every numeric fact its inline XBRL tags (concept, period, dimensions, unit, and the value with scale and sign applied), and the key figures of its consolidated statements from the IFRS conceptsAssets,Equity,ProfitLossandRevenue. A package must hold exactly one report underreports/.unternehmensregisterNotice(publishedOn, deposited?),unternehmensregisterOriginThe Unternehmensregister's origin note, "Auszug aus dem Unternehmensregister", with the day a document was posted or deposited.maxStatementBytes(100 MiB),maxPdfPages(500),maxEsefFacts(100,000),maxEsefPackageEntries(10,000)FinancialStatementErrorwith the codesinvalid_input,unsupported_media_type,too_large,invalid_package,unreadable,no_textandunparseable
Amounts are decimal strings in the currency itself, the unit's factor applied (1.234 printed in TEUR is '1234000'), never floats.
import { FinancialStatementError, readFinancialStatement } from '@fin.cx/opendata/financialstatements';
const statement = await readFinancialStatement({
filing: {
source: 'unternehmensregister',
kind: 'annual_accounts',
companyName: 'Muster Werkzeugbau GmbH',
fiscalYearStart: '2024-01-01',
fiscalYearEnd: '2024-12-31',
publishedOn: '2025-11-03',
},
bytes, // the PDF as saved from the register, a Uint8Array
mediaType: 'application/pdf',
});
// statement.raw: { bytes, mediaType: 'application/pdf', sha256: '...' }
// statement.notice: 'Auszug aus dem Unternehmensregister, eingestellt am 03.11.2025'
if (statement.parsed?.standard === 'hgb') {
statement.parsed.keyFigures.current;
// { totalAssets: '500000.00', equity: '300000.00', netIncome: '40000.00', revenue: '1000000.00' }
} else if (statement.parseFailure) {
statement.parseFailure; // { code: 'unparseable', message: 'No balance sheet was found.' }
}
try {
await readFinancialStatement({ filing: statement.filing, bytes: scan, mediaType: 'image/png' });
} catch (error) {
if (error instanceof FinancialStatementError) {
console.log(error.code); // 'unsupported_media_type': nothing is kept
}
}Read, never guessed
parsed is null rather than guessed. Everything parseStatementDocument returns was printed and adds up: each position with parts equals their sum, each side of the balance sheet its total, both totals each other, and the lines of the income statement its net income, whether it prints its expenses with a minus or as amounts to subtract. A balance sheet whose current year does not add up, whose columns do not state their year or unit, that holds text where an amount belongs, or that is in a currency other than the euro ends in unparseable; readFinancialStatement then keeps the document with parsed null and the reason in parseFailure. A prior year that does not add up, and an income statement that does not, are left out with a note in notes. A PDF without a text layer, such as a scan, ends in no_text: nothing is recognised from images.
The ESEF reader takes values as the report tags them. It reads the formats num-dot-decimal, num-comma-decimal, fixed-zero and zerodash (and the older numdotdecimal and numcommadecimal); a fact in another format is left out and counted in notes. A key figure tagged twice with values that differ at the coarser of their decimals is left out with a note.
Where the documents come from
The entry point makes no request; the caller obtains each document and passes its bytes in. The statements of financial years beginning after 31 December 2021 are published in the Unternehmensregister, earlier ones in the Bundesanzeiger. Both show a statement only after a security check (a captcha); the Unternehmensregister shows it as HTML and usually also as PDF. A balance sheet a micro company deposited (§ 326 Abs. 2 HGB) is retrieved only for a fee and after registration. Listed issuers publish their ESEF reports on their own sites as well.
The Unternehmensregister's terms of use allow copying the statements it makes accessible by print or as a file (§ 3 Abs. 3) and mark them with the origin note "Auszug aus dem Unternehmensregister" and the day the document was posted, or for a deposited balance sheet the day it was deposited (§ 3 Abs. 5). A filing from the Unternehmensregister therefore needs publishedOn, and notice gives the note, so that a kept copy keeps it.
Before You Start
- No stock or fundamentals providers are pre-registered. You must register them before calling the services.
MarketstackProviderrequires an API key.SecEdgarProviderrequires a validUser-Agentstring in the formatCompany Name [email protected].CoinGeckoProviderworks without an API key, but a key gives you better rate limits.HandelsregisterClientneeds a gate. The portal's terms allow at most 60 searches or retrievals of entities per hour from one installation, so share oneSlidingWindowGate(or a gate of your own) among every client that reaches the portal from the same address.- The portal's terms allow single lookups for information purposes. Systematic retrieval to build or update a copy of the registers is not allowed, and neither is publishing the data under the name "Handelsregister" (§ 8 Abs. 2 HGB).
StockDataServicedefaults to a 24 hour price cache. If you want fresher quote caching, passcache.priceTTLexplicitly or useStockPriceServicedirectly.
Quick Start
Combined Stock Data
Use StockDataService when you want one call that returns both the latest price and SEC fundamentals.
import {
MarketstackProvider,
SecEdgarProvider,
StockDataService,
} from '@fin.cx/opendata';
const stocks = new StockDataService({
cache: {
priceTTL: 60_000,
},
});
stocks.registerPriceProvider(
new MarketstackProvider(process.env.MARKETSTACK_API_KEY!)
);
stocks.registerFundamentalsProvider(
new SecEdgarProvider({
userAgent: 'Acme Inc. [email protected]',
})
);
const apple = await stocks.getStockData('AAPL');
console.log({
ticker: apple.ticker,
price: apple.price.price,
provider: apple.price.provider,
companyName: apple.fundamentals?.companyName,
marketCap: apple.fundamentals?.marketCap,
priceToEarnings: apple.fundamentals?.priceToEarnings,
fetchedAt: apple.fetchedAt,
});You can also batch this:
const batch = await stocks.getBatchStockData(['AAPL', 'MSFT', 'NVDA']);Price-Only Stocks And Intraday Data
Use StockPriceService if you want smarter per-request caching and direct access to current, historical, and intraday request types.
import { MarketstackProvider, StockPriceService } from '@fin.cx/opendata';
const prices = new StockPriceService();
prices.register(new MarketstackProvider(process.env.MARKETSTACK_API_KEY!));
const current = await prices.getPrice({ ticker: 'AAPL' });
const historical = await prices.getData({
type: 'historical',
ticker: 'AAPL',
from: new Date('2025-01-01'),
to: new Date('2025-01-31'),
sort: 'DESC',
});
const intraday = await prices.getData({
type: 'intraday',
ticker: 'AAPL',
interval: '1hour',
limit: 24,
});
console.log({
live: current.price,
candles: intraday.length,
historicalRows: historical.length,
});Supported stock request shapes:
{ type: 'current', ticker, exchange? }{ type: 'batch', tickers, exchange? }{ type: 'historical', ticker, from, to, exchange?, sort?, limit?, offset? }{ type: 'intraday', ticker, interval, exchange?, limit?, date? }
StockPriceService also exposes operational helpers like checkProvidersHealth(), getProviderStats(), clearCache(), and getCacheStats().
Crypto Prices With The Same Interface
CoinGeckoProvider plugs into StockPriceService, so crypto lookups use the same request model as stock lookups.
import { CoinGeckoProvider, StockPriceService } from '@fin.cx/opendata';
const prices = new StockPriceService({ ttl: 30_000 });
prices.register(new CoinGeckoProvider(process.env.COINGECKO_API_KEY));
const btc = await prices.getPrice({ ticker: 'BTC' });
const top = await prices.getPrices({ tickers: ['BTC', 'ETH', 'SOL'] });
console.log(btc.price, top.map((item) => item.ticker));You can also pass CoinGecko ids like bitcoin or ethereum instead of ticker symbols.
Fundamentals Only
Use FundamentalsService when you only care about SEC filing data.
import { FundamentalsService, SecEdgarProvider } from '@fin.cx/opendata';
const fundamentals = new FundamentalsService();
fundamentals.register(
new SecEdgarProvider({
userAgent: 'Acme Inc. [email protected]',
})
);
const apple = await fundamentals.getFundamentals('AAPL');
console.log({
companyName: apple.companyName,
revenue: apple.revenue,
netIncome: apple.netIncome,
earningsPerShareDiluted: apple.earningsPerShareDiluted,
filingDate: apple.filingDate,
});GLEIF Level 1 data (@fin.cx/opendata/gleif)
GleifClient discovers the current GLEIF Golden Copy publication, streams its LEI-CDF 3.1 ZIP/CSV records, or looks up one LEI through GLEIF's public JSON:API. It keeps no files or data. fetch is injectable; every request has a signal and a time limit, with no retries. Breaking a for await loop cancels the download. Only publication metadata and single-record JSON responses are collected (at most 2 MiB); the ZIP and CSV are read incrementally with backpressure and a default 1,048,576-character limit per CSV row. A completed stream checks the compressed byte and unfiltered record counts against the publication descriptor.
import {
GleifClient,
courtFromRegistrationAuthority,
registrationAuthorityFromCourt,
parseRegistrationAuthorityEntityId,
registerReferenceFromRegistrationAuthority,
} from '@fin.cx/opendata/gleif';
const gleif = new GleifClient({ userAgent: 'Example Books (+https://example.com/contact)' });
const publish = await gleif.getLatestPublish();
// Save this descriptor in your own store to identify the import's publication.
for await (const record of gleif.readGoldenCopy(publish.fullFile, { country: 'DE' })) {
console.log(record.lei, record.legalName.name, record.registrationAuthority);
}
// Or discover and stream in one call. Other kinds: GoldenCopy, LastDay, LastWeek, LastMonth.
for await (const record of gleif.streamRecords({ kind: 'IntraDay', jurisdiction: 'DE' })) {
console.log(record.lei, record.lastUpdateDate);
}
const record = await gleif.lookup('529900W18LQJJN6SJ336');
const court = courtFromRegistrationAuthority('RA000222'); // { code: 'H1101', name: 'Bremen' }
const raCode = registrationAuthorityFromCourt('H1101'); // RA000222
const number = parseRegistrationAuthorityEntityId('HRB 35230HB'); // { type: 'HRB', number: '35230 HB' }
const reference = registerReferenceFromRegistrationAuthority('RA000222', 'HRB 35230HB');
// IRegisterReference: { court, type: 'HRB', number: '35230 HB' }The exact yielded/lookup contract is IGleifRecord:
interface IGleifRecord {
lei: string;
legalName: IGleifName;
otherNames: IGleifName[];
transliteratedOtherNames: IGleifName[];
legalForm?: { elfCode?: string; other?: string };
legalAddress: IGleifAddress;
headquartersAddress: IGleifAddress;
jurisdiction?: string;
entityStatus: string;
entityCategory?: string;
registrationAuthority?: { id?: string; otherId?: string; entityId?: string };
registrationStatus: string;
initialRegistrationDate: string;
lastUpdateDate: string;
}IGleifName carries name, optional language, and optional published other-name type. IGleifAddress carries addressLines, city, country, and optional language, region, postalCode, addressNumber, addressNumberWithinBuilding and mailRouting. Empty optional source fields are absent, other-name lists are arrays, and dates remain the published ISO 8601 strings, including fractional seconds. ELF codes remain codes; no legal-form designation is invented. Status and category strings remain as published. country compares the legal address's ISO country, jurisdiction compares the exact legal jurisdiction; when both are supplied, both must match. Filtering happens after parsing each record, before yielding it.
The full file and delta descriptors are IGleifGoldenCopyFile (url, format: 'csv', cdfVersion: 'LEI_3.1', kind, recordCount, size). getLatestPublish() returns IGleifPublish (publishDate, fullFile, deltaFiles). Options include fetch, goldenCopyBaseUrl, restBaseUrl, userAgent and timeoutMs (default 30 minutes per request, including consumer time). Read calls accept country, jurisdiction, signal and maxRowChars. lookup() and discovery accept signal. parseGleifRestRecord(json) maps an already obtained JSON:API response without fetching. GleifError exposes code and, for HTTP errors, httpStatus.
GLEIF publishes full files three times daily and deltas for the preceding 8 hours (IntraDay), 24 hours (LastDay), 7 days (LastWeek) and 31 days (LastMonth). Level 1 deltas contain the current version of new or changed LEI records. They contain no per-record change kind: this module does not infer insert/update/delete events. The deletion flag described in the manual concerns Level 2 relationships, which this module does not read. A delta overlaps earlier windows; callers own their import checkpoint and idempotent persistence. Discovery selects the CSV format from GLEIF's descriptors rather than constructing file download URLs. Unsupported CDF versions or missing columns fail explicitly.
The generated German RA ↔ XJustiz crosswalk uses the official GLEIF Registration Authorities List v1.9, 2026-09-30. Its 177 German authorities contain 147 mapped courts and 30 non-court authorities with unmapped reasons. Of opendata's 150 court codes, 147 are mapped; Y1303 (Arnstadt Zweigstelle Ilmenau), Y1208 (Pößneck Zweigstelle Bad Lobenstein) and R1105 (Langenfeld) have no distinct RA in that version. No parent/successor mapping is inferred. germanRegistrationAuthorities, gleifUnmappedCourts and gleifRegistrationAuthoritiesSource expose the list and its provenance. Unknown/unmapped codes and unparseable register identifiers return undefined. Register identifiers must state their type (HRA, HRB, GnR, PR, VR or GsR); leading zeros and location suffixes are preserved.
Reproduce the code list offline with tsx scripts/generate.gleif-crosswalk.ts, or verify it with --check. --refresh fetches only the small official RA CSV and updates the committed German subset and generated list. For a new RA-list version, update the version/date constants in the generator, review new names and mapping ambiguity, then refresh and run the exhaustive tests. The generator never contacts a register portal.
Official sources, read on 2026-10-13:
- Golden Copy and Delta Files overview and specification/user manual v2.2, especially §§1.5–1.6 and 2.2: ZIP compression, formats, delta windows and API discovery at
https://goldencopy.gleif.org/api/v2/golden-copies/publishes/latest.leidata-preview.gleif.orgis the documented legacy domain for the same API. - LEI-CDF 3.1 format and official XSD. CSV uses the published dotted columns, including
Entity.LegalName, numbered other/transliterated names,Entity.RegistrationAuthority.RegistrationAuthorityID/RegistrationAuthorityEntityID,Entity.LegalForm.EntityLegalFormCode, addresses and registration dates. The parser reads the header rather than assuming column positions; fixtures preserve all 338 published columns. - GLEIF REST API documentation:
GET https://api.gleif.org/api/v1/lei-records/{lei}, JSON:APIdata.attributes.entityanddata.attributes.registration, includingregisteredAt.idandregisteredAs. - RA-list publication page and v1.9 CSV.
- GLEIF LEI Data Terms of Use: data available through its access service are provided under CC0 1.0 Universal.
Fixtures are 100 records each from the 2026-10-13 00:00 full and intra-day CSVs, publication descriptors, and the REST documentation's single-record response. Tests make no GLEIF requests; ZIPs are created from these small CSV samples in memory. Provenance is in test/fixtures/gleif/readme.md. Full-population imports and GLEIF request-rate limits have not been verified; only bounded live samples, documented shapes and offline fixtures were checked.
Register Lookups (Handelsregister)
HandelsregisterClient speaks to the register portal of the federal states over plain HTTP, the way a browser would: it opens the search form, sends the search, reads the result list and, for one entry, downloads the structured register content (SI, an XJustiz XML file) and parses it.
import {
HandelsregisterClient,
HandelsregisterError,
SlidingWindowGate,
findRegisterCourt,
formatRegisterReference,
} from '@fin.cx/opendata/handelsregister';
// one gate for the whole installation: 60 searches or retrievals per hour
const gate = new SlidingWindowGate();
const register = new HandelsregisterClient({
gate,
userAgent: 'Example Books (+https://example.com/contact)',
});
// by name: every row the portal lists, in its order, never narrowed down to one
const found = await register.search({ name: 'Task Venture Capital', seat: 'Bremen' });
const choices = found.matches.map((match) => ({
reference: formatRegisterReference(match.reference), // 'Amtsgericht Bremen HRB 35230 HB'
name: match.name,
sheet: match.sheetStatus, // 'current' or 'closed'
formerNames: match.formerNames,
}));
if (found.truncated) {
// the portal stops at 100 hits: add the seat, the court or the register number
}
// one register sheet, with its structured content
const court = findRegisterCourt('Amtsgericht Bremen');
if (court) {
const entry = await register.getEntry({ courtCode: court.code, type: 'HRB', number: '35230 HB' });
// entry.rawStructuredContent is null by default: no SI XML copy is returned.
const content = entry.structuredContent; // null when the portal offers none for this entry
if (content) {
const facts = {
name: content.name,
legalForm: content.legalForm?.short, // 'GmbH'
address: content.address,
status: content.status?.state, // 'active', 'deleted', 'in_liquidation', 'insolvent', 'merged'
representatives: content.representatives, // [{ name, function, functionCode, kind }]
retrievedAt: entry.retrievedAt,
notice: entry.notice.en, // the portal's note: the structured content is not binding
};
}
}
// every failure is typed
try {
await register.getEntry({ courtCode: 'H1101', type: 'HRB', number: '35230' });
} catch (error) {
if (error instanceof HandelsregisterError && error.code === 'rate_limited') {
const nextPossibleAt = new Date(error.retryAt ?? Date.now());
}
}Searches take a name or a register number:
{ name, match?: 'all' | 'any' | 'exact', seat?, type?, courtCode?, includeClosed? }{ type, number, courtCode?, includeClosed? }wheretypeisHRA,HRB,GnR,PR,VRorGsRandnumbermay carry the location suffix (999924 B,999928 NM). The portal is searched by the digits, so rows with any suffix come back. WithoutcourtCode, every court's entry with that number is returned.
getEntry({ courtCode, type, number }) identifies one sheet. It includes closed sheets, compares the location suffix when you give one, and never picks one of several: it fails with not_found or ambiguous and lists the candidates.
SI XML copies are opt-in. getEntry(reference, { structuredContentCopy }) accepts TStructuredContentCopy = 'none' | 'minimised' | 'original'. The default is 'none': the result holds parsed SI only and excludes natural-person birth details and private addresses. Choose 'minimised' when retaining an XML copy; choose 'original' explicitly when the unmodified portal payload is needed. The client constructor also accepts structuredContentCopy as a default for its getEntry calls, with the same default 'none'. A per-call choice overrides it, including 'none' to disable a client-level copy default. None of these choices makes an extra portal request.
What counts against the 60 per hour
| Call | Counted requests | Also loaded |
| --- | --- | --- |
| search() | 1 search | the search form and the result list |
| getEntry() | 1 search, plus 1 retrieval when the portal offers structured content | the search form and the result list |
The client asks the gate before each counted request and before that step sends anything, so a refusal costs no traffic. A slot taken for a step that then fails before its request is sent stays taken; the count errs on the safe side. The client never caches and never writes to the console. Every call opens its own visit to the portal and forgets it afterwards.
The client repeats a request in one case only, and only once: when the portal closed the connection without any answer. The portal's front end, Apache httpd before its application servers, closes a kept-alive connection that way when it could not read its back end's answer, so that the client sends the request again, as a browser does. A search or retrieval sent once more asks the gate again and counts again, so countedRequests can be one higher than the table says. Closed a second time, the call ends in dropped.
A refusal of SlidingWindowGate is a HandelsregisterError with the code rate_limited and retryAt, the moment the next request is possible. Like every error of the client it carries the refused step (search or structured_content) and the countedRequests made before it: a getEntry() refused at the retrieval has already counted its search. A gate of your own may refuse with any error; a HandelsregisterError gains the step and the count the same way, and any other error reaches the caller unchanged.
What a result holds
IRegisterMatch, one row of the result list:reference(court with code and name, register type, number with suffix),formerCourt,federalState,name,seat,sheetStatus(current,closedorother, with the portal's word insheetStatusText),formerNamesand thedocumentsthe portal offers.IRegisterEntry.rawStructuredContent, the requested SI copy:IRegisterRawContent = { bytes: Uint8Array, mediaType: string, sha256: string, minimised: boolean, originalSha256: string }. It shares the base raw-document fields ofIFinancialStatement.rawand adds copy provenance. It is non-null exactly when the effectivestructuredContentCopyis'minimised'or'original'andstructuredContentis non-null; otherwise it is null.mediaTypepreserves the portal's Content-Type, including parameters and case (or''when absent);sha256is lowercase hex of exactly the returnedbytes.originalSha256is lowercase hex of the portal's original bytes on both kinds, preserving provenance without retaining original XML in a minimised result.minimisedis true for a minimised copy (even if no elements needed removal), false for an original.- An explicitly requested
'original'copy can contain birth dates, birth places and private addresses. Callers who persist it own its retention, rewriting and deletion, including suppressions under GDPR Articles 17 and 21; the client stores nothing. The default'none'prevents whole-result persistence from silently adding these fields. Names and business details remain in parsed SI and can still be personal data. IRegisterStructuredContent, the parsed SI:entityKind,name,legalForm(XUnternehmen code with short and long designation),seat, the businessaddress,register,status(XJustiz status of the legal entity),lastEntryOn,representativesandrepresentationStated,producedAt,binding: falseand the portal'snotice.entityKindclassifies the registered entity, not its representatives or the XML element's name. Natural-person business forms return'person'even when XJustiz uses an<organisation>element: commercial sole traders (410000), registered sole traders (411000,411100in Germany,411200abroad), farmers/foresters (422000), other economic activity of a natural person (428100), foreign economic activity of a natural person (490000) and foreign sole traders under EU (491000) or non-EU law (492000). These entries belong to XUnternehmen's category "Wirtschaftliche Tätigkeit einer natürlichen Person" (400000); the internalnaturalPersonmarker stays in the code table and never appears in the returnedlegalForm. Partnerships such as OHG and KG remain'organisation', including those with natural-person partners.- Otherwise, including unknown, empty or missing legal forms,
entityKindretains the structural rule: abasisdatenRegister/rechtstraegerholder or an entity participant described as<organisation>means'organisation'; without either it is'person'. This fallback does not confirm that an unknown form is an organisation, so consumers must account for it when applying privacy rules. Search results and the GLEIF RA ↔ court crosswalk do not provideentityKind; read SI for this classification. - A closed sheet ("Geschlossenes Registerblatt") means the entry was deleted, or its sheet was closed when the entity moved to another court, changed its legal form or merged. The result list does not say which, and the portal offers no structured content for it.
- The structured content states no date for the status itself;
lastEntryOnis the date of the latest entry on the sheet. - Of natural-person representatives the parser reads the name and the function only. Birth dates, birth places, places of residence and private addresses never enter the parsed result. A registered natural person's business address can appear in
address. - Codes are decoded with tables taken from the code lists on XRepository on 2026-09-23. A code the tables do not know is kept as a code, without a designation.
What can happen
| Case | Result |
| --- | --- |
| No match | search() returns no matches; getEntry() fails with not_found |
| Several matches in different courts | every row, in the portal's order |
| More than 100 hits | the first 100 and truncated: true |
| Renamed entity | the current name, earlier names in formerNames |
| Deleted or moved entity | sheetStatus: 'closed', no structured content |
| Dissolved or insolvent entity | status.state in_liquidation or insolvent, and the liquidators among the representatives |
| Branch of a company under foreign law | its foreign legal form and seat, and its permanent representatives |
| Partnership, cooperative, association, civil-law partnership | register types PR, GnR, VR, GsR |
| Number without suffix that fits several sheets | ambiguous with the candidates |
| Suffix that fits no sheet | not_found with the sheets of the same number |
| Structured content of another sheet | unexpected_response |
| Portal answers with an HTTP error (blocked address, overload) | refused with status |
| Portal shows its notice that it refuses retrievals from this address | refused with its words in portalMessage |
| Portal too slow, unreachable, or call aborted | timeout, unreachable, aborted |
| Portal closes the connection without answering, also when the request is sent once more | dropped |
| Portal changed its pages or answered in another language | unexpected_response, with the portal's own message in portalMessage when it showed one |
| Structured content that cannot be read | invalid_structured_content |
Errors of the client carry countedRequests and the step that failed, so you can say whether a request may have counted even when its answer did not arrive. Error messages contain no register data.
Parsing without a request
import { parseStructuredContent } from '@fin.cx/opendata/handelsregister';
// an SI file a person downloaded from the portal themselves
const content = parseStructuredContent(xmlText);parseStructuredContent also accepts UTF-8 Uint8Array bytes, so parseStructuredContent(entry.rawStructuredContent.bytes) reproduces the parsed SI without another request when a copy was requested and SI is available. Invalid UTF-8, malformed XML, DTDs and other message roots fail with invalid_structured_content.
Minimising an SI copy
import { parseStructuredContent } from '@fin.cx/opendata/handelsregister';
const entry = await register.getEntry(
{ courtCode: 'H1101', type: 'HRB', number: '35230 HB' },
{ structuredContentCopy: 'minimised' },
);
if (entry.rawStructuredContent) {
const minimised = entry.rawStructuredContent;
// { bytes, mediaType, sha256, minimised: true, originalSha256 }
// Persist under minimised.sha256 if needed; originalSha256 identifies the source payload.
const content = parseStructuredContent(minimised.bytes); // same as entry.structuredContent
}The minimised copy is the data-minimising choice for provenance and later re-parsing. The public helper await minimiseStructuredContent(originalCopy) also minimises an already obtained IRegisterRawContent without a request. It preserves originalSha256, including when given an already minimised copy. It removes these entire XJustiz element subtrees, identified by namespace http://www.xjustiz.de and local name, regardless of prefix:
natuerlichePerson/geburt, includinggeburtsdatum,geburtsortand any other birth details inside it;natuerlichePerson/anschriftunlessanschriftstyp/codeis003(Dienst-/Geschäftsanschrift) or021(Lage der Geschäftsräume). This removes residence/private addresses, including type017, and addresses with unknown, empty or missing type codes.
Business addresses are retained, including those of a registered natural person. Names, functions and every other element remain; declarations, whitespace, comments, processing instructions, CDATA and prefixes outside removed subtrees retain their original bytes. The input is untouched, mediaType is retained, minimised is set to true and sha256 is computed from the copy's own bytes. The parser and helper share the exclusion policy. The copy is valid XML but is not asserted to validate against the original XJustiz schema after elements are removed. It still contains personal data such as names and is not an anonymisation or a complete GDPR suppression mechanism: callers also own deletion or rewriting of persisted minimised copies and any personal data in retained free text.
Printed court names
findRegisterCourt() takes a court's code or its name as the portal or a business prints it: AG Bremen, Amtsgericht Bremen – Registergericht, Registergericht Bremen, Handelsregister B des Amtsgerichts Bremen, Amtsgericht Frankfurt a. M., Frankfurt/O., Kempten for Kempten (Allgäu), Freiburg i. Br., and AG Berlin or Charlottenburg for the register court of Berlin. Umlauts may be spelled out (Muenchen). It returns a court only when exactly one fits: Frankfurt alone could be either Frankfurt am Main or Frankfurt (Oder) and names no court.
Public Holidays
import {
getPublicHolidays,
isWorkingDay,
nextWorkingDay,
workingDayOnOrAfter,
} from '@fin.cx/opendata/holidays';
getPublicHolidays(2026, 'DE-NI');
// [{ date: '2026-01-01', id: 'neujahr', name: 'Neujahr', state: 'DE-NI', scope: 'state',
// legalBasis: '§ 2 Abs. 1 NFeiertagsG' }, …, { date: '2026-10-31', id: 'reformationstag', … }, …]
isWorkingDay('2026-06-04', 'DE-HE', { regionalHolidays: 'ignore' }); // false: Fronleichnam
nextWorkingDay('2026-04-02', 'DE-BY', { regionalHolidays: 'ignore' }); // '2026-04-07'
// § 193 BGB: a deadline that ends on a Saturday, Sunday or holiday ends on the next working day
workingDayOnOrAfter('2026-05-14', 'DE-BY', { regionalHolidays: 'ignore' }); // '2026-05-15'States are ISO 3166-2 codes (DE-BY); findGermanState('Bayern') and findGermanState('BY') find one. Dates are plain calendar dates written YYYY-MM-DD, so no time zone can move them.
Bank working days
A bank working day (Bankarbeitstag) is neither a Saturday, nor a Sunday, nor a statutory public holiday, nor 24 or 31 December. On those two days the Deutsche Bundesbank is closed although they are no statutory holidays; its list of holidays names them as its additional closing days. German social insurance counts its due dates in these days, with the holidays of the state where the collecting agency (Einzugsstelle) has its seat: contributions are due on the third-last bank working day of the month, and the contribution statement has to be with the collecting agency at the start of the fifth-last.
import { isBankWorkingDay, nthLastBankWorkingDay } from '@fin.cx/opendata/holidays';
isBankWorkingDay('2026-12-24', 'DE-HH', { regionalHolidays: 'ignore' }); // false: the Bundesbank is closed
nthLastBankWorkingDay('2026-12', 3, 'DE-HH', { regionalHolidays: 'ignore' }); // '2026-12-28'
nthLastBankWorkingDay('2026-12', 5, 'DE-HH', { regionalHolidays: 'ignore' }); // '2026-12-22'
// Reformationstag 2025 was a Friday: a holiday in Niedersachsen, a working day in Bayern
nthLastBankWorkingDay('2025-10', 3, 'DE-NI', { regionalHolidays: 'ignore' }); // '2025-10-28'
nthLastBankWorkingDay('2025-10', 3, 'DE-BY', { regionalHolidays: 'ignore' }); // '2025-10-29'Months are written YYYY-MM. The position counts from the end of the month, 1 being the last bank working day; a position the month does not have ends in invalid_position. The tests hold the results against the due dates the collecting agencies published for 2025 and 2026.
Regional holidays
Four holidays apply only in part of a state:
| Holiday | State | Where | Legal basis | | --- | --- | --- | --- | | Augsburger Friedensfest (8 August) | Bayern | Stadt Augsburg | Art. 1 Abs. 2 FTG | | Mariä Himmelfahrt (15 August) | Bayern | municipalities with a predominantly Catholic population, as the Bayerisches Landesamt für Statistik determines them after the last census | Art. 1 Abs. 1 Nr. 2, Abs. 3 FTG | | Fronleichnam | Sachsen | the municipalities listed in the annex of the FronleichnamsVO (former districts Bautzen, Hoyerswerda and Kamenz), after a merger only the respective part of the municipality | § 1 Abs. 1 SächsSFG, § 1 FronleichnamsVO | | Fronleichnam | Thüringen | the parts of Thüringen where Fronleichnam was a statutory holiday in 1994 | § 2 Abs. 2, § 10 Abs. 1 ThürFGtG |
getPublicHolidays() lists them with scope: 'regional' and the region they apply to. The calendar holds no list of these municipalities: the Bavarian list follows the census and the Saxon and Thuringian ones name parts of municipalities, so a place cannot be matched to them reliably by a key. Every working-day question therefore says how to treat them:
{ regionalHolidays: 'count' }treats them as holidays. A period then never ends on a day that is a holiday at the place in question, at the price of ending one day later where it is not. Use it when the place is unknown.{ regionalHolidays: 'ignore' }treats them as working days, when the place is known to lie outside that part.
What the calendar covers
- The statutory public holidays of the 16 federal states from 2018 on, and 3 October from Art. 2 Abs. 2 of the Unification Treaty. Every change since 2018 applies from its year: Reformationstag in Bremen, Hamburg, Niedersachsen and Schleswig-Holstein (2018), Frauentag in Berlin (2019) and Mecklenburg-Vorpommern (2023), Weltkindertag in Thüringen (2019), and the one-off holidays of Berlin on 8 May 2020, 8 May 2025 and 17 June 2028. Years before 2018 end in
HolidayCalendarErrorwith the codeunsupported_year. - Later years follow the laws as they stood when this version read them (
holidayLawsReadOn). Several states allow their government to declare one-off holidays by ordinance; one declared after that day needs a new version. - The Easter-based holidays follow Easter Sunday of the Gregorian calendar; Buß- und Bettag (Sachsen) is the Wednesday before the last Sunday before the first Sunday of Advent.
- The laws of Berlin and Hessen also count every Sunday as a public holiday, and Brandenburg names Ostersonntag and Pfingstsonntag. The calendar lists the Sundays Brandenburg names; for working days every Sunday is excluded anyway.
- Religious holidays that protect only church services or give employees time off (for example Fronleichnam outside its region in Sachsen, § 3 SächsSFG) and memorial days (Gedenktage) are no statutory public holidays and are not listed.
API At A Glance
StockPriceService
register(provider, config?)unregister(providerName)getPrice({ ticker })getPrices({ tickers })getData(request)checkProvidersHealth()getProviderStats()clearCache()getCacheStats()
FundamentalsService
register(provider, config?)getFundamentals(ticker)getBatchFundamentals(tickers)getData(request)enrichWithPrice(fundamentals, price)checkProvidersHealth()getProviderStats()clearCache()
StockDataService
registerPriceProvider(provider, config?)registerFundamentalsProvider(provider, config?)getPrice(ticker)getPrices(tickers)getFundamentals(ticker)getBatchFundamentals(tickers)getStockData(ticker | request)getBatchStockData(tickers | request)checkProvidersHealth()getProviderStats()clearCache()
HandelsregisterClient (@fin.cx/opendata/handelsregister)
new HandelsregisterClient({ gate, portalUrl?, userAgent?, requestTimeoutMs?, maxResponseBytes?, now?, structuredContentCopy? })search(query, { signal? })getEntry({ courtCode, type, number }, { signal?, structuredContentCopy? })
SlidingWindowGate
new SlidingWindowGate({ limit?, windowMs?, now? })acquire({ kind })remaining()nextAllowedAt()
Register helpers
parseStructuredContent(xmlTextOrUtf8Bytes)minimiseStructuredContent(rawContent),IRegisterRawContent,TStructuredContentCopyandIRegisterEntryCallOptionsparseSearchResults(html)registerCourtsfindRegisterCourt(codeOrName)parseRegisterReferenceText(text)parseRegisterNumber(text)formatRegisterReference(reference)structuredContentNotice
Holiday calendar (@fin.cx/opendata/holidays)
getPublicHolidays(year, state)getPublicHolidaysOn(date, state)isPublicHoliday(date, state, { regionalHolidays })isWorkingDay(date, state, { regionalHolidays })nextWorkingDay(date, state, { regionalHolidays })workingDayOnOrAfter(date, state, { regionalHolidays })isBankWorkingDay(date, state, { regionalHolidays })nthLastBankWorkingDay(month, position, state, { regionalHolidays })easterSunday(year)germanStates,findGermanState(codeOrName)holidayCalendarFirstYear,holidayLawsReadOnHolidayCalendarErrorwith the codesinvalid_date,invalid_month,invalid_position,unsupported_year,unknown_stateandinvalid_options
VAT ID (@fin.cx/opendata/vatid)
EvatrClientwithconfirm(input);ViesClientwithcheck(input)splitVatId(value),vatIdCountryCodes,evatrStatusCodes,evatrBaseUrl,viesBaseUrl,defaultVatIdTimeoutMsVatIdErrorwith the codesinvalid_input,vat_id_malformed,requester_invalid,not_permitted,qualified_limit_reached,unavailable,member_state_unavailable,busy,blocked,unreachable,timeout,abortedandunexpected_answer
Base rate (@fin.cx/opendata/baserate)
getBaseRate(date)splitByBaseRate(from, to)baseRatePeriods,baseRateFirstDay,baseRatesReadOn,baseRateSourceBaseRateErrorwith the codesinvalid_date,invalid_range,before_first_periodandnot_published
Consumer price index (@fin.cx/opendata/cpi)
BundesbankCpiClientwithgetMonthly({ from, to? }),getAnnual({ from, to? })andgetMonth(month)cpiChangePercent(old, new),cpiChangePoints(old, new),cpiThresholds(reference, percent),cpiFirstReaching(values, reference, percent, options),cpiFirstReachingPoints(values, reference, points, options),cpiScaleAmount(amountCents, old, new, options?)bundesbankCpiBaseUrl,bundesbankVpiMonthlyKey,bundesbankVpiAnnualKey,bundesbankVpiFirstMonth,bundesbankVpiFirstYear,destatisLicence,defaultCpiTimeoutMsCpiErrorwith the codesinvalid_input,before_first_period,not_published,base_mismatch,series_withdrawn,busy,unavailable,unreachable,timeout,abortedandunexpected_answer
Financial statements (@fin.cx/opendata/financialstatements)
readFinancialStatement({ filing, bytes, mediaType, now? })parseStatementDocument(bytes, mediaType)parseEsefPackage(zipBytes),parseEsefReport(xhtmlBytes, reportPath?)unternehmensregisterNotice(publishedOn, deposited?),unternehmensregisterOrigin,maxStatementBytes,maxPdfPages,maxEsefFacts,maxEsefPackageEntries- the types
IFiling,IFinancialStatement,IHgbStatement,IEsefStatement,IKeyFigures,IStatementPositionand their parts FinancialStatementErrorwith the codesinvalid_input,unsupported_media_type,too_large,invalid_package,unreadable,no_textandunparseable
Data Sources
The package ships no market or register data. Prices and fundamentals come at run time from the providers you register (Marketstack, CoinGecko, SEC EDGAR), and register entries come at run time from the register portal of the federal states (handelsregister.de), one lookup at a time. Financial statements come from the caller: @fin.cx/opendata/financialstatements reads the documents it is given and fetches none.
The GLEIF module also carries the generated RA ↔ court crosswalk described above. The package carries two other kinds of data: the holiday rules of @fin.cx/opendata/holidays (ts/holidays/data.holidays.ts), described at the end of this section, and the code tables that @fin.cx/opendata/handelsregister uses to decode the structured register content (ts/handelsregister/data.codelists.ts). The code tables are official code lists, published on XRepository (https://www.xrepository.de), and were taken on 2026-09-23:
| Table | Code list | Issued by |
| --- | --- | --- |
| legalFormLabels | urn:xoev-de:xunternehmen:codeliste:rechtsformen_2.4 (Rechtsformen) | XUnternehmen |
| roleLabels | urn:xoev-de:xjustiz:codeliste:gds.rollenbezeichnung_3.6 (GDS.Rollenbezeichnung) | BLK-AG IT-Standards in der Justiz |
| entityStatusLabels | urn:xoev-de:xjustiz:codeliste:reg.status-rechtstraeger_2.0 (REG.Status_Rechtstraeger) | BLK-AG IT-Standards in der Justiz |
| stateKeyToIso | urn:xoev-de:bund:bfj:codeliste:bfj.staat_7.0 (BfJ Staat) | Bundesamt für Justiz, Bonn |
| registerCourts | the register courts of the portal's search form, with their codes from urn:xoev-de:xjustiz:codeliste:gds.gerichte | BLK-AG IT-Standards in der Justiz |
Codes and designations are reproduced as published, without changes. The tables hold a selection of each list, never an edited entry:
legalFormLabels: the concrete legal forms that one of the justice registers can carry, with the short and the long designation;roleLabels: the 71 roles that occur in register content;entityStatusLabels: the whole list;stateKeyToIso: the key and the ISO 3166-1 alpha-2 code of each state or territory that has one;registerCourts: the 150 register courts as the portal's search form named them on the same day.
A later version of a code list may add codes. The parser keeps a code it does not know as a code, without a designation, and never guesses one.
The holiday calendar is derived from the holiday laws of the federal states, read on 2026-09-24 in their official portals, from Art. 2 Abs. 2 of the Unification Treaty for 3 October, and it answers the question of § 193 BGB:
| State | Law | Provision | | --- | --- | --- | | all | Einigungsvertrag | Art. 2 Abs. 2 (Tag der Deutschen Einheit) | | Baden-Württemberg | Feiertagsgesetz (FTG) | § 1 | | Bayern | Feiertagsgesetz (FTG) | Art. 1 | | Berlin | Gesetz über die Sonn- und Feiertage | § 1 Abs. 1 | | Brandenburg | Feiertagsgesetz (FTG) | § 2 Abs. 1 | | Bremen | Gesetz über die Sonn-, Gedenk- und Feiertage | § 2 Abs. 1 | | Hamburg | Feiertagsgesetz | § 1 | | Hessen | Hessisches Feiertagsgesetz (HFeiertagsG) | § 1 Abs. 1 | | Mecklenburg-Vorpommern | Feiertagsgesetz (FTG M-V) | § 2 Abs. 1 | | Niedersachsen | Niedersächsisches Gesetz über die Feiertage (NFeiertagsG) | § 2 Abs. 1 | | Nordrhein-Westfalen | Gesetz über die Sonn- und Feiertage (Feiertagsgesetz NW) | § 2 Abs. 1 | | Rheinland-Pfalz | Feiertagsgesetz (LFtG) | § 2 Abs. 1 | | Saarland | Feiertagsgesetz (SFG) | § 2 Abs. 1 | | Sachsen | Sächsisches Sonn- und Feiertagsgesetz (SächsSFG), FronleichnamsVO | § 1 Abs. 1; § 1 and annex | | Sachsen-Anhalt | Gesetz über die Sonn- und Feiertage (FeiertG LSA) | § 2 | | Schleswig-Holstein | Gesetz über Sonn- und Feiertage (SFTG) | § 2 Abs. 1 | | Thüringen | Thüringer Feier- und Gedenktagsgesetz (ThürFGtG) | § 2, § 10 Abs. 1 | | all | Bürgerliches Gesetzbuch | § 193 (Sonntag, Feiertag, Sonnabend) |
The bank working days follow the Deutsche Bundesbank's list "Feiertage in Deutschland im Jahr 2026", read on 2026-10-08, which names 24 December (Heiligabend) and 31 December (Silvester) as its additional closing days ("Zusätzliche geschäftsfreie Tage der Deutschen Bundesbank"), and the collecting agencies' statement that these two days are no bank working days for the due dates of contributions, that a holiday counts by the seat of the collecting agency, and
