Skip to content

rates.json schema

rates.json is a precomputed snapshot of every IGCP-published monthly base rate and every cohort-anchored annual rate, generated by scripts/build-rates-json.ts and published with the docs site. Python, Java, Excel, and spreadsheet users can fetch it once and reproduce IGCP’s numbers without porting any code.

  • Latest: https://igcp-aforro.primor.me/rates.json
  • Per-release snapshot: https://igcp-aforro.primor.me/v/<calver>/rates.json (e.g. v/2026.420.0/rates.json)

The file is regenerated:

  • after every release (the workflow runs pnpm build:rates-json and deploys the snapshot to Pages);
  • after every Euribor / IGCP base-rate refresh PR is merged via data-refresh.yml.
{
"schemaVersion": 1,
"generatedAt": "2026-04-20T08:00:00Z",
"libraryVersion": "2026.420.0",
"euriborSourceMeta": {
"lastRefreshedAt": "2026-04-19T07:42:11Z",
"source": "Deutsche Bundesbank time-series API",
"sourceUrl": "https://api.statistiken.bundesbank.de/rest/download/BBIG1/...",
"seriesId": "BBIG1.D.D0.EUR.MMKT.EURIBOR.M03.BID._Z"
},
"euribor12mSourceMeta": {
"lastRefreshedAt": "2026-04-19T07:42:11Z",
"source": "Deutsche Bundesbank time-series API",
"sourceUrl": "https://api.statistiken.bundesbank.de/rest/download/BBIG1/...",
"seriesId": "BBIG1.D.D0.EUR.MMKT.EURIBOR.M12.BID._Z"
},
"series": {
"B": { "metadata": { "...": "..." }, "monthlyBaseRates": [], "cohortRates": [] },
"C": { "metadata": { "...": "..." }, "monthlyBaseRates": [], "cohortRates": [] },
"D": { "metadata": { "...": "..." }, "monthlyBaseRates": [], "cohortRates": [] },
"E": { "metadata": { "...": "..." }, "monthlyBaseRates": [], "cohortRates": [] },
"F": { "metadata": { "...": "..." }, "monthlyBaseRates": [], "cohortRates": [] }
}
}

euriborSourceMeta covers the bundled 3M Euribor series; euribor12mSourceMeta covers 12M Euribor used in Série B TBA. The builder only emits months while both series have observations through the required fixingDate (effective horizon is limited by whichever dataset ends earliest).

series.B, series.C, series.D, series.E, and series.F carry the same shape and are populated independently — Série B starts at the first month where full TBA inputs resolve; Série C at the first resolvable month after the subscription floor (typically 2008-02); Série D at the first resolvable month in the bundled Euribor history; Série E in November 2017; and Série F in June 2023.

schemaVersion is bumped whenever a backwards-incompatible change ships, so consumers can pin or assert. Adding a new series under series.<code> is not considered a breaking change.

The static SeriesMetadata for the series — maturityYears (finite years or null for perpetual Série B), subscriptionStartDate, subscriptionEndDate (closed series: B, C, D, and E), minUnits, maxUnits, baseRateClampMinPct, baseRateClampMaxPct, base-rate fields (baseRateSpreadPct on D/E/F; baseRateEuriborMultiplierPct and baseRatePostMeanOffsets on C; B uses TBA with euribor3mAveragingDays: 20), ratesJsonMaxContractYears (caps cohortRates contract-year depth for B), defaultIrsRate, premiumTiers (plus premiumTiersLegacy on C), etc. Identical to what the npm library returns from getSeries('B') through getSeries('F').

One entry per calendar month for which a fixing can be resolved.

{
"month": "2024-03",
"fixingDate": "2024-02-27",
"basePct": "3.892"
}
FieldTypeNotes
monthYYYY-MMCalendar month the rate applies to.
fixingDateYYYY-MM-DDAntepenultimate TARGET2 business day of the previous month.
basePctdecimal stringFinal, post-clamp base rate rounded to 3 decimals. For Série F: rounded mean clamped to [0, 2.5]. For Séries D and E: rounded mean + 1.000 (the +1pp spread), clamped to [0, 3.5]. For Série B: 0.60 × TBA after the rounding sequence in tba.ts.

One entry per anchored quarter, from each subscription month through min(maturity, last published month). For perpetual Série B, the generator applies an explicit metadata.ratesJsonMaxContractYears cap so the artifact stays bounded.

{
"subscribed": "2024-03",
"subscriptionDate": "2024-03-01",
"quarterIndex": 8,
"quarterStartDate": "2026-03-01",
"quarterEndDate": "2026-06-01",
"yearsSinceSubscription": 2,
"basePct": "2.500",
"premiumTierYearsRange": "2-5",
"premiumPct": "0.25",
"annualRatePct": "2.750"
}
FieldTypeNotes
subscribedYYYY-MMCohort identifier; subscriptionDate always resolves to the 1st.
subscriptionDateYYYY-MM-DDAlways <subscribed>-01; see the day-of-month note below.
quarterIndexinteger0-based, counted from subscription.
quarterStartDate / quarterEndDateYYYY-MM-DDAnchored to the subscription day, with end-of-month roll-forward.
yearsSinceSubscriptionintegerWhole anniversary years elapsed at quarterStartDate.
basePctdecimal stringSame format as in monthlyBaseRates.
premiumTierYearsRangestringE.g. "2-5".
premiumPctdecimal stringPremium added to basePct.
annualRatePctdecimal stringComposite annual rate for that cohort x quarter.
  • All percentage fields are decimal strings (e.g. "2.500", not 2.5). This matches the npm library’s public API and avoids float drift across language boundaries.
  • subscribed is a calendar month (always normalised to YYYY-MM-01). IGCP’s anchored-quarter rule keys off the subscription day, so a precomputed table cannot represent every day-of-month cohort without exploding in size. Consumers needing day-precision should use the npm library or replicate the math from monthlyBaseRates plus the premium tiers in metadata.
  • cohortRates rows enumerate every anchored quarter from subscription through min(maturity, last published month) (or, for B, through the configured ratesJsonMaxContractYears cap). quarterEndDate is the next quarter’s quarterStartDate; both follow shiftMonths’ end-of-month roll-forward semantics.
import json, urllib.request
from decimal import Decimal, ROUND_HALF_EVEN
data = json.load(urllib.request.urlopen('https://igcp-aforro.primor.me/rates.json'))
rows = [r for r in data['series']['F']['cohortRates'] if r['subscribed'] == '2024-03']
units = Decimal('1000')
balance = units
irs = Decimal('0.28')
for r in rows:
annual = Decimal(r['annualRatePct']) / Decimal('100')
quarterly = annual / 4
gross = (balance * quarterly).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)
withheld = (gross * irs).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)
balance += gross - withheld
print(balance)