Tutorial
Live stock prices in Excel with Power Query and an API
The Stocks data type is great for a glance and awkward as a foundation. Power Query can call a real API with a header key, refresh on a timer, and put bid, ask, FX, crypto and history into ordinary tables.
On this page
- Excel stock data type, STOCKHISTORY or an API
- The two-minute version: one symbol from the From Web dialog
- A reusable workbook: one parameter, two functions, three sheets
- Daily history in its own sheet
- Refresh Excel stock prices on a timer without burning the quota
- Errors and what to do about them
- Questions
Key takeaways
- Power Query's Web connector can call a REST API with an `x-api-key` header, parse the JSON and load it as an Excel table that refreshes on a timer.
- Microsoft says Stocks data type information is delayed, and STOCKHISTORY returns daily, weekly or monthly bars only; an API adds bid, ask and intraday bars.
- One parameter holds the key, one function fetches a snapshot, and one line per sheet turns a symbol table into a stocks, FX or crypto sheet.
- A workbook refreshing every 5 minutes keeps calling while it is open, nights and weekends included, so price the refresh interval before you set it.
To get live Excel stock prices from an API, use Power Query's From Web connector: point it at a REST endpoint such as https://api.tickerlayer.com/stocks/snapshot/US:KO, send your key in an x-api-key header, load the JSON as a table, and set the query to refresh every few minutes. The same query shape works for currency pairs, crypto and commodities, and it reads exactly the numbers your code gets from the stock API.
Below: the one-symbol version you can do from a dialog in two minutes, then a reusable workbook with a key parameter, a snapshot function, separate stocks, FX and crypto sheets, a daily history sheet, and the refresh arithmetic. The menus are those of Excel for Windows with Microsoft 365; Excel for Mac offers fewer Get Data connectors, so check that From Web is there before you start.
Excel stock data type, STOCKHISTORY or an API
Excel already has two stock features. Type a ticker, choose Data > Stocks, and the cell becomes a data type with fields such as price, change and previous close. STOCKHISTORY returns historical bars into a range. Both are quick, and both are built for glancing at a portfolio: Microsoft's own note says the stock information is delayed and not for trading purposes, and STOCKHISTORY stops at daily, weekly and monthly intervals.
| Feature | Stocks data type | STOCKHISTORY | Power Query + API |
|---|---|---|---|
| Setup | None | One formula | One query per sheet |
| Latency | Delayed, per Microsoft | Daily history | Same feed your code uses |
| Bid and ask with sizes | |||
| Intraday bars (1 minute to 4 hours) | |||
| Crypto, FX and commodities by code | |||
| Same numbers as your application | |||
| Refresh schedule you control |
The strongest reason is the last-but-one row. When a spreadsheet sits next to an application, a report or a model, it should show the same numbers from the same source. A finance team checking a dashboard against Excel should never have to explain a difference caused by two different feeds.
The two-minute version: one symbol from the From Web dialog
- Open the Web connectorData > Get Data > From Other Sources > From Web, then choose Advanced.
- Enter the URLURL parts:
https://api.tickerlayer.com/stocks/snapshot/US:KO. Stocks always carry a market prefix; a bareKOreturns 400. - Add the headerUnder HTTP request header parameters type
x-api-keyon the left and your key on the right. Click OK. - Connect anonymouslyWhen Excel asks how to authenticate, pick Anonymous. The key already travels in the header.
- Turn the record into a tablePower Query shows a record. Choose Into Table, then Close & Load. You now have a two-column table of field names and values.
A reusable workbook: one parameter, two functions, three sheets
- Symbol tableStockSymbols, FxSymbols, CryptoSymbols
- fnQuotesone row per symbol
- fnSnapshotWeb.Contents + x-api-key
- REST snapshot/{asset}/snapshot/{symbol}
- Loaded tableLast, Change %, Bid, Ask, time
- Create the key parameterOpen Power Query (Data > Get Data > Launch Power Query Editor), then Home > Manage Parameters > New. Name
TL_API_KEY, type Text, current value your key. - Create the symbol tablesOn three sheets, type a header
Symboland a few symbols, then Insert > Table and name the tablesStockSymbols,FxSymbolsandCryptoSymbolsin the Table Design tab. - Add the two functionsIn Power Query choose Home > New Source > Other Sources > Blank Query, open Advanced Editor, paste each listing below, and name the queries
fnSnapshotandfnQuotes. - Add one query per sheetThree more blank queries, each a single line:
= fnQuotes("StockSymbols", "stocks"),= fnQuotes("FxSymbols", "forex")and= fnQuotes("CryptoSymbols", "crypto"). Load each to its own sheet.
(asset as text, symbol as text) as record =>
let
Response = Web.Contents(
"https://api.tickerlayer.com",
[
RelativePath = asset & "/snapshot/" & symbol,
Headers = [#"x-api-key" = TL_API_KEY],
// Read these statuses ourselves instead of failing the whole refresh
ManualStatusHandling = {400, 401, 403, 404, 429}
]
),
Status = Value.Metadata(Response)[Response.Status],
Result =
if Status = 200 then Json.Document(Response)
else [symbol = symbol, error = "HTTP " & Text.From(Status)]
in
Result(tableName as text, asset as text) as table =>
let
Source = Excel.CurrentWorkbook(){[Name = tableName]}[Content],
Typed = Table.TransformColumnTypes(Source, {{"Symbol", type text}}),
Symbols = Table.SelectRows(Typed, each [Symbol] <> null and Text.Trim([Symbol]) <> ""),
WithData = Table.AddColumn(Symbols, "Data",
each fnSnapshot(asset, Text.Upper(Text.Trim([Symbol])))),
Expanded = Table.ExpandRecordColumn(WithData, "Data",
{"last_price", "change_percent", "bid", "ask", "prev_close", "last_timestamp", "error"},
{"Last", "Change %", "Bid", "Ask", "Prev close", "last_timestamp", "Error"}),
// Unix milliseconds (UTC) to an Excel date-time
WithTime = Table.AddColumn(Expanded, "Last trade (UTC)",
each if [last_timestamp] = null then null
else #datetime(1970, 1, 1, 0, 0, 0) + #duration(0, 0, 0, [last_timestamp] / 1000),
type datetime),
// The API sends 0.3883 for +0.39%; Excel percent format wants 0.003883
AsPercent = Table.TransformColumns(WithTime,
{{"Change %", each if _ = null then null else _ / 100, Percentage.Type}}),
Result = Table.RemoveColumns(AsPercent, {"last_timestamp"})
in
ResultManualStatusHandling is what keeps one typo from breaking the sheet: a 404 for an unknown symbol becomes a row with HTTP 404 in the Error column while the other rows load normally. RelativePath keeps the base URL constant, which is also what Power BI needs if you ever publish the same query there. The time conversion is the usual Unix-milliseconds arithmetic; the Unix timestamp guide covers the edge cases.
| Sheet | Symbol | Last | Change % | Bid | Ask | Last trade (UTC) |
|---|---|---|---|---|---|---|
| Stocks | US:KO | 88.151 | 0.39% | 88.15 | 88.2 | 2026-09-28 10:56:49 |
| FX | USDJPY | 157.092 | -0.09% | 157.0982 | 157.1052 | 2026-09-28 10:25:14 |
| Crypto | BTCUSD | 82,820 | -1.96% | 82,820 | 82,820.01 | 2026-09-28 10:25:13 |
FX pairs are six letters (EURUSD, USDJPY), crypto pairs are concatenated without a separator (BTCUSD, ETHUSD), and commodities use reference codes such as XAUUSD or NGASUSD. A fourth sheet for commodities is one more line: = fnQuotes("CommoditySymbols", "commodities"). For how FX quotes differ from a mid rate, see the exchange rate API guide.
Daily history in its own sheet
The snapshot answers "where is it now". For "how did it get here", call the bars endpoint, GET /stocks/agg/{symbol}/1/day/{from}/{to}, and turn its results array into a table. The same route serves 1, 5 and 15-minute, 1 and 4-hour, and daily bars, so an intraday sheet is the same query with 1/minute in the path.
let
Response = Web.Contents(
"https://api.tickerlayer.com",
[
RelativePath = "stocks/agg/US:KO/1/day/2026-01-02/2026-09-25",
Query = [sort = "asc", limit = "5000"],
Headers = [#"x-api-key" = TL_API_KEY]
]
),
Body = Json.Document(Response),
Bars = Table.FromRecords(Body[results]),
Renamed = Table.RenameColumns(Bars,
{{"o", "Open"}, {"h", "High"}, {"l", "Low"}, {"c", "Close"}, {"v", "Volume"}}),
// Daily bars are stamped at 00:00 UTC of the session date
WithDate = Table.AddColumn(Renamed, "Date",
each Date.From(#datetime(1970, 1, 1, 0, 0, 0) + #duration(0, 0, 0, [t] / 1000)), type date),
Result = Table.SelectColumns(WithDate, {"Date", "Open", "High", "Low", "Close", "Volume"})
in
Result| Date | Open | High | Low | Close | Volume |
|---|---|---|---|---|---|
| 2026-09-21 | 88.32 | 88.50 | 87.12 | 87.12 | 18,212,480 |
| 2026-09-22 | 87.60 | 88.97 | 87.455 | 88.61 | 17,355,834 |
| 2026-09-23 | 88.95 | 88.995 | 87.605 | 88.09 | 13,105,701 |
| 2026-09-24 | 88.82 | 89.36 | 88.10 | 88.10 | 13,252,762 |
| 2026-09-25 | 88.16 | 88.3183 | 87.555 | 87.81 | 12,261,067 |
Query values must be text, hence limit = "5000". One page holds up to 5,000 bars, which is about twenty years of daily data; for minute bars, pass the response's next_offset back as offset until it is null. The historical stock data guide explains official closes, split adjustments and session boundaries.
Refresh Excel stock prices on a timer without burning the quota
- Open the query propertiesData > Queries & Connections, right-click a query, Properties.
- Pick an intervalTick Refresh every N minutes. Tick Refresh data when opening the file so a morning open starts fresh.
- Keep typing while it runsLeave Enable background refresh on so the grid stays usable during a refresh.
The timer is blunt: it fires whenever the workbook is open, at 3 a.m. and on Sundays too, and every refresh calls the API once per symbol. That makes the interval the biggest lever on your monthly request count.
| Workbook | Refresh | Open for | Requests a month | Fits |
|---|---|---|---|---|
| 10 stocks | every 60 min | 8 h x 21 days | 1,680 | Free tier (3,000) |
| 10 stocks | every 5 min | 8 h x 21 days | 20,160 | Individual (250K) |
| 10 stocks + 10 FX + 10 crypto | every 5 min | 8 h x 21 days | 60,480 | Individual (250K) |
| 30 symbols, left open | every 5 min | 24 h x 30 days | 259,200 | Business (25M) |
The last row is the trap: the same 30-symbol workbook that fits an Individual plan during office hours overshoots it when someone leaves it open on a desktop over the month. Plan limits and the per-second ceiling are on the pricing page and in the limits docs.
Errors and what to do about them
| You see | Meaning | Fix |
|---|---|---|
HTTP 401 in the Error column | Key missing or wrong | Re-enter TL_API_KEY; no quotes or spaces |
HTTP 400 | Malformed symbol | Stocks need a prefix: US:KO, DE:SAP, JP:7203 |
HTTP 404 | Unknown symbol or wrong sheet | Check it with the symbol checker |
HTTP 429 | Per-second limit or monthly quota | Refresh less often, or split a very large table |
| Formula.Firewall message | Privacy levels block sending workbook data to a web source | Give the web source a privacy level in Data Source Settings, or for a private workbook use Query Options > Privacy > Ignore the Privacy Levels |
| A credentials prompt on every refresh | The source was saved with the wrong method | Data Source Settings > Edit Permissions > Anonymous |
Questions
How do I get live stock prices in Excel?
Use Data > Get Data > From Web with a stock API URL and an x-api-key header, load the JSON as a table, and set the query to refresh every few minutes in its properties.
Is the Excel stock data type real time?
No. Microsoft states that stock information in the data type is delayed and not for trading purposes. Use an API through Power Query when you need current quotes or intraday bars.
Can Power Query send an API key in a header?
Yes. Web.Contents accepts a Headers record, for example [#"x-api-key" = TL_API_KEY], and the From Web dialog has an Advanced section for header parameters.
How do I get historical stock prices in Excel?
STOCKHISTORY covers daily, weekly and monthly bars. For intraday bars or non-stock assets, call a bars endpoint such as /stocks/agg/US:KO/1/day/{from}/{to} from Power Query and convert the Unix timestamps to dates.
Why does my Power Query show a Formula.Firewall error?
Power Query refuses to send data from the workbook, here your symbol list, to a web source when their privacy levels conflict. Set the web source's privacy level in Data Source Settings or, for a workbook only you use, choose Query Options > Current Workbook > Privacy > Ignore the Privacy Levels.