Funds API reference
Prices, NAV, history and distributions for ETFs and mutual funds
Overview
Prices, price history, distributions, profiles and search for ETFs and mutual funds. Every request needs a Bearer key, and REST and MCP share one account quota.
An ETF id is its listing id, such as ARCX:SPY. A mutual fund id is FUND, a colon and its ticker, such as FUND:VFIAX. A bare US ticker such as SPY also works when it names one US listing. A stock id on a fund route returns 404 not_found, and a fund id on a stock route moves here with a 301.
An ETF has a price. A mutual fund has a net asset value (NAV) once a day, in nav, previous_nav and date. The price answer has a type field that tells you which one you got.
Base URL: https:/. The MCP tool is funds, with one action for each route below.
Plans and limits
| What | Free | Basic | Pro |
|---|---|---|---|
| ETF latest price | 15 minutes late | Live | Live |
| Mutual fund NAV | Latest NAV | Latest NAV | Latest NAV |
| Daily bars and past distributions | Last 12 months | Last 5 years | Full history |
| One-minute bars (ETFs only) | Not included | Included, within the plan's history | Included, within the plan's history |
| Search and profile | Included | Included | Included |
See pricing for plan prices and monthly request limits. Each request counts as one call unless a route says otherwise.
- Free ETF prices are delayed by 15 minutes. A delayed answer carries delayed: true and leaves out the day volume.
- A mutual fund NAV is an end-of-day value and is never delayed. It is stale once it is older than the next business day.
- A mutual fund has no one-minute bars. Asking for them returns 400 invalid_interval.
- A mutual fund's daily bar is its NAV: open, high, low and close are the same number, and there is no volume.
- Basic reaches back 5 years and Pro has full history. When your plan cuts older rows, start comes back with the first date served.
- kind on a distribution is left out until our sources give it.
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.
Search ETFs and mutual funds by ticker or name
GET /v1/funds/search
Funds whose ticker or name starts with q, any case: an exact ticker first, then other tickers, then names. Optional type (etf or mutual_) and country (three letters) filters. Up to 50 a page, with next_. Read from our own symbol list, so it is open to every plan and counts one call.
MCP: tool funds, action search.
Parameters
| Parameter | Where | Required | Type | Meaning |
|---|---|---|---|---|
q | query | Yes | string | Start of a ticker or of a fund name, any case, at least 2 characters (for example vf or vanguard 500). Order: the exact ticker, other tickers, the start of a name, then the start of a later word of a name. |
country | query | No | string | Only funds in this country, three letters (USA). |
type | query | No | one of etf, mutual_fund | Only ETFs or only mutual funds. |
cursor | query | No | string | next_ of the previous page, with the same other parameters. |
Response fields
| Field | Type | Sent | Meaning |
|---|---|---|---|
results | list of object | Always | At most 50 a page. |
results[].id | string | Always | Canonical fund id. |
results[].symbol | string | Always | The fund's ticker. |
results[].name | string | Always | Name as our symbol list holds it. |
results[].type | one of etf, mutual_fund | Always | What kind of instrument it is. |
results[].mic | string | Optional | The exchange's four-letter code. Left out for a mutual fund (no exchange). |
results[].country | string | Optional | Country of the fund, three letters. |
results[].currency | string | Always | Currency of the prices. |
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/funds/search?q=vanguard&type=mutual_fund"{
"results": [
{
"id": "ARCX:SPY",
"symbol": "SPY",
"name": "SPDR S&P 500 ETF Trust",
"type": "etf",
"mic": "ARCX",
"country": "USA",
"currency": "USD"
},
{
"id": "FUND:VFIAX",
"symbol": "VFIAX",
"name": "Vanguard 500 Index Fund Admiral Shares",
"type": "mutual_fund",
"country": "USA",
"currency": "USD"
}
],
"next_cursor": null
}Profile of one fund
GET /v1/funds/{id}
The fund's symbol, name, type (etf or mutual_), exchange (ETFs only), country and currency, as our symbol list holds them. Open to every plan. Fields that are not known are left out.
MCP: tool funds, action profile.
Parameters
| Parameter | Where | Required | Type | Meaning |
|---|---|---|---|---|
id | path | Yes | string | Fund id. An ETF is its listing id: the exchange's four-letter code, a colon and the ticker (ARCX:SPY). A mutual fund is FUND, a colon and its ticker (FUND:VFIAX). A bare US ticker such as SPY also works when it names one US listing; the answer carries the canonical id. |
Response fields
| Field | Type | Sent | Meaning |
|---|---|---|---|
id | string | Always | Canonical fund id. |
symbol | string | Always | The fund's ticker. |
name | string | Always | Name as our symbol list holds it. |
type | one of etf, mutual_fund | Always | What kind of instrument it is. |
mic | string | Optional | The exchange's four-letter code. Left out for a mutual fund (no exchange). |
country | string | Optional | Country of the fund, three letters. |
currency | string | Always | Currency of the prices. |
curl -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/funds/ARCX:SPY"{
"id": "ARCX:SPY",
"symbol": "SPY",
"name": "SPDR S&P 500 ETF Trust",
"type": "etf",
"mic": "ARCX",
"country": "USA",
"currency": "USD"
}Latest price of one fund
GET /v1/funds/{id}/price
An ETF's last price, in the stock price shape plus type: etf (Free sees it 15 minutes late and the answer carries delayed: true), or a mutual fund's latest NAV (type: mutual_: nav, previous_, change, change_ and the NAV's date; an end-of-day value, never delayed, stale once it is older than the next business day). change_ is in percent. A stock id is 404 not_found.
MCP: tool funds, action price.
Parameters
| Parameter | Where | Required | Type | Meaning |
|---|---|---|---|---|
id | path | Yes | string | Fund id. An ETF is its listing id: the exchange's four-letter code, a colon and the ticker (ARCX:SPY). A mutual fund is FUND, a colon and its ticker (FUND:VFIAX). A bare US ticker such as SPY also works when it names one US listing; the answer carries the canonical id. |
Response fields
| Field | Type | Sent | Meaning |
|---|---|---|---|
id | string | Always | Canonical listing id. |
symbol | string | Always | Ticker on that exchange. |
currency | string | Always | Currency of every price field. Pence are converted to pounds. |
price | string | Optional | Last price, in currency. |
previous_ | string | Optional | Previous session's close. Left out when it is not known. |
change | string | Optional | price minus previous_. Left out with previous_. |
change_ | string | Optional | Change since previous_, in percent (1.5 means 1.5%). |
volume | string | Optional | Shares traded today. Left out when it is not known, and always left out on a delayed answer. |
market_ | one of pre, open, post, closed | Optional | State of the listing's exchange at timestamp: pre, open, post or closed. Only US exchanges have pre and post. |
timestamp | date-time | Optional | When the price was valid (UTC, whole seconds). |
stale | boolean | Always | True when the price is older than 15 minutes while the exchange is open, or could not be refreshed. A closed market is never stale. |
delayed | always true | Optional | Present, as true, only when the price is 15 minutes late (Free plan). |
type | one of etf, mutual_fund | Always | etf for an ETF, mutual_fund for a mutual fund. |
nav | string | Optional | Net asset value per share, in currency. |
previous_ | string | Optional | The NAV of the day before. Left out when it is not known. |
date | date | Optional | The day the NAV is for (YYYY-MM-DD). |
curl -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/funds/ARCX:SPY/price"{
"id": "ARCX:SPY",
"symbol": "SPY",
"currency": "USD",
"price": "578.12",
"previous_close": "576.40",
"change": "1.72",
"change_percent": "0.30",
"volume": "61230500",
"market_session": "open",
"timestamp": "2026-10-26T15:42:00Z",
"stale": false,
"type": "etf"
}Latest prices for up to 50 funds
GET /v1/funds/prices
Latest price of each listed fund: an ETF's last price (Free sees it 15 minutes late) or a mutual fund's latest NAV. 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 and cost nothing. change_ is in percent.
Send up to 50 ids, ETFs and mutual funds together. 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 funds, action prices.
Parameters
| Parameter | Where | Required | Type | Meaning |
|---|---|---|---|---|
ids | query | Yes | string | Comma-separated fund 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 | Canonical listing id. |
prices[].symbol | string | Always | Ticker on that exchange. |
prices[].currency | string | Always | Currency of every price field. Pence are converted to pounds. |
prices[].price | string | Optional | Last price, in currency. |
prices[].previous_ | string | Optional | Previous session's close. Left out when it is not known. |
prices[].change | string | Optional | price minus previous_. Left out with previous_. |
prices[].change_ | string | Optional | Change since previous_, in percent (1.5 means 1.5%). |
prices[].volume | string | Optional | Shares traded today. Left out when it is not known, and always left out on a delayed answer. |
prices[].market_ | one of pre, open, post, closed | Optional | State of the listing's exchange at timestamp: pre, open, post or closed. Only US exchanges have pre and post. |
prices[].timestamp | date-time | Optional | When the price was valid (UTC, whole seconds). |
prices[].stale | boolean | Always | True when the price is older than 15 minutes while the exchange is open, or could not be refreshed. A closed market is never stale. |
prices[].delayed | always true | Optional | Present, as true, only when the price is 15 minutes late (Free plan). |
prices[].type | one of etf, mutual_fund | Always | etf for an ETF, mutual_fund for a mutual fund. |
prices[].nav | string | Optional | Net asset value per share, in currency. |
prices[].previous_ | string | Optional | The NAV of the day before. Left out when it is not known. |
prices[].date | date | Optional | The day the NAV is for (YYYY-MM-DD). |
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 | one of not_found, not_covered, upstream_busy | Always | not_found: no such listing. not_covered: the exchange has no prices yet. upstream_busy: the price could not be fetched now; try again shortly. |
curl -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/funds/prices?ids=ARCX:SPY,FUND:VFIAX"{
"prices": [
{
"id": "ARCX:SPY",
"symbol": "SPY",
"currency": "USD",
"price": "578.12",
"previous_close": "576.40",
"change": "1.72",
"change_percent": "0.30",
"volume": "61230500",
"market_session": "open",
"timestamp": "2026-10-26T15:42:00Z",
"stale": false,
"type": "etf"
},
{
"id": "FUND:VFIAX",
"symbol": "VFIAX",
"currency": "USD",
"type": "mutual_fund",
"nav": "551.07",
"previous_nav": "549.80",
"change": "1.27",
"change_percent": "0.23",
"date": "2026-10-23",
"stale": false
}
],
"errors": [
{
"id": "FUND:NOPE",
"code": "not_found"
}
]
}Price bars of one fund
GET /v1/funds/{id}/bars
Daily (1d) or one-minute (1m) bars, oldest first, in the stock bar shape. A mutual fund's daily bar is its daily NAV (open, high, low and close are the NAV, no volume); it has no one-minute bars (400 invalid_interval). One-minute bars are for ETFs on Basic and Pro (Free gets 403 plan_upgrade_required). 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_; a cursor that no longer matches the prices is 409 cursor_expired. One call a page.
If a split or distribution changes the history between two pages, the cursor stops working and returns 409 cursor_expired.
MCP: tool funds, action bars.
Parameters
| Parameter | Where | Required | Type | Meaning |
|---|---|---|---|---|
id | path | Yes | string | Fund id. An ETF is its listing id: the exchange's four-letter code, a colon and the ticker (ARCX:SPY). A mutual fund is FUND, a colon and its ticker (FUND:VFIAX). A bare US ticker such as SPY also works when it names one US listing; the answer carries the canonical id. |
interval | query | No | one of 1d, 1m | 1d (one bar per session; a mutual fund's bar is its daily NAV) or 1m (one-minute bars: ETFs only, Basic and Pro only). 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 session whose bar is out. 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 other parameters. A cursor stops working when the prices behind it changed (a split or dividend): 409 cursor_expired. |
Response fields
| Field | Type | Sent | Meaning |
|---|---|---|---|
id | string | Always | Canonical listing id. |
currency | string | Always | Currency of every price. Pence become pounds. |
adjusted | boolean | Always | Always true: prices are adjusted for splits and dividends. A page and the next one always agree; if a split or dividend changes the history between two pages, the cursor stops working (409 cursor_expired) instead of mixing the two. |
start | string | Optional | Sent only when the plan raised the start: the first day (daily) or time (1m) served. |
end | string | Optional | Sent only on Free daily bars, when the 15-minute delay cut the range: the last day served. Today's bar reaches Free 15 minutes after the exchange closes. |
bars | list of object | Always | At most 5,000 bars a page. |
bars[].date | string | Optional | Session date (YYYY-MM-DD). Daily bars only; left out on one-minute bars. |
bars[].timestamp | date-time | Always | Start of the bar's period (UTC): the exchange's open 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. |
bars[].volume | string | Optional | Shares traded in the period. Left out when it is not known. |
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/funds/ARCX:SPY/bars?interval=1d&start=2026-10-01"{
"id": "ARCX:SPY",
"currency": "USD",
"adjusted": true,
"bars": [
{
"date": "2026-10-23",
"timestamp": "2026-10-23T13:30:00Z",
"open": "575.10",
"high": "577.60",
"low": "573.90",
"close": "576.40",
"volume": "48210300"
}
],
"next_cursor": null
}Distributions of one fund
GET /v1/funds/{id}/distributions
Distributions of one fund, newest ex-date first, paged. The plan sets how far back the past goes: Free 12 months, Basic 5 years, Pro full; start is sent only when older rows were cut off. Each request counts one call. An unknown id is 404 not_found.
MCP: tool funds, action distributions.
Parameters
| Parameter | Where | Required | Type | Meaning |
|---|---|---|---|---|
id | path | Yes | string | Fund id. An ETF is its listing id: the exchange's four-letter code, a colon and the ticker (ARCX:SPY). A mutual fund is FUND, a colon and its ticker (FUND:VFIAX). A bare US ticker such as SPY also works when it names one US listing; the answer carries the canonical id. |
cursor | query | No | string | next_ of the previous page, with the same other parameters. |
Response fields
| Field | Type | Sent | Meaning |
|---|---|---|---|
id | string | Always | Canonical fund id. |
start | date | Optional | First day the plan's history window reaches (Free 12 months, Basic 5 years, Pro full). Present only when older rows exist that the plan cannot see. |
distributions | list of object | Always | Newest ex-date first. |
distributions[].ex_ | date | Always | First day the fund trades without the distribution. |
distributions[].amount | string | Always | Distribution per share, in currency. |
distributions[].currency | string | Always | Currency of amount. |
distributions[].pay_ | date | Optional | Day it is paid. Left out when not known. |
distributions[].record_ | date | Optional | Day the holder must be on record. Left out when not known. |
distributions[].kind | one of income, capital_gain | Optional | What the distribution is. Left out where the source does not say. |
next_ | string | Always | Pass as cursor for the next page; null on the last page. |
curl -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/funds/ARCX:SPY/distributions"{
"id": "ARCX:SPY",
"distributions": [
{
"ex_date": "2026-09-18",
"amount": "1.78",
"currency": "USD",
"pay_date": "2026-10-30",
"record_date": "2026-09-18"
}
],
"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. |
cursor_ | 409 | The prices behind the cursor changed. Start again without the cursor. |
quota_ | 429 | Your plan has fewer calls left than the request needs. |
upstream_ | 503 | A price could not be fetched now. In a batch it is listed in errors. Try again shortly. |
area_ | 503 | This area cannot answer right now. |
Docs