Version v1.
25 series across 15 categories.
Every endpoint is a static file behind a CDN. There is no key, no sign-up and no rate limit, and every response allows any origin, so you can fetch these straight from a browser. Requests are GET only.
| Path | Returns |
|---|---|
/v1/catalog.json | Everything that exists. Start here. |
/v1/series/<id>.json | One series: observations, provenance and licences. |
/v1/csv/<id>.csv | The same series as CSV. The provenance sits in # comments above the data. |
/v1/composite/<category>.json | Every series in a category, aligned to one date axis. This is what the charts read. |
/v1/figures/<category>.pdf, .svg, .png | The category figure. Vector for a journal, 300 dpi otherwise. |
Identifiers come from the catalogue. Read path and csv
off a series and follow them, rather than assembling URLs yourself. That way a
renamed series is a changed link, not a 403.
curl https://evresearch.co.uk/v1/catalog.json curl https://evresearch.co.uk/v1/series/uk-bank-rate.json curl https://evresearch.co.uk/v1/csv/uk-bank-rate.csv curl https://evresearch.co.uk/v1/composite/bonds.json
| Field | Meaning |
|---|---|
version | The API version. Currently v1. |
licences | Every licence any series refers to, keyed by id. The vocabulary for the licences list on a series. |
series | Every series, with its date range, units, licences and links. |
composites | Every category, with the number of series in it. |
gaps | Categories deliberately not covered, and why. An empty shelf on purpose reads differently from an oversight. |
| Field | Meaning |
|---|---|
observations | [[date, value], ...], oldest first. A value may be null where the source has a gap; the date is still listed. |
start | First observation date. Currently 1694-11-01 for this series. |
end | Last observation date. |
count | Number of observations, including nulls. |
units | What a value means. Never mixed within a series. |
vintage | The newest last_updated across the sources. See below. |
proxy | True when the series stands in for the thing you asked for rather than measuring it. |
sources | One entry per upstream series used, with its own licence, attribution, frequency, date range and last_updated. |
splices | Where one source hands over to the next. |
checks | How far the sources disagreed where they overlapped. |
licences | The licence ids this series is published under. Look them up in the catalogue's licences. |
/v1/composite/bonds.json. One category, drawn on one axis, so every series in it shares one unit.
| Field | Meaning |
|---|---|
dates | The shared date axis, as ISO strings. |
x | The same axis as Unix seconds, for plotting libraries that want numbers. |
series[].values | Values aligned to dates position by position, padded with null where that series does not reach. Same length as dates, always. |
series[].splices | Handover dates, so a chart can mark them. |
log | Whether this category is meant to be read on a log axis. |
credits | The attribution strings the sources require. Reproduce them if you publish the chart. |
The point of this API is that you can tell where a number came from, so four fields are worth understanding.
vintage is the newest date any upstream source
was updated. It is not the build time. Rebuilding changes nothing about what a
citation refers to, which is what makes the URLs citable.
sources lists each upstream series and the date
range it supplied. A long series is usually several sources end to end: a
historical reconstruction, then a modern official series.
splices records each handover. Every entry has
rescaled: false, and always will. Sources are joined end to end and
never shifted or stretched to meet, so a step at a handover is a real
disagreement between two sources and not an artefact of the join.
checks quantifies that disagreement over the
range where two sources overlap: the mean and maximum difference, absolutely and
as a percentage. A large number is not a bug. It is the honest measure of how
much the join is asking you to accept.
Every # line above the data carries the same provenance as the
JSON, plus a citation line. The data itself is two columns, date,value.
import pandas as pd
df = pd.read_csv("https://evresearch.co.uk/v1/csv/uk-bank-rate.csv",
comment="#", parse_dates=["date"])
Terms belong to whoever published the data, not to us and not to the API it arrived through, so they are stated per source. There is no blanket licence over this data. A series that draws on two publishers carries both sets of terms.
| Id | Licence | Redistribution | Terms pass through |
|---|---|---|---|
boe-millennium | Bank of England, 'A Millennium of Macroeconomic Data' series note | yes | no |
cc-by-4.0 | Creative Commons Attribution 4.0 International | yes | no |
oecd | OECD Terms and Conditions, section 3 | yes | yes |
ogl-v3 | Open Government Licence v3.0 | yes | no |
us-public-domain | US Government work, in the public domain | yes | no |
Where terms pass through, anyone you pass the data to is bound by them too, so you have to tell them. Each CSV says which of these applies to which source, and quotes the required attribution.
Every CSV carries a citation line built from its own vintage. It reads:
evresearch.co.uk. Bank of England policy rate (uk-bank-rate) [dataset], data vintage 2026-07-16. https://evresearch.co.uk/v1/csv/uk-bank-rate.csv
Cite the upstream publishers as well. The attribution strings they require are
in the CSV header and in credits on every composite.
| Path | max-age |
|---|---|
/v1/catalog.json | 5 minutes |
/v1/ everything else | 1 hour |
/v1/figures/ | 1 day |
Series identifiers and URLs are stable: an id that exists today keeps pointing at the same measurement. Fields may be added. Anything that would break a reader gets a new version under a different prefix rather than changing under you.
What is not promised is that the numbers never move. Upstream publishers
revise, and a revision changes values and lifts vintage. That is why
citations name the vintage.