How to get CPI release dates in Python (free API)

You can get the next CPI release date, the full schedule and past prints with consensus and actual in Python with one GET request to the free Finance Calendar API, using only the standard library. This guide walks through urllib, requests, pandas and the financecalendar package, with real responses.

The Finance Calendar API needs no key and returns JSON. US CPI is series code us-cpi (alias cpi); the UK, euro area, Germany, Japan, Canada, Australia and China have their own codes, listed on the CPI release dates API page. Everything below runs as written.

1. The next CPI date with the standard library

import json
import urllib.request

BASE = "https://www.financecalendar.com/wp-json/fc/v1"

def get(path):
    req = urllib.request.Request(BASE + path, headers={"User-Agent": "cpi-script/1.0"})
    with urllib.request.urlopen(req, timeout=20) as r:
        return json.load(r)

nxt = get("/next?series=cpi")["next"]
print(nxt["date"], nxt["time_et"], "ET", nxt["time_utc"])
print("period:", nxt["period"], "| consensus:", nxt["consensus"], "| prior:", nxt["prior"])

For the UK, change the series to uk-cpi. The response for that today is:

{
  "series": "uk-cpi",
  "series_code": "gb-cpi",
  "country": "GB",
  "next": {
    "id": "uk-cpi-inflation-october-2026",
    "series_code": "gb-cpi",
    "series_name": "UK CPI Inflation",
    "name": "UK CPI Inflation",
    "title": "UK CPI Inflation October 2026",
    "country": "GB",
    "category": "economic-indicators",
    "impact": "high",
    "date": "2026-10-21",
    "release_at_utc": "2026-10-21T06:00:00+00:00",
    "time_utc": "2026-10-21T06:00:00+00:00",
    "time_et": "02:00",
    "release_tz": "Europe/London",
    "all_day": false,
    "period": "September 2026",
    "status": "confirmed",
    "consensus": null,
    "prior": "2.9% (August 2026)",
    "actual": null,
    "unit": "% y/y",
    "source_url": "https://www.ons.gov.uk/releasecalendar",
    "source_name": "Office for National Statistics",
    "url": "https://www.financecalendar.com/event/uk-cpi-inflation-october-2026/",
    "exchange": null,
    "holiday_status": null,
    "close_time_local": null,
    "updated_at": "2026-10-19T16:02:05+00:00",
    "note": "Covers September 2026"
  },
  "attribution": {
    "source": "financecalendar.com",
    "terms": "Free for any use with attribution: link to https://www.financecalendar.com",
    "docs": "https://www.financecalendar.com/api/"
  }
}

Three fields matter for scheduling. time_utc is the exact instant with offset, so parse it with datetime.fromisoformat and convert to any zone. status is confirmed when the ONS or BLS has published the date, estimated when it comes from a rule. consensus is null until roughly 2 days before the release.

2. Convert the time to your zone

from datetime import datetime
from zoneinfo import ZoneInfo

t = datetime.fromisoformat(nxt["time_utc"])          # aware datetime, UTC
print(t.astimezone(ZoneInfo("Europe/London")))        # 2026-10-21 07:00:00+01:00
print(t.astimezone(ZoneInfo("Asia/Singapore")))       # 2026-10-21 14:00:00+08:00

Or let the API do it: add &tz=Europe/London and every row gains a time_local field, "07:00" here.

3. The whole CPI schedule

/calendar takes a comma-separated series list and a window of up to 92 days. This pulls every CPI release for all 8 countries for the next 3 months:

codes = "us-cpi,gb-cpi,eu-cpi,de-cpi,jp-cpi,ca-cpi,au-cpi,cn-cpi"
data = get(f"/calendar?series={codes}&to=2027-01-19&limit=500")
print(data["total"], "releases")
for e in data["events"]:
    print(e["date"], e["time_utc"], e["country"], e["name"], "|", e["period"], "| status", e["status"])

