Query the Narwhal API with DuckDB

You can use DuckDB and Narwhal API to query holidays, release dates, economics, commodities, FX and prediction-market odds in SQL. A small Python function fetches the JSON with your API key, saves each response as a file, and lets DuckDB read it into tables.

What you'll set up

DuckDB reads local JSON with read_json, and this setup gives you a narwhal.duckdb database with a table for each data area. Python handles the API requests and writes the response files that SQL reads. The full example includes queries for holiday and release counts, CPI observations, Brent prices, exchange rates and Federal Reserve policy probabilities. Narwhal uses one account and one API key across these APIs; REST and MCP share that key.

Set your API key

  1. Create a free key Create your free key at narwhalapi.com/signup. Use the same Narwhal API key for the data areas in the example; the request helper sends it in the Authorization header as Bearer YOUR_KEY.
  2. Store the key in the environment Set an environment variable called NARWHAL_API_KEY for the Python file. The Python setup reads it with os.environ and puts it in the headers used by fetch, so the key stays out of SQL text.
  3. Install the Python packages Install duckdb and requests for the Python file. The example imports both packages, uses requests to fetch each response and opens the database with duckdb.connect.
    Install DuckDB and requests
    pip install duckdb requests

Pull data with code

Save the Python example as a file and run it with NARWHAL_API_KEY available in its environment. Each fetch call writes a named JSON file, and the SQL that follows reads that file into its corresponding table. The helper sends GET requests to https://api.narwhalapi.com and calls raise_for_status before writing the response. The final queries read the resulting tables through the same DuckDB connection. Use the shorter area samples with this helper and connection when you want to pull an individual area.

DuckDB, six areas into tables
import os
import duckdb
import requests

API = "https://api.narwhalapi.com"
HEADERS = {"Authorization": f"Bearer {os.environ['NARWHAL_API_KEY']}"}

def fetch(name, path, **params):
    r = requests.get(API + path, params=params, headers=HEADERS, timeout=10)
    r.raise_for_status()
    with open(f"{name}.json", "w") as file:
        file.write(r.text)

con = duckdb.connect("narwhal.duckdb")

fetch("holidays", "/v1/calendars/holidays/USA/2026")
con.sql("CREATE OR REPLACE TABLE holidays AS SELECT unnest(holidays, recursive := true) FROM read_json('holidays.json')")

fetch("releases", "/v1/economics/calendar", countries="USA", start_date="2026-11-01", end_date="2026-11-30", limit=100)
con.sql("CREATE OR REPLACE TABLE releases AS SELECT unnest(releases, recursive := true) FROM read_json('releases.json')")

fetch("cpi", "/v1/economics/USA/cpi/observations", start="2025-09")
con.sql("CREATE OR REPLACE TABLE cpi AS SELECT unnest(observations, recursive := true) FROM read_json('cpi.json')")

fetch("brent", "/v1/commodities/spot/BRENT/USD")
con.sql("CREATE OR REPLACE TABLE brent AS SELECT price, unit FROM read_json('brent.json')")

fetch("fx", "/v1/fx/rates/USD", currencies="EUR,IDR")
con.sql("CREATE OR REPLACE TABLE fx AS SELECT unnest(rates, recursive := true) FROM read_json('fx.json')")

fetch("fed", "/v1/predictions/policy-paths/federal-reserve")
con.sql("CREATE OR REPLACE TABLE fed AS SELECT unnest(meetings, recursive := true) FROM read_json('fed.json')")

print(con.sql("SELECT count(*) FROM holidays").fetchall(), con.sql("SELECT count(*) FROM releases").fetchall())
print(con.sql("SELECT period, value FROM cpi ORDER BY period DESC LIMIT 2").fetchall())
print(con.sql("SELECT * FROM brent").fetchall(), con.sql("SELECT currency, rate FROM fx").fetchall())
print(con.sql("SELECT p_hold, expected_change_bps FROM fed ORDER BY resolves_at LIMIT 1").fetchall())

Run it on a schedule

Run the Python file from cron to repeat the pull. Make NARWHAL_API_KEY available to that scheduled Python file, since the request headers read the key from the environment. Each call fetches its response again and writes its JSON file before DuckDB reads it. CREATE OR REPLACE TABLE makes each run repeatable: the same table names receive the data read from those files. For a scheduled pull, keep the fetch calls and the table creation statements together in the Python file.

Holidays in DuckDB

Fetch the USA holiday calendar for 2026, then read its holidays field into the holidays table. The sample selects date and name so the SQL result contains the holiday dates alongside their names. On 24 October 2026, this response contained 11 holidays. The full Python example also includes a count query for this table if you want a count instead of the individual holiday rows.

Read holiday dates and names
fetch("holidays", "/v1/calendars/holidays/USA/2026")
con.sql("CREATE OR REPLACE TABLE holidays AS SELECT unnest(holidays, recursive := true) FROM read_json('holidays.json')")
print(con.sql("SELECT date, name FROM holidays").fetchall())

Release dates in DuckDB

Pull the USA economic calendar for November 2026 with the supplied start date, end date and limit. The response's releases field contains at, period, topic and status; this sample reads the period, topic and status columns from the table. On 24 October 2026, the response contained 31 releases. These are economic release dates, while the observations request in the following sample reads CPI values by period.

Read release periods and topics
fetch("releases", "/v1/economics/calendar", countries="USA", start_date="2026-11-01", end_date="2026-11-30", limit=100)
con.sql("CREATE OR REPLACE TABLE releases AS SELECT unnest(releases, recursive := true) FROM read_json('releases.json')")
print(con.sql("SELECT period, topic, status FROM releases").fetchall())

Economics in DuckDB

Read USA CPI observations with start set to 2025-09. The JSON field is called observations, and the resulting cpi table has period and value columns for the query to select. On 24 October 2026, the CPI value for 2026-09 was 336.940. The sample orders observations by period in descending order and limits the result, using the same query as the complete Python example.

Read CPI observations
fetch("cpi", "/v1/economics/USA/cpi/observations", start="2025-09")
con.sql("CREATE OR REPLACE TABLE cpi AS SELECT unnest(observations, recursive := true) FROM read_json('cpi.json')")
print(con.sql("SELECT period, value FROM cpi ORDER BY period DESC LIMIT 2").fetchall())

Commodities in DuckDB

Fetch Brent spot prices in USD and read price and unit directly from the saved JSON. This table creation statement selects those fields without a list expansion. On 24 October 2026, Brent was 116.8809 dollars a barrel. The sample reads the resulting brent table with SELECT *, so both the price and its unit are available in the result returned to Python.

Read Brent price and unit
fetch("brent", "/v1/commodities/spot/BRENT/USD")
con.sql("CREATE OR REPLACE TABLE brent AS SELECT price, unit FROM read_json('brent.json')")
print(con.sql("SELECT * FROM brent").fetchall())

FX in DuckDB

Use USD as the base currency and request EUR and IDR in the currencies parameter. Read the response's rates field into the fx table, then select currency and rate together. On 24 October 2026, USD/EUR was 0.89271 and USD/IDR was 18107.8. Those rates are stored as text strings. The query keeps each currency beside its rate when it reads the rows back into Python.

Read exchange rates
fetch("fx", "/v1/fx/rates/USD", currencies="EUR,IDR")
con.sql("CREATE OR REPLACE TABLE fx AS SELECT unnest(rates, recursive := true) FROM read_json('fx.json')")
print(con.sql("SELECT currency, rate FROM fx").fetchall())

Predictions in DuckDB

Read the Federal Reserve policy path from the response's meetings field into the fed table. On 24 October 2026, the hold probability for the 28 October 2026 meeting was 0.9188, with an expected change of 1.7 basis points. The query orders by resolves_at and reads p_hold and expected_change_bps for that meeting. It returns the numeric columns to Python.

Read hold probability and expected change
fetch("fed", "/v1/predictions/policy-paths/federal-reserve")
con.sql("CREATE OR REPLACE TABLE fed AS SELECT unnest(meetings, recursive := true) FROM read_json('fed.json')")
print(con.sql("SELECT p_hold, expected_change_bps FROM fed ORDER BY resolves_at LIMIT 1").fetchall())

Why use unnest for the list fields?

A list sits inside a single JSON field. unnest(list_column, recursive := true) turns it into a row per item, so the holiday, release, observation, rate and meeting lists become rows in their respective tables. Use the response field name inside unnest: for example, the CPI response uses observations even though its saved file and table are named cpi.

How should I select timestamps through Python?

Install pytz before selecting the at or resolves_at timestamps through Python. You can also cast the timestamps in SQL as needed. The prediction sample orders by resolves_at inside SQL and returns numeric columns to Python; the release sample selects period, topic and status.

Published .