You can load holidays, release dates, economics, commodity spot prices, FX and prediction-market odds from Narwhal into Postgres. Fetch JSON with curl, turn it into CSV with jq, then use psql to load a staging table and upsert rows.
What you'll set up
Postgres is the database destination for this workflow, and you end up with CSV files for each data area plus a holidays table that supports repeat loads. Postgres cannot fetch HTTP itself, so the requests happen in the shell before the database load. You need curl, jq and psql. The fetch commands share a helper for the API URL and authorization header; the database example uses a temporary staging table before it writes to the holidays table. The exports keep holidays, release dates, CPI, commodity prices, FX and policy-path odds in separate files, with the response fields selected for each area.
Set your API key
- Create a free key Create your free key at Narwhal signup. Narwhal uses one account and one API key across holidays, the economic calendar, economics, commodity spot prices, exchange rates and prediction-market odds.
- Store the key in your environment Keep the key in an environment variable named
NARWHAL_API_KEYfor the shell that runs the fetch commands. The helper reads that variable when it sends each request, so the same key applies across the CSV exports. - Use the shared request helper The commands send the key in the
Authorization: Bearer YOUR_KEYheader tohttps://api.narwhalapi.com. REST and MCP share the key. For this Postgres workflow, use the REST requests in the shell helper before loading the resulting files throughpsql.
Pull data with code
- Fetch each area to CSV Run the shell commands to request each area and select its response fields with
jq. The helper usescurl -fto fail on an HTTP error status. Each pipeline writes a CSV file whose column order follows the field order in itsjqexpression. Holidays become date and name columns; CPI becomes period and value columns. Keep that order when defining the staging table for a file.Step 1, fetch each area to CSV with curl and jqget() { curl -sf -H "Authorization: Bearer $NARWHAL_API_KEY" "https://api.narwhalapi.com$1"; } get /v1/calendars/holidays/USA/2026 | jq -r '.holidays[] | [.date, .name] | @csv' > holidays.csv get "/v1/economics/calendar?countries=USA&start_date=2026-11-01&end_date=2026-11-30&limit=100" \ | jq -r '.releases[] | [.at, .period, .topic, .status] | @csv' > releases.csv get "/v1/economics/USA/cpi/observations?start=2025-09" | jq -r '.observations[] | [.period, .value] | @csv' > cpi.csv get /v1/commodities/spot/BRENT/USD | jq -r '[(now | todate), .price] | @csv' > brent.csv get "/v1/fx/rates/USD?currencies=EUR,IDR" | jq -r '.rates[] | [.currency, .rate, .timestamp] | @csv' > fx.csv get /v1/predictions/policy-paths/federal-reserve | jq -r '.meetings[] | [.resolves_at, .p_cut, .p_hold, .p_hike] | @csv' > fed.csv - Load holidays through a staging table Run the load command where
psqlcan readholidays.csv. It creates the destination table if needed, copies the CSV into a temporary table, and inserts the staged rows.DISTINCT ONdrops duplicate dates within the load. When a date already exists in the destination,ON CONFLICTupdates its name. The other CSV files use the same loading pattern with tables and primary keys that match their fields. The holidays destination has adatecolumn typed asdateand anamecolumn typed astext. Its staging table has the same column order as the CSV.Step 2, load holidays with a staging table and upsertpsql <<'SQL' CREATE TABLE IF NOT EXISTS holidays (date date PRIMARY KEY, name text); CREATE TEMP TABLE staging (date date, name text); \copy staging FROM 'holidays.csv' WITH (FORMAT csv) INSERT INTO holidays SELECT DISTINCT ON (date) date, name FROM staging ORDER BY date, name ON CONFLICT (date) DO UPDATE SET name = EXCLUDED.name; SQL
Run it on a schedule
Use a daily cron line to run the script. Put the fetch step before the database load so the CSV is available when \copy reads it, and make NARWHAL_API_KEY available to the script. Because the load upserts, a rerun is safe: an existing holiday date has its name updated. For the other files, use their own primary keys rather than copying the holidays key unchanged. CPI uses period; FX uses currency and timestamp together. Keep the CSV field order aligned with each staging table when extending the scheduled load.
Holidays in Postgres
Request USA holidays for 2026 and read the holidays array. Each item has a date and name, which become the columns in holidays.csv. This is the file used by the staging-table example. The destination uses the holiday date as its primary key, and a repeated load updates the name for that date.
get() { curl -sf -H "Authorization: Bearer $NARWHAL_API_KEY" "https://api.narwhalapi.com$1"; }
get /v1/calendars/holidays/USA/2026 \
| jq -r '.holidays[] | [.date, .name] | @csv' > holidays.csvRelease dates in Postgres
Request the USA economic calendar for November 2026 and read releases. These are release dates. Keep at, period, topic and status in the CSV so the database has the release time alongside its subject and period. The request includes the start and end dates in its URL; its output goes to releases.csv.
get() { curl -sf -H "Authorization: Bearer $NARWHAL_API_KEY" "https://api.narwhalapi.com$1"; }
get "/v1/economics/calendar?countries=USA&start_date=2026-11-01&end_date=2026-11-30&limit=100" \
| jq -r '.releases[] | [.at, .period, .topic, .status] | @csv' > releases.csvEconomics in Postgres
Request USA CPI observations starting at September 2025 and read observations. The sample writes each period and value to cpi.csv. Use period as the destination primary key for this series. The values arrive as decimal strings, so use a numeric column when loading them into Postgres. The period column identifies the observation that an upsert updates.
get() { curl -sf -H "Authorization: Bearer $NARWHAL_API_KEY" "https://api.narwhalapi.com$1"; }
get "/v1/economics/USA/cpi/observations?start=2025-09" \
| jq -r '.observations[] | [.period, .value] | @csv' > cpi.csvCommodity spot prices in Postgres
Request the BRENT spot price in USD and read price. The CSV expression adds a timestamp with jq and places it before the price in brent.csv; that timestamp comes from the expression. Store the price in a numeric column. Spot codes include, for example, XAU, XAG, XPT, XPD, XCU, BRENT and WTI.
get() { curl -sf -H "Authorization: Bearer $NARWHAL_API_KEY" "https://api.narwhalapi.com$1"; }
get /v1/commodities/spot/BRENT/USD \
| jq -r '[(now | todate), .price] | @csv' > brent.csvFX in Postgres
Request EUR and IDR rates with USD as the base and read rates. The CSV keeps currency, rate and timestamp in that order. For the destination table, use currency and timestamp together as the primary key, and store the decimal rate in a numeric column. Load the resulting fx.csv through its own staging table.
get() { curl -sf -H "Authorization: Bearer $NARWHAL_API_KEY" "https://api.narwhalapi.com$1"; }
get "/v1/fx/rates/USD?currencies=EUR,IDR" \
| jq -r '.rates[] | [.currency, .rate, .timestamp] | @csv' > fx.csvPrediction-market odds in Postgres
Request the Federal Reserve policy path and read meetings. Each CSV row keeps resolves_at, p_cut, p_hold and p_hike from a meeting. The output goes to fed.csv. Define the staging columns in that same order and use numeric for decimal values when adapting the load to a prediction-market odds table.
get() { curl -sf -H "Authorization: Bearer $NARWHAL_API_KEY" "https://api.narwhalapi.com$1"; }
get /v1/predictions/policy-paths/federal-reserve \
| jq -r '.meetings[] | [.resolves_at, .p_cut, .p_hold, .p_hike] | @csv' > fed.csvWhy can copying straight into the holidays table fail?
\copy fails on a duplicate key. A temporary staging table accepts the rows before the insert resolves duplicates. In the holidays example, the date and name ordering determines which name survives when a single file contains a repeated date. The upsert then handles dates already present in the destination.
How do I store prices?
Decimals come as strings. Cast them to numeric when loading prices into Postgres. The CSV is the transfer format; the destination column determines how the price is stored in the database. Apply the same decimal-column choice to the CPI values and FX rates in these exports.
Published .