Every list response carries count (rows returned) and total (rows matched); with limit=500 they are equal for any 92-day window. For more than 92 days, loop over windows or download the full dataset once (12 months back to 400 days ahead, refreshed hourly) and filter it locally:

cal = json.load(urllib.request.urlopen(
    "https://www.financecalendar.com/wp-content/uploads/fc/calendar.json"))
cpi = [e for e in cal["events"] if e["series_code"] == "us-cpi"]
print(len(cpi), "US CPI rows from", cal["window"]["from"], "to", cal["window"]["to"])

4. Past prints with consensus and actual

hist = get("/history?series=us-cpi&limit=12")
for e in hist["events"]:                     # newest first
    print(e["date"], "consensus", e["consensus"], "| prior", e["prior"], "| actual", e["actual"])

The September 2026 row, for example, is 2026-09-11 consensus None | prior July 2026 CPI unchanged month-on-month; up 3.4% year-on-year (US Bureau of Labor Statistics data cited via USDA ERS) | actual +0.1% MoM, 3.4% YoY (core +0.3% MoM, 2.4% YoY). Figures are strings as reported, so if you want numbers, parse the headline with a regular expression:

import re

def headline_yoy(s):
    m = re.search(r"(-?\d+(?:\.\d+)?)%\s*(?:y/y|YoY)", s or "", re.I)
    return float(m.group(1)) if m else None

for e in hist["events"]:
    print(e["date"], headline_yoy(e["actual"]))

/series/us-cpi returns the same history plus the series metadata, the next release, every upcoming date in the snapshot, and a verified long-run dataset of CPI readings under dataset.

Why not scrape the BLS or ONS page?

You can, and for one country it is a short script, but you then own the parsing. The BLS publishes its schedule as an HTML table, the ONS as a release calendar with its own markup, Eurostat as a PDF and a web calendar, and each changes layout without notice. You also have to work out the timezone of each (Eastern, London, Brussels, Tokyo) and the clock changes in each, and you get no consensus or actual. The API has already done that for 8 agencies, carries the official link in source_url on every row so you can audit it, and re-reads each calendar hourly. If a date is only estimated from a rule, the row says status: estimated; if an agency moves a date, status becomes rescheduled and the /changes endpoint records the old and new values.

Handling status

A scheduling script should treat the 5 status values differently. confirmed rows are safe to schedule against. estimated rows are a rule-based guess until the agency publishes its calendar, so re-check them weekly. rescheduled rows have moved at least once; read /changes?series=us-cpi to see from what. released rows have an actual (or will within the hour). cancelled rows should be dropped. A filter such as status=confirmed,estimated on /calendar returns only what is still ahead:

ahead = get("/calendar?series=us-cpi,gb-cpi&status=confirmed,estimated&to=2027-01-19")
for e in ahead["events"]:
    flag = " (estimate, re-check)" if e["status"] == "estimated" else ""
    print(e["date"], e["name"], flag)

5. With requests

import requests

s = requests.Session()
s.headers["User-Agent"] = "cpi-script/1.0"
# s.headers["X-API-Key"] = "fc_live_..."     # optional free key: 10,000 requests a day

r = s.get(BASE + "/next", params={"series": "cpi", "country": "ca"}, timeout=20)
r.raise_for_status()
print(r.json()["next"]["date"])               # 2026-10-19 (Canada CPI)
print(r.headers["X-RateLimit-Remaining"], "requests left today")

Errors are JSON too. A bad parameter returns HTTP 400 with an error message, for example tz must be an IANA timezone such as Europe/London or Asia/Tokyo., and the daily limit returns HTTP 429 with a Retry-After header in seconds. Check r.ok before reading r.json()["next"].

6. Into pandas

import pandas as pd

df = pd.read_csv("https://www.financecalendar.com/wp-json/fc/v1/calendar?series=us-cpi,gb-cpi,eu-cpi&to=2027-01-19&format=csv",
                 comment="#", parse_dates=["time_utc"])
