Futures API reference
Prices and history for 29 futures
Overview
Prices and price history for 29 futures: commodities, stock indexes, Treasuries and currencies. Every request needs a Bearer key, and REST and MCP share one account quota.
An id is the exchange's root code, such as CL for WTI crude oil, GC for gold, ES for the E-mini S&P 500 or 6E for the euro. Any case works. The list route returns every id with its name, category, exchange, currency and unit.
Each series is the front month contract, joined end to end and not adjusted. On the day the front month changes, the price steps from one contract to the next. That step is in the data as it traded.
Base URL: https:/. The MCP tool is futures, with one action for each route below.
Plans and limits
| What | Free | Basic | Pro |
|---|---|---|---|
| Latest price | 15 minutes late | Live | Live |
| Daily bars | Last 12 months | Last 5 years | Full history, from April 2017 |
| One-minute bars | Not included | Last 12 months | Last 12 months |
| Series list | Included | Included | Included |
See pricing for plan prices and monthly request limits. Each request counts as one call unless a route says otherwise.
- Free prices are delayed by 15 minutes. A delayed answer carries delayed: true.
- A futures price has no previous close, change or volume. A change across a contract roll would be false, so the API does not work one out.
- Prices and bars are the front month as it traded. They step on each roll day: CL stepped by about 4 dollars on 19 October 2026. Do not read that step as a one day move.
- market_session is open or closed by the contract's published hours. A closed series answers with its last price and is not stale. An open series is stale when its price is more than 15 minutes old.
- A daily bar is one trading day and appears at 00:00 UTC. On Free it appears 15 minutes later, and end comes back when that cut your range. Today's partial bar is never shown.
- One-minute bars are kept for 12 months, and one request covers at most 5 days. Free gets 403 plan_upgrade_required.
- A batch holds up to 50 ids and counts one call for each id that returns a price. An unknown id is listed in errors and is not counted.
Endpoints
Every route is a GET and needs your key in the Authorization: Bearer header. The examples show the shape of an answer, not live data.
Futures we price
GET /v1/futures
Every futures series we price, by id, with its name and unit. Read from our own list, so it is open to every plan and counts one call.
MCP: tool futures, action list.
Parameters
This route takes no parameters.
Response fields
| Field | Type | Sent | Meaning |
|---|---|---|---|
futures | list of object | Always | Every series we price, by id. |
futures[].id | string | Always | Root code, for example CL. |
futures[].name | string | Always | The series' name. |
futures[].category | one of energy, metals, grains, livestock, equity_index, interest_rate, currency | Always | |
futures[].exchange | string | Always | The exchange that lists it. |
futures[].currency | string | Always | The currency of the price. |
futures[].unit | string | Always | What the price is per, for example usd_per_barrel or index_point. |
curl -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/futures"{
"futures": [
{
"id": "CL",
"name": "WTI Crude Oil",
"category": "energy",
"exchange": "NYMEX",
"currency": "USD",
"unit": "usd_per_barrel"
},
{
"id": "GC",
"name": "Gold",
"category": "metals",
"exchange": "COMEX",
"currency": "USD",
"unit": "usd_per_troy_ounce"
}
]
}Latest price of one futures series
GET /v1/futures/{id}/price
The latest price. market_ is open or closed by the series' hours; a closed series answers with its last price, and stale is true only when an open series has no price from the last 15 minutes. Free sees the price 15 minutes late and the answer carries delayed: true. A futures price has no previous close, change or volume: the series is the front month joined end to end and not adjusted, so it has a gap on each roll day.
MCP: tool futures, action price.
Parameters
| Parameter | Where | Required | Type | Meaning |
|---|---|---|---|---|
id | path | Yes | string | Futures id: the exchange's root code, for example CL, GC, ES or 6E. Any case. The series is the continuous front month, not adjusted: there is a gap in it on each roll day. |
Response fields
| Field | Type | Sent | Meaning |
|---|---|---|---|
id | string | Always | Root code. |
name | string | Always | Name as our symbol list holds it. |
exchange | string | Always | |
currency | string | Always | Currency of the prices. |
unit | string | Always | |
price | string | Always | Last price of the continuous front month. |
market_ | one of open, closed | Always | open while the series trades by its published hours, else closed. A closed series still answers with its last price; a holiday inside the hours reads open and shows as stale. |
timestamp | date-time | Always | When the price was valid (UTC, whole seconds). |
stale | boolean | Always | True when the series is open and its price is more than 15 minutes old, or could not be refreshed. A closed series is not stale. |
delayed | always true | Optional | Present, as true, only when the price is 15 minutes late (Free plan). |
curl -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/futures/CL/price"{
"id": "CL",
"name": "WTI Crude Oil",
"exchange": "NYMEX",
"currency": "USD",
"unit": "usd_per_barrel",
"price": "102.11",
"market_session": "open",
"timestamp": "2026-10-29T18:38:00Z",
"stale": false
}Latest prices for up to 50 futures series
GET /v1/futures/prices
Latest price of each listed id (Free sees them 15 minutes late). One call is counted for each id that returns a price, taken in one step: if fewer calls are left than ids that resolved, the whole request is refused with 429 quota_exceeded. Ids that return no price are listed in errors (as id) and cost nothing; if no id resolves the answer is still 200, with an empty prices. A futures price has no previous close, change or volume: the series is the front month joined end to end and not adjusted, so it has a gap on each roll day.
Send up to 50 ids. A batch is charged in one step: if you have fewer calls left than ids that resolve, the whole request returns 429 quota_exceeded.
MCP: tool futures, action prices.
Parameters
| Parameter | Where | Required | Type | Meaning |
|---|---|---|---|---|
ids | query | Yes | string | Comma-separated ids, at most 50 after duplicates are removed. Each id follows the same rules as the single-price route. One call is counted for each id that returns a price. |
Response fields
| Field | Type | Sent | Meaning |
|---|---|---|---|
prices | list of object | Always | One price per id that resolved, by id. |
prices[].id | string | Always | Root code. |
prices[].name | string | Always | Name as our symbol list holds it. |
prices[].exchange | string | Always | |
prices[].currency | string | Always | Currency of the prices. |
prices[].unit | string | Always | |
prices[].price | string | Always | Last price of the continuous front month. |
prices[].market_ | one of open, closed | Always | open while the series trades by its published hours, else closed. A closed series still answers with its last price; a holiday inside the hours reads open and shows as stale. |
prices[].timestamp | date-time | Always | When the price was valid (UTC, whole seconds). |
prices[].stale | boolean | Always | True when the series is open and its price is more than 15 minutes old, or could not be refreshed. A closed series is not stale. |
prices[].delayed | always true | Optional | Present, as true, only when the price is 15 minutes late (Free plan). |
errors | list of object | Always | Ids that returned no price. They are not counted against the quota. |
errors[].id | string | Always | The id as it was asked for. |
errors[].code | always "not_found" | Always | not_found: no such series. |
curl -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/futures/prices?ids=CL,GC,ES"{
"prices": [
{
"id": "CL",
"name": "WTI Crude Oil",
"exchange": "NYMEX",
"currency": "USD",
"unit": "usd_per_barrel",
"price": "102.11",
"market_session": "open",
"timestamp": "2026-10-29T18:38:00Z",
"stale": false
},
{
"id": "GC",
"name": "Gold",
"exchange": "COMEX",
"currency": "USD",
"unit": "usd_per_troy_ounce",
"price": "4021.3",
"market_session": "open",
"timestamp": "2026-10-29T18:38:00Z",
"stale": false
}
],
"errors": [
{
"id": "XX",
"code": "not_found"
}
]
}Price bars of one futures series
GET /v1/futures/{id}/bars
Daily (1d, one bar per trading day) or one-minute (1m) bars, oldest first. A day's bar is out at 00:00 UTC after the day (Free: 15 minutes later, and end comes back when that cut the range); today's partial bar is never shown. One-minute bars are for Basic and Pro (Free gets 403 plan_upgrade_required) and kept for 12 months. History: Free 12 months, Basic 5 years, Pro everything; start is sent back when the plan raised it. Up to 5,000 bars a page, with next_. One call a page.
Bars are raw front month prices, so a bar can open away from the close before it on a roll day.
MCP: tool futures, action bars.
Parameters
| Parameter | Where | Required | Type | Meaning |
|---|---|---|---|---|
id | path | Yes | string | Futures id: the exchange's root code, for example CL, GC, ES or 6E. Any case. The series is the continuous front month, not adjusted: there is a gap in it on each roll day. |
interval | query | No | one of 1d, 1m | 1d (one bar per trading day, dated by the day it belongs to) or 1m (one-minute bars, Basic and Pro only; kept for 12 months). Anything else is 400 invalid_interval. |
start | query | No | string | First bar. 1d: a date (YYYY-MM-DD), inclusive; left out means the oldest bar your plan reaches. 1m: a UTC time (YYYY-MM-DDTHH:MM:SSZ, or a date for 00:00 UTC), inclusive; left out means one day before end. |
end | query | No | string | Last bar. 1d: a date, inclusive; left out means the latest finished day. 1m: a UTC time, exclusive; left out means now, or five days after start. At most 5 days between start and end for 1m (400 range_too_large). |
cursor | query | No | string | next_ of the previous page, with the same parameters. |
Response fields
| Field | Type | Sent | Meaning |
|---|---|---|---|
id | string | Always | |
currency | string | Always | Currency of the prices. |
start | string | Optional | Sent only when the plan raised the start. |
end | string | Optional | Sent only on Free daily bars, when the 15-minute delay cut the range. |
bars | list of object | Always | At most 5,000 bars a page. |
bars[].timestamp | date-time | Always | Start of the bar's period (UTC): 00:00 of the trading day for a daily bar, the minute for a one-minute bar. |
bars[].open | string | Always | First price of the period. |
bars[].high | string | Always | Highest price of the period. |
bars[].low | string | Always | Lowest price of the period. |
bars[].close | string | Always | Last price of the period. |
next_ | string | Always | Pass it back to read the next page. Null on the last page. |
curl -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/futures/ES/bars?interval=1d&start=2017-04-03&end=2017-04-05"{
"id": "ES",
"currency": "USD",
"bars": [
{
"timestamp": "2017-04-03T00:00:00Z",
"open": "2357.5",
"high": "2363.25",
"low": "2340",
"close": "2354.5"
}
],
"next_cursor": null
}Errors
Errors use the shared problem shape with a request_. See Errors and quota for the shape and the quota headers. Failed requests are not charged.
| Code | Status | Meaning |
|---|---|---|
invalid_ | 400 | A parameter is missing, unknown, repeated or outside its range. |
invalid_ | 400 | An id or pair is not written the way this API expects. |
too_ | 400 | A batch holds more than 50 ids. |
invalid_ | 400 | The interval is not 1d or 1m, or not available for this instrument. |
range_ | 400 | The window is longer than the route allows. |
plan_ | 403 | The request needs a Basic or Pro plan. |
not_ | 404 | No such listing, fund or pair. |
invalid_ | 400 | The cursor is not one this request returned, or it comes from a different request. |
quota_ | 429 | Your plan has fewer calls left than the request needs. |
area_ | 503 | This area cannot answer right now. |
Docs