sifting/io
Economic Calendar
6 min readSiftingIO Team

Economic calendar webhook alerts: get CPI and jobs report releases pushed to your app

Get CPI and jobs report releases pushed to any webhook: a Node.js poller on SiftingIO's economic calendar API, with plan limit math and common pitfalls.

Economic calendar webhook alerts: get CPI and jobs report releases pushed to your app

How do you get an economic calendar webhook, a POST hitting your own endpoint the moment CPI or the monthly jobs report is released? Most market data providers publish scheduled economic releases through a pull API, and SiftingIO is no exception: you request the calendar, you get JSON back, and nothing is pushed to you. The webhook is the part you own. The delivery layer is small enough that owning it isn't a burden, about fifty lines of Node.js: a poller that reads SiftingIO's economic calendar endpoint, remembers what it has already delivered, and POSTs anything new to whatever URL you give it. The receiving end can be an internal service, a Slack incoming webhook, or an n8n workflow.

This tutorial builds that notifier with no dependencies beyond Node 18, then works through the polling cadence math so the poller stays inside your plan's request budget instead of silently burning through it.

How the economic calendar API works#

The calendar lives under SiftingIO's fundamentals family:

curl -H "X-API-Key: $SIFTING_KEY" \
     "https://api.sifting.io/v1/fnd/economic-calendar?limit=5"

It returns upcoming US economic events across 25 event types, the releases that move markets on a schedule: CPI, the employment report, FOMC decisions, and the rest of the US statistical calendar. Each event carries an impact tier of low, medium, or high, which is the first filter worth applying when you only care about headline releases.

Three conventions matter for the code you're about to write. Pagination is cursor based: ?limit defaults to 50 and caps at 200, and the response's meta object carries next_cursor (null on the last page) plus an as_of timestamp. Full timestamps are RFC 3339 in UTC. Date-only fields are plain ISO dates. Run the curl once and read a real response before writing any parsing code; the full field reference is on the economic calendar endpoint docs.

Build the webhook notifier#

The notifier does four things every cycle: fetch the calendar, filter to the releases you care about, skip anything already delivered, and POST the rest to WEBHOOK_URL. Deduplication uses a hash of the entire event object rather than an ID field. That choice is deliberate. Economic figures get revised after first publication, and an event whose numbers changed produces a new hash, so revisions are re-delivered instead of silently swallowed.

import { createHash } from "node:crypto";

const API = "https://api.sifting.io/v1/fnd/economic-calendar";
const WEBHOOK_URL = process.env.WEBHOOK_URL;
const POLL_MS = 5 * 60 * 1000; // 5 minutes, see the cadence math below

// Match CPI and the monthly jobs report. Tighten this to the
// documented event-type field once you've inspected a live response.
const WATCH = /(consumer price|\bcpi\b|nonfarm|payroll|employment situation)/i;

const delivered = new Set();

const fingerprint = (event) =>
  createHash("sha256").update(JSON.stringify(event)).digest("hex");

async function deliver(event) {
  const res = await fetch(WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ source: "economic-calendar", event }),
  });
  if (!res.ok) throw new Error(`webhook responded ${res.status}`);
}

async function poll() {
  let waitMs = POLL_MS;
  try {
    const res = await fetch(`${API}?limit=200`, {
      headers: { "X-API-Key": process.env.SIFTING_KEY },
    });
    if (res.status === 429) {
      waitMs = Number(res.headers.get("Retry-After") ?? "60") * 1000;
    } else {
      const body = await res.json();
      // Event list plus meta, per the cursor pagination convention.
      for (const event of body.data ?? []) {
        if (!WATCH.test(JSON.stringify(event))) continue;
        const fp = fingerprint(event);
        if (delivered.has(fp)) continue;
        await deliver(event);
        delivered.add(fp);
      }
    }
  } catch (err) {
    console.error("poll failed:", err.message);
  }
  setTimeout(poll, waitMs);
}

poll();

Two notes on hardening. The delivered set grows forever as written; in production, prune it once it passes a few thousand entries. And the regex filter matches against the serialized event, which is a blunt instrument to start with: once you've inspected a live response, tighten it to the documented event-type field so a stray mention of "payroll" in an unrelated description can't trigger a false delivery. A failed POST is also handled sensibly for free: deliver throws, the event never enters delivered, and the next cycle retries it.

Wire WEBHOOK_URL to anything that accepts JSON over POST. If you'd rather not run code at all, the same poll, filter, and POST loop assembles in a few nodes in n8n, and SiftingIO ships an n8n integration.

Polling cadence and the request budget#

A webhook notifier is a background process, and background processes are where request budgets die quietly. The arithmetic is worth doing once. Polling every minute costs 43,200 calls in a 30 day month. On SiftingIO's free tier, which allows 10,000 REST calls a month, that poller exhausts the entire budget in about a week, and every other job sharing the key starts failing with it. Polling every 5 minutes costs 8,640 calls a month, which fits, barely, if the key does nothing else.

There are two ways out. Either move up a tier (Builder allows 250,000 calls a month, enough to poll every minute with room to spare), or poll adaptively: the calendar tells you when the next release is scheduled, so a smarter loop sleeps until a few minutes before the next event it cares about and only then tightens to a fast cadence. Scheduled releases are the rare case where adaptive polling is easy, because the schedule itself is the data you're already fetching. Plan quotas change over time, so verify the current numbers on the pricing page before you build around them.

Common pitfalls#

The monthly cap fails silently until it doesn't. A poller that works perfectly in a two day test can still be on pace to blow the monthly quota. Watch the X-RateLimit-Remaining header on every response, and treat a 429 with rate_limit_exceeded as a signal to lengthen the cadence, not just retry. The code above honors Retry-After; make sure any version you write does too.

Daylight saving time moves releases against UTC. US economic releases are scheduled in US Eastern local time, and that timezone's UTC offset changes twice a year. A release you've mentally filed as "13:30 UTC" arrives at 12:30 UTC for part of the year. The API's timestamps are RFC 3339 UTC, so compare them directly against the current UTC time, and never hard-code a UTC hour for a release.

Revisions look like duplicates. Prior month figures are routinely revised when the next month's release comes out. If you deduplicate on an event identifier alone, the revised event looks like something you already delivered and gets dropped, so downstream consumers keep an outdated number. Hash the full object, as above, and make the webhook receiver idempotent, since the same logical event can now legitimately arrive more than once.

The notifier is a building block rather than an end state. The same loop that POSTs to a chat webhook can annotate charts in a dashboard, pause a data pipeline around a high impact release, or stamp release times into a research database for later event studies. The free tier needs no credit card, and one credential covers the calendar alongside the rest of the API. Start building free

Keep reading

Related posts