print(df[["date", "time_utc", "country", "name", "period", "status"]])

format=csv returns the same rows as columns (date, time_utc, time_et, all_day, name, title, impact, category, consensus, prior, actual, url, country, exchange, id, series_code, status, period, unit, source_url) with a trailing attribution comment line, which comment="#" skips. For history, build the frame from JSON:

h = pd.DataFrame(get("/history?series=us-cpi&limit=24")["events"])
h["time_utc"] = pd.to_datetime(h["time_utc"])
h["actual_yoy"] = h["actual"].map(headline_yoy)
print(h[["date", "actual", "actual_yoy"]])

7. The financecalendar package

If you would rather not write the HTTP yourself, pip install financecalendar wraps the same endpoints, reads FC_API_KEY from the environment, and raises on 4xx and 5xx with the API’s message. The standard-library version above is all you need for a script, and it has no dependencies. Whichever you use, send a User-Agent that names your project; it is not required, but it lets us see what people build and get in touch if a series you rely on is about to change.

Scheduling an alert

A common use is a message 30 minutes before each print. Fetch the next date once a day, compute the alert time in UTC, and sleep until then; the code below uses only the standard library and sends the alert to standard output, which you can replace with a webhook call.

import time
from datetime import datetime, timedelta, timezone

def wait_for(series, minutes_before=30):
    nxt = get(f"/next?series={series}")["next"]
    t = datetime.fromisoformat(nxt["time_utc"]) - timedelta(minutes=minutes_before)
    delay = (t - datetime.now(timezone.utc)).total_seconds()
    if delay > 0:
        time.sleep(delay)
    print("Due in", minutes_before, "minutes:", nxt["title"], "| consensus", nxt["consensus"], "| prior", nxt["prior"])

wait_for("cpi")

Re-fetch on each run rather than caching the date for weeks, because the row can move and because consensus only appears about 2 days before the release. Under cron, run the script daily at a quiet hour; it costs 1 request. If you want the result as well, poll /history?series=us-cpi&limit=1 every 5 minutes after the release time until actual is set.

8. Keep it fresh without hammering the API

Responses are cached for 5 minutes, so polling faster than that returns the same data and still counts against the limit (2,000 a day keyless, 10,000 with a free key). For a long-running job, poll /changes once an hour instead and only refetch what moved:

from datetime import datetime, timedelta, timezone

since = (datetime.now(timezone.utc) - timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ")
for c in get(f"/changes?since={since}&series=us-cpi")["changes"]:
    print(c["at"], c["change"], c["id"], c["field"], c["old"], "->", c["new"])

A change row names the event id, the field that changed (date, consensus, actual and so on) and the old and new values. New rows have change: added with no field.

Frequently asked questions

Do I need an API key for Python scripts?

No. A script that runs a few times a day is well inside the 2,000 keyless requests. If you run many scripts behind one office IP address, get a free key (10,000 a day) and send it as X-API-Key.

Why is the CPI figure a string?

Because the headline is reported as text (“+0.1% MoM, 3.4% YoY (core +0.3% MoM, 2.4% YoY)”) and different series report differently. The unit field tells you what the headline number means; parse it as shown above.

How do I get CPI for a country other than the US?

Use the country’s code (gb-cpi, eu-cpi, de-cpi, jp-cpi, ca-cpi, au-cpi, cn-cpi) or add country= to /next. A country without a CPI series returns HTTP 404 with the message No series "cpi" for country NO. See /series?country=NO.

Can I get more than 12 months of history?

The snapshot holds 12 months back. /series/us-cpi adds a verified long-run dataset, and the public repository keeps every hourly snapshot in its commit history.

Attribution

Data from this API is free for any use, commercial included, on one condition: a visible link to https://www.financecalendar.com near where the data appears, or on your credits page. Earnings dates are not included.

Similar Posts