Indices API reference
Levels and history for 18 indices
Overview
Levels and level history for 18 stock and volatility indices. Every request needs a Bearer key, and REST and MCP share one account quota.
An id is the index's standard code, such as SPX for the S&P 500, NDX for the Nasdaq-100 or DJI for the Dow Jones Industrial Average. Any case works. The list route returns every id with its name, region and currency.
A price carries the previous close, the change and the change in percent once the index's daily history is in.
Base URL: https:/. The MCP tool is indices, with one action for each route below.
Plans and limits
| What | Free | Basic | Pro |
|---|---|---|---|
| Latest level | 15 minutes late | Live | Live |
| Daily bars | Last 12 months | Last 5 years | Full history |
| One-minute bars | Not included | Last 12 months | Last 12 months |
| Index 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 levels are delayed by 15 minutes. A delayed answer carries delayed: true.
- History starts where we hold it. The S&P 500 starts in February 2023.
- A volatility index (VIX, OVX, GVZ) is not money, so its answers carry no currency.
- market_session is open or closed by the index's published hours. A closed index answers with its last level and is not stale. An open index is stale when its level 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.
- Bars carry no volume. 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 level. 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.
Indices we price
GET /v1/indices
Every indices 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 indices, action list.
Parameters
This route takes no parameters.
Response fields
| Field | Type | Sent | Meaning |
|---|---|---|---|
indices | list of object | Always | Every index we price, by id. |
indices[].id | string | Always | Index code, for example SPX. |
indices[].name | string | Always | The index's name. |
indices[].region | one of us, uk, europe, global, asia | Always | Where its members trade. |
indices[].currency | string | Optional | The currency the index is in. Left out for a volatility index, which is not money. |
curl -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/indices"{
"indices": [
{
"id": "SPX",
"name": "S&P 500",
"region": "us",
"currency": "USD"
},
{
"id": "VIX",
"name": "Cboe Volatility Index",
"region": "us"
}
]
}Latest price of one indices series
GET /v1/indices/{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. previous_ is the close of the previous trading day and change_ is in percent.
MCP: tool indices, action price.
Parameters
| Parameter | Where | Required | Type | Meaning |
|---|---|---|---|---|
id | path | Yes | string | Index id, for example SPX, NDX or DJI. Any case. |
Response fields
| Field | Type | Sent | Meaning |
|---|---|---|---|
id | string | Always | Index code. |
name | string | Always | Name as our symbol list holds it. |
currency | string | Optional | The currency the index is in. Left out for a volatility index, which is not money. |
price | string | Always | Last level of the index. |
previous_ | string | Optional | Close of the previous trading day. Left out when it is not known. |
change | string | Optional | price minus previous_. |
change_ | string | Optional | The change in percent (1.5 means 1.5%). Left out with change. |
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/indices/SPX/price"{
"id": "SPX",
"name": "S&P 500",
"currency": "USD",
"price": "7696.46",
"previous_close": "7681.2",
"change": "15.26",
"change_percent": "0.2",
"market_session": "open",
"timestamp": "2026-10-29T18:38:00Z",
"stale": false
}Latest prices for up to 50 indices series
GET /v1/indices/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. previous_ is the close of the previous trading day and change_ is in percent.
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 indices, 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 | Index code. |
prices[].name | string | Always | Name as our symbol list holds it. |
prices[].currency | string | Optional | The currency the index is in. Left out for a volatility index, which is not money. |
prices[].price | string | Always | Last level of the index. |
prices[].previous_ | string | Optional | Close of the previous trading day. Left out when it is not known. |
prices[].change | string | Optional | price minus previous_. |
prices[].change_ | string | Optional | The change in percent (1.5 means 1.5%). Left out with change. |
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/indices/prices?ids=SPX,NDX,DJI"{
"prices": [
{
"id": "SPX",
"name": "S&P 500",
"currency": "USD",
"price": "7696.46",
"previous_close": "7681.2",
"change": "15.26",
"change_percent": "0.2",
"market_session": "open",
"timestamp": "2026-10-29T18:38:00Z",
"stale": false
},
{
"id": "NDX",
"name": "Nasdaq-100",
"currency": "USD",
"price": "27310.5",
"previous_close": "27264.9",
"change": "45.6",
"change_percent": "0.17",
"market_session": "open",
"timestamp": "2026-10-29T18:38:00Z",
"stale": false
}
],
"errors": [
{
"id": "XX",
"code": "not_found"
}
]
}Price bars of one indices series
GET /v1/indices/{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.
MCP: tool indices, action bars.
Parameters
| Parameter | Where | Required | Type | Meaning |
|---|---|---|---|---|
id | path | Yes | string | Index id, for example SPX, NDX or DJI. Any case. |
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 | Optional | The currency the index is in. Left out for a volatility index, which is not money. |
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/indices/SPX/bars?interval=1d&start=2026-10-01"{
"id": "SPX",
"currency": "USD",
"bars": [
{
"timestamp": "2026-10-28T00:00:00Z",
"open": "7655.1",
"high": "7702.4",
"low": "7640.8",
"close": "7681.2"
}
],
"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