What is actually in a row of commodity history?
/commodities/historical/{code} takes a commodity code in the path and four documented query parameters: interval, from, to and fmt (json or csv). Simple enough. Then you call it and discover that almost everything you assumed about the response is worth checking.
Call it and read the envelope
Requested on 2026-07-28 for WTI at monthly interval, the response came back like this:
{"meta": {"name": "Crude Oil Prices: West Texas Intermediate (WTI) - Cushing, Oklahoma",
"interval": "monthly", "unit": "Dollars per Barrel",
"offset": 0, "limit": 1000, "total": 486},
"data": [{"date": "2026-06-01", "value": 70.56}, ...],
"links": {"next": null}}
Two things deserve your attention immediately.
First, a row has a value, not a close. There is no open, no high, no low, no volume — one observation per period. The published OpenAPI schema for this path describes an array of OHLCV objects instead. The specification and the live response do not agree, and the live response is the authority. This is not unusual in any API of this size; specifications drift behind implementations. The habit that protects you is to print one raw row before you write a parser, every time, on every endpoint.
Second, rows arrive newest first. If your chart looks like it is running backwards, that is why.
meta.unit is the most important field
Three series from this one endpoint, all fetched on 2026-07-28:
WTI— "Crude Oil Prices: West Texas Intermediate (WTI) - Cushing, Oklahoma", unit "Dollars per Barrel"COPPER— "Global price of Copper", unit "U.S. Dollars per Metric Ton"ALL_COMMODITIES— "Global Price Index of All Commodities", unit "Index 2016 = 100"
One endpoint, three incompatible units: a dollar price per volume, a dollar price per mass, and a dimensionless index. Plot them on one axis and the chart is meaningless. Average them into a "commodity basket" and you have performed arithmetic on unlike things — adding barrels to tonnes to index points.
Percentage changes are comparable across all three, which is exactly why returns rather than levels are the common currency of cross-asset work.
The code list is a vocabulary, and it is not the documented one
These codes are not tickers. There is no exchange behind them and no SYMBOL.EXCHANGE structure — WTI is the name of a published statistical series.
The endpoint's own documentation lists example codes including WTI, BRENT, NATURAL_GAS, GOLD, SILVER, COPPER and about seventeen others. On 2026-07-28, GOLD and SILVER both returned 404 Symbol not found, while WTI, COPPER and ALL_COMMODITIES returned data — and all five still behaved the same way when re-checked a month later, so this is a standing gap rather than an outage.
Two lessons in that. The documented example list is a hint, not a contract — discover the real vocabulary by probing and record what worked. And note what a 404 means here: "no such code", which is a completely different failure from "no data in that date range", which elsewhere in this API returns an empty array. Code that treats every non-200 the same will report a missing metal and a missing decade identically.
Gold is in the API — under a different namespace
The honest completion of that story: gold is not missing from the platform, only from this path. A separate virtual exchange, COMM, held 245 active tickers on 2026-07-28, with Type values Commodity and Futures — including GC, "Gold (COMEX)", and ES, "S&P 500 E-Mini Futures".
So the same physical commodity is reachable through two unrelated namespaces with different codes, different response shapes and different underlying sources. That is a normal condition in market data, not a defect, and the practical rule is: when a code 404s, ask which namespace the instrument lives in before concluding it is not covered.
Dates you send, pages you get
Three more behaviours, all checked on 29 September 2026, and none of them visible until you count rows.
Leave out interval and you get monthly. meta.interval says so, which is the only place the default is written down.
Not every code has every interval. WTI, BRENT and NATURAL_GAS answer daily and weekly. CORN and COPPER answer both with 422 Invalid parameters and serve only monthly, quarterly and annual. A pipeline that asks every code for daily data fails on some of them for a reason the error does not name.
The date window does nothing. Neither from/to nor filter[from]/filter[to] narrows the answer: ask daily WTI for March and April 2020 and you get the newest 1,000 days of the whole series. The window you can set is the page: page[limit] and page[offset] work, meta.total says how many rows exist, and links.next is the next page's URL, without your key on it. To reach 2020 you walk the pages and filter the dates yourself.
Try it now
- Here is
/commodities/historical/WTI?interval=monthlyas it arrives: the envelope and the first two rows with every key they carry. Confirm for yourself whether a row hasvalueorclose, which way the dates run, and note the date beside the table's title as the day it was checked.
- Thirteen codes probed at
interval=monthlyon 28 September 2026:
| Code | Answer | meta.unit |
|---|---|---|
WTI |
data | Dollars per Barrel |
BRENT |
data | Dollars per Barrel |
NATURAL_GAS |
data | Dollars per Million BTU |
COPPER |
data | U.S. Dollars per Metric Ton |
ALUMINUM |
data | U.S. Dollars per Metric Ton |
WHEAT |
data | U.S. Dollars per Metric Ton |
CORN |
data | U.S. Dollars per Metric Ton |
SUGAR |
data | U.S. Cents per Pound |
COTTON |
data | U.S. Cents per Pound |
ALL_COMMODITIES |
data | Index 2016 = 100 |
GOLD |
404 Symbol not found | |
SILVER |
404 Symbol not found | |
COFFEE |
404 Symbol not found |
Turn it into the vocabulary your code will use, with the date. Then find the unit that is not a dollar amount at all, and the two that would be off by a factor of 100 if read as dollars.
- Here is daily WTI asked for
from=2020-03-01&to=2020-04-30. Compare the first date with the window you asked for, then dividemeta.totalbymeta.limitand round up: that is how many requests the whole daily history costs. Say how your loader finds April 2020, and where it gets the key for thelinks.nextcall.