Load Narwhal data into Postgres

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

  1. 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.
  2. Store the key in your environment Keep the key in an environment variable named NARWHAL_API_KEY for 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.
  3. Use the shared request helper The commands send the key in the Authorization: Bearer YOUR_KEY header to https://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 through psql.

Pull data with code

  1. Fetch each area to CSV Run the shell commands to request each area and select its response fields with jq. The helper uses curl -f to fail on an HTTP error status. Each pipeline writes a CSV file whose column order follows the field order in its jq expression. 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 jq
    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.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
  2. Load holidays through a staging table Run the load command where psql can read holidays.csv. It creates the destination table if needed, copies the CSV into a temporary table, and inserts the staged rows. DISTINCT ON drops duplicate dates within the load. When a date already exists in the destination, ON CONFLICT updates its name. The other CSV files use the same loading pattern with tables and primary keys that match their fields. The holidays destination has a date column typed as date and a name column typed as text. Its staging table has the same column order as the CSV.
    Step 2, load holidays with a staging table and upsert
    psql <<'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.

Fetch holiday dates and names
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.csv

Release 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.

Fetch economic release dates
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.csv

Economics 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.

Fetch CPI periods and values
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.csv

Commodity 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.

Fetch a commodity spot price
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.csv

FX 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.

Fetch exchange rates
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.csv

Prediction-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.

Fetch policy-path meeting odds
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.csv

Why 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 .