Use the Narwhal API in Google Sheets

You can pull holidays, release dates, economics, commodity spot prices, exchange rates and prediction-market odds into Google Sheets with Narwhal API. Use Apps Script to write the data into separate tabs, then add a time-driven trigger to refresh them on a schedule.

What you'll set up

Apps Script connects your spreadsheet to the API through UrlFetchApp. You will have a tab for each data area and a pullAll function that fills them using the same API key. The key stays in Script properties, and the script reads it when it makes a request. REST and MCP share that key across the data APIs. The base URL is https://api.narwhalapi.com, and each data call uses GET.

Set your API key

  1. Create a free key Get a free Narwhal API key. Keep the key for the Script property you will add in the editor; never put it in a spreadsheet cell.
  2. Open Apps Script In Google Sheets, open the Extensions menu and select Apps Script. This is where you will add the script and set up its access to your key.
  3. Store the key in Script properties In Project Settings, add a Script property named NARWHAL_API_KEY and set its value to your key. The script reads it with PropertiesService.getScriptProperties() and sends it in the Authorization header as Bearer YOUR_KEY.

Pull data with Apps Script

  1. Create the data tabs Create tabs named Holidays, Releases, CPI, Brent, FX and Fed. Keep these names exactly as written because the script uses them to find the destination for each response.
  2. Add the script Paste this code into Apps Script. The shared pull helper reads the response field named in each call and writes its rows to the matching tab. Brent uses the response object directly because its price is outside a list.
    Apps Script, six areas
    const API = "https://api.narwhalapi.com";
    
    function pull(sheetName, path, listKey) {
      const key = PropertiesService.getScriptProperties().getProperty("NARWHAL_API_KEY");
      const res = UrlFetchApp.fetch(API + path, { headers: { Authorization: "Bearer " + key } });
      const body = JSON.parse(res.getContentText());
      const rows = listKey ? body[listKey] : [body];
      if (!rows || rows.length === 0) return;                       // an empty list writes nothing
      const columns = [];
      rows.forEach((row) => Object.keys(row).forEach((name) => {    // every row, so a null first value cannot hide a column
        if (!columns.includes(name) && (row[name] === null || typeof row[name] !== "object")) columns.push(name);
      }));
      const values = [columns].concat(rows.map((row) => columns.map((name) => (row[name] === undefined || row[name] === null ? "" : row[name]))));
      const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
      sheet.clear();
      // Text columns (dates, periods like 2026-09) stay plain text so Sheets does not turn them into dates.
      // Decimal strings such as "1.0823" stay numbers.
      columns.forEach((name, index) => {
        const sample = rows.find((row) => typeof row[name] === "string" && row[name] !== "");
        if (sample && !/^-?\d+(\.\d+)?$/.test(sample[name]))
          sheet.getRange(1, index + 1, values.length, 1).setNumberFormat("@");
      });
      sheet.getRange(1, 1, values.length, columns.length).setValues(values);
    }
    
    function pullAll() {
      pull("Holidays", "/v1/calendars/holidays/USA/2026", "holidays");
      pull("Releases", "/v1/economics/calendar?countries=USA&start_date=2026-11-01&end_date=2026-11-30&limit=100", "releases");
      pull("CPI", "/v1/economics/USA/cpi/observations?start=2025-09", "observations");
      pull("Brent", "/v1/commodities/spot/BRENT/USD", null);
      pull("FX", "/v1/fx/rates/USD?currencies=EUR,IDR", "rates");
      pull("Fed", "/v1/predictions/policy-paths/federal-reserve", "meetings");
    }
  3. Run pullAll Run pullAll from the editor to fill the tabs. For a nonempty response, the helper clears the destination tab and writes a header row followed by the data, so keep formulas outside those tabs. Empty lists write nothing. It reads column names across every row, so a null first value cannot hide a column.

Text columns, including dates and periods such as 2026-09, stay plain text so Sheets cannot turn them into dates. Decimal values sent as strings, such as 1.0823, stay numbers. The calls below use the same helper, API base URL and Script property; each names its destination tab, request path and response field.

