A desk that trims risk, an on-call engineer who pauses jobs and a shift lead who briefs staff need the same message before a release: the time in UTC and local time, the period it covers and the previous value. The Narwhal economic calendar returns all four, and a short scheduled script turns them into one line per release.
What a pre-release alert has to carry
Before a major number a desk trims risk, an on-call engineer pauses batch jobs and a shift lead briefs staff. All three act on the same facts: when the release lands, which period it covers and what the last figure was. Staff in another city also need the time in their own zone, because a message that says 13:30 UTC leaves them doing the sum under pressure. GET /v1/economics/calendar returns each of those facts. This request asks for the United States on 10 November.
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/economics/calendar?countries=USA&start_date=2026-11-10&end_date=2026-11-10&limit=100"{
"releases": [
{
"release_id": "66f7f9f6-0318-4f25-8896-541c428ee07e",
"revision": 2,
"country": "USA",
"series_id": "usa-wages-and-earnings",
"name": "Wages and earnings",
"topic": "wages",
"family": "labour",
"period": "2026-10",
"status": "scheduled",
"at": "2026-11-10T13:30:00Z",
"timezone": "America/New_York",
"values": [
{
"measure": "real_average_hourly_earnings_mom",
"concept": "real_average_hourly_earnings",
"unit": "percent",
"transformation": "month_over_month",
"previous": "-0.5"
},
{
"measure": "real_average_hourly_earnings_yoy",
"concept": "real_average_hourly_earnings",
"unit": "percent",
"transformation": "year_over_year",
"previous": "-0.7"
}
]
},
{
"release_id": "f01e2289-2e17-49b3-9d64-d29118742dee",
"revision": 3,
"country": "USA",
"series_id": "usa-consumer-prices",
"name": "Consumer prices",
"topic": "consumer_prices",
"family": "prices",
"period": "2026-10",
"status": "scheduled",
"at": "2026-11-10T13:30:00Z",
"timezone": "America/New_York",
"values": [
{
"measure": "core_cpi_yoy",
"concept": "core_cpi",
"unit": "percent",
"transformation": "year_over_year",
"previous": "2.6",
"not_seasonally_adjusted": true
},
{
"measure": "cpi_mom",
"concept": "cpi",
"unit": "percent",
"transformation": "month_over_month",
"previous": "0.6"
},
{
"measure": "cpi_yoy",
"concept": "cpi",
"unit": "percent",
"transformation": "year_over_year",
"previous": "3.7",
"not_seasonally_adjusted": true
}
]
}
],
"next_cursor": null
}at is the release time in UTC. period names the month the figures cover, and values[].previous holds the last published figure for each measure, with its unit. The response lists the previous figure and carries no forecast, so a desk that wants a consensus number adds it from its own source. status stays scheduled until the figures are out.
Write one alert line per release
The script below is meant to run on a schedule, for example every morning, and prints one line for each release due in the next 24 hours.
- Set the desk zone and the countries
DESK_TZis the zone the staff read the message in, andCOUNTRIESlists the economies the desk trades or depends on. - Ask for a padded date window The date filter follows each publisher's local date, so the script asks from one day before to two days after today and filters on
atitself. - Keep what is still ahead A release is kept when its status is
scheduled, it has anattime and that time falls between now and the lead time.NOWis an optional variable that lets you test with a fixed clock. - Convert the time and add the previous figure
astimezoneturns the UTC time into the desk zone, so a clock change in either place needs no hand-made offset. The previous figures come fromvalues.import os from datetime import datetime, timedelta, timezone from zoneinfo import ZoneInfo import requests DESK_TZ = ZoneInfo("Europe/Zurich") COUNTRIES = ["USA", "EMU"] LEAD = timedelta(hours=24) now = datetime.fromisoformat(os.environ["NOW"]) if "NOW" in os.environ else datetime.now(timezone.utc) r = requests.get( "https://api.narwhalapi.com/v1/economics/calendar", params={ "countries": COUNTRIES, "start_date": (now - timedelta(days=1)).date().isoformat(), "end_date": (now + timedelta(days=2)).date().isoformat(), "limit": 100, }, headers={"Authorization": f"Bearer {os.environ['NARWHAL_API_KEY']}"}, timeout=10, ) r.raise_for_status() due = [] for x in r.json()["releases"]: if x["status"] != "scheduled" or "at" not in x: continue at = datetime.fromisoformat(x["at"].replace("Z", "+00:00")) if now <= at < now + LEAD: due.append((at, x)) for at, x in sorted(due, key=lambda t: t[0]): prev = ", ".join(f"{v['measure']} {v['previous']}" for v in x["values"] if "previous" in v) print(f"{at:%H:%M} UTC / {at.astimezone(DESK_TZ):%H:%M} {DESK_TZ.key} | " f"{x['country']} {x['name']} {x.get('period', '-')} | previous: {prev or 'none listed'}")
With NOW set to 09:00 UTC on 10 November, the script printed two lines. Zurich is one hour ahead of UTC on that date, which is why 13:30 UTC reads 14:30. The two US releases share one release time, so a desk that wants a single message can group the lines by their at value.
13:30 UTC / 14:30 Europe/Zurich | USA Wages and earnings 2026-10 | previous: real_average_hourly_earnings_mom -0.5, real_average_hourly_earnings_yoy -0.7
13:30 UTC / 14:30 Europe/Zurich | USA Consumer prices 2026-10 | previous: core_cpi_yoy 2.6, cpi_mom 0.6, cpi_yoy 3.7Match the alert to the person who reads it
| Reader | Action before the release | What the alert needs |
|---|---|---|
| Trader | Trim or hedge a position | UTC and local time, the previous figure |
| On-call engineer | Pause a deploy or a batch job around the minute | The exact at time, and status to confirm it is still scheduled |
| Shift lead | Brief staff on what is coming | Release name, period and the previous figure |
Send the lines through whatever channel the team already watches. The API supplies the facts, and your own chat or mail tool delivers them. Run the script from a scheduler as in schedule a daily data pull, at least as often as the lead time you want.
Keep the alert true when a release moves
A release time can change after you sent the morning list. Each calendar item has a revision that rises when its details change, so a script that re-reads the calendar shortly before the release can compare the revision with the one it sent. For a push, webhooks report release.scheduled, release.rescheduled and release.cancelled; see receive webhooks. A webhook arrives when the schedule changes and carries no countdown, so the lead time still comes from your own timer.
Staff who only want a phone reminder can subscribe a calendar app to a feed created with POST /v1/calendars/subscriptions. The request takes countries, start_date and end_date, and the response has a url that is returned once, so store it as a secret. That call creates something, so this page describes it and does not run it. The calendar subscriptions documentation has the full request.
What if the release has no exact time yet?
Some releases carry date, between or by instead of at. The script skips them because it cannot place them on a clock. List them in a separate morning note so the desk knows a release may come without a fixed minute.
Can the API send the reminder itself?
No endpoint sends a timed reminder. The reminder comes from a calendar app, from a scheduled script of yours or from a webhook consumer that starts a timer of its own.
Why does the script ask for dates around today?
The date window follows the publisher's local date. A release in Tokyo or Sydney can sit on a different calendar date than its UTC time, so the request is padded and at decides what is due.
Published .
