@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, and the German consumer price index for index clauses. It is built for programmers who want scriptable APIs and real workflows instead of a thin wrapper around one remote endpoint.
It gives you seven 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 and the working days of § 193 BGB, 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
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/opendata
What 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.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.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.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, 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)
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()gives 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.
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@example.com.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. dev@acme.dev',
})
);
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. dev@acme.dev',
})
);
const apple = await fundamentals.getFundamentals('AAPL');
console.log({
companyName: apple.companyName,
revenue: apple.revenue,
netIncome: apple.netIncome,
earningsPerShareDiluted: apple.earningsPerShareDiluted,
filingDate: apple.filingDate,
});
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' });
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.
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 repeats a request by itself, never caches, and never writes to the console. Every call opens its own visit to the portal and forgets it afterwards.
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.IRegisterStructuredContent, the parsed SI: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.- 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 people the parser reads the name and the function only. Birth dates, places of residence and private addresses never leave it.
- 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 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);
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.
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? })search(query, { signal? })getEntry({ courtCode, type, number }, { signal? })
SlidingWindowGate
new SlidingWindowGate({ limit?, windowMs?, now? })acquire({ kind })remaining()nextAllowedAt()
Register helpers
parseStructuredContent(xml)parseSearchResults(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 })easterSunday(year)germanStates,findGermanState(codeOrName)holidayCalendarFirstYear,holidayLawsReadOnHolidayCalendarErrorwith the codesinvalid_date,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),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
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.
The package carries two 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:
The base rate of @fin.cx/opendata/baserate (ts/baserate/data.baserate.ts) is the table "Basiszinssatz nach § 247 BGB" of the Deutsche Bundesbank (https://www.bundesbank.de/de/bundesbank/organisation/agb-und-regelungen/basiszinssatz-607820), which the Bundesbank publishes under § 247 Abs. 2 BGB, read on 2026-09-27 and reproduced unaltered: every half-year from 1 July 2002, the first row it lists, to the half-year from 1 July 2026 (1,52 %). The rate changes on 1 January and 1 July (§ 247 Abs. 1 Satz 2 BGB); a day after 31 December 2026 answers not_published until a release carries the next row.
The VAT ID clients of @fin.cx/opendata/vatid ask two official services at run time, one request at a time, read on 2026-09-27:
- The BZSt's eVatR REST API (
https://api.evatr.vies.bzst.de/app, OpenAPI athttps://api.evatr.vies.bzst.de/api-docs, version "v:1.2.3.19.886"):POST /v1/abfragewithanfragendeUstid,angefragteUstidand, for a qualified confirmation,firmennameandort(and optionallystrasse,plz). The status codes are those of/v1/info/statusmeldungen, and the country codes those of/v1/info/eu_mitgliedstaaten. The BZSt's page on foreign VAT IDs names the online form (https://www.bzst.de/evatr) and the REST API, and says the former XML-RPC interface is obsolete from 30 November 2025. - The European Commission's VIES REST API (
https://ec.europa.eu/taxation_customs/vies/rest-api, contractswagger_publicVAT.yaml):POST /check-vat-number. Its errors arrive with HTTP 200 andactionSucceed: false, as its test service (/check-vat-test-service) shows. - Not verified: the terms of use of either service, whether eVatR needs a registration (its documentation names none), how many qualified confirmations the BZSt allows per session (
evatr-0008), and the form ofgueltigAb/gueltigBis, which are passed on as the BZSt writes them. The OpenAPI document listsevatr-0008under HTTP 400 and the status list under 403, so the client decides by the code.
The consumer price index of @fin.cx/opendata/cpi is read at run time, one request at a time, from the Deutsche Bundesbank's time series API (https://api.statistiken.bundesbank.de/rest/data/BBDP1/), read on 2026-10-05: the series M.DE.N.VPI.C.A00000..A (monthly) and A.DE.N.VPI.C.A00000..A (annual averages), whose base dimension is left open so that the answer names the current base (BBDP1.M.DE.N.VPI.C.A00000.I20.A, 2020=100). The series begins in January 1991. The Bundesbank names the Statistisches Bundesamt as the source of the figures. The arithmetic follows the Statistisches Bundesamt's "Anleitung für die Berechnung von Schwellenwerten und Veränderungsraten für Wertsicherungsklauseln" (Stand Januar 2024) and its notes on points and percent rules. Every rounding is half away from zero; the Statistisches Bundesamt asks for one decimal without naming a rounding mode, and its examples are consistent with it.
- Not verified: limits on the number of requests to the Bundesbank's API (none is documented) and terms of use specific to it. The Bundesbank's terms for reusing statistics exclude data of third parties, so the figures are used under the Statistisches Bundesamt's licence. Whether the Bundesbank keeps the series of an old base for a while after a change of base year is not known; today the key of 2015=100 answers 404.
Behavior And Caveats
- Historical market data is cached aggressively because it is treated as immutable.
- Intraday data uses interval-aware caching and tries incremental refreshes for recent data.
StockDataServiceis convenience-first.StockPriceServiceis the better low-level choice if you care about request-shape-specific cache behavior.- The register lookup reads the portal's German pages and asks for German explicitly. It depends on the portal's current page structure: when that changes, calls end in
unexpected_responserather than in guessed data. - The structured register content is a non-binding service of the portal; the current printout is binding. Results carry
binding: falseand the portal's note. - There is no hosted backend in this package. Everything is meant to run from your own TypeScript/Node process.
Testing
pnpm test
Some market data tests hit live remote services, so they may depend on network access or optional API credentials. The register lookup is tested against minimal hand-written pages and structured content with synthetic values only; the tests never contact the register portal. The VAT ID clients are tested against the answers the two services gave on 2026-09-27 to their documented test values (test/fixtures/vatid/); the tests never contact them. The consumer price index client is tested against the answers the Bundesbank's API gave on 2026-10-05 and two answers written by hand from them (test/fixtures/cpi/); the tests never contact it.
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 file.
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.