Run it on a schedule

  1. Open triggers Use the clock icon in the Apps Script editor to open triggers.
  2. Add a time-driven trigger Add a time-driven trigger for pullAll, for example daily. It calls the same function that fills the data tabs from the editor.

Custom functions can call URL Fetch, but they re-run whenever the sheet recalculates, which costs a request each time; a function run from a menu item or a time-driven trigger writes the values once.

Holidays in Google Sheets

Pull USA holidays for 2026 into Holidays. The response list is holidays, with date and name on each entry. The helper writes both fields as columns and keeps the date text intact. Use this call with the shared script when you want the holiday rows in their own tab.

Holidays pull
pull(
  "Holidays", "/v1/calendars/holidays/USA/2026", "holidays"
);
// Read date and name in the Holidays tab.

Release dates in Google Sheets

Pull the USA economic release calendar for November 2026 into Releases. Read releases from the response, with at, period, topic and status on each entry. This request is for release dates; use the CPI observations call for the economic values associated with periods.

Releases pull
pull(
  "Releases", "/v1/economics/calendar?countries=USA&start_date=2026-11-01&end_date=2026-11-30&limit=100", "releases"
);
// Read at, period, topic and status in the Releases tab.

Economics in Google Sheets

Pull USA CPI observations with a requested start of September 2025 into CPI. The response list is observations, and the tab columns are period and value. Read the header row after the first pull. For month-over-month calculations, look up each month by its period text so a missing month cannot shift the formula.

CPI pull
pull(
  "CPI", "/v1/economics/USA/cpi/observations?start=2025-09", "observations"
);
// Read period and value in the CPI tab.

For September 2026 compared with August 2026, use the formula below outside the CPI tab. Each VLOOKUP asks for the named month in CPI!A:B; the calculation divides the September value by the August value and subtracts one.

Month-over-month CPI
=VLOOKUP("2026-09",CPI!A:B,2,FALSE)/VLOOKUP("2026-08",CPI!A:B,2,FALSE)-1

Commodity spot prices in Google Sheets

Pull the Brent spot price in USD into Brent. Read the price field from the response object. This call passes null for the list key, so the helper treats the whole object as a row and writes its fields under their column names.

Brent pull
pull(
  "Brent", "/v1/commodities/spot/BRENT/USD", null
);
// Read price in the Brent tab.

Exchange rates in Google Sheets

Pull EUR and IDR exchange rates with USD as the base into FX. The rates list has currency and rate on each entry. Keep those columns together when you read the tab so each rate stays associated with its currency.

FX pull
pull(
  "FX", "/v1/fx/rates/USD?currencies=EUR,IDR", "rates"
);
// Read currency and rate in the FX tab.

Prediction-market odds in Google Sheets

Pull the Federal Reserve policy path into Fed. The response list is meetings, with resolves_at, p_cut, p_hold and p_hike on each entry. Read the resolution time alongside the probabilities so the cut, hold and hike fields stay associated with their meeting.

Fed pull
pull(
  "Fed", "/v1/predictions/policy-paths/federal-reserve", "meetings"
);
// Read resolves_at, p_cut, p_hold and p_hike in the Fed tab.

Why not use IMPORTDATA or IMPORTXML?

IMPORTDATA and IMPORTXML cannot send an Authorization header. Narwhal API requests require the Bearer header, so use Apps Script with UrlFetchApp to make the request. The helper reads the key from Script properties and adds the header before it fetches the response.

Can I use NETWORKDAYS with the holiday data?

Yes. Use the Holidays tab’s date column as the holiday argument to NETWORKDAYS. Use the dates from that column rather than the holiday names. The holiday request in this script supplies the USA holiday dates for 2026.

Why can the CPI response begin later than requested?

If the requested start date is earlier than your plan’s history window, the API moves it up to the window’s first day without an error. The response then begins later than you asked. This also applies to a request such as the CPI call above: its start parameter specifies the requested beginning, and the plan window determines how far back the response can go.

403 plan_upgrade_required appears when the whole requested range ends before the window, or when you read a single date or period outside it. An earlier start date alone does not cause that error.

Published .