Growthvine's backend is two separate services, plus an MCP server on top of the Data API:
| Service | Base URL | Purpose |
|---|---|---|
| Auth Service | https://auth.growthvine.in |
Issues access tokens, manages API clients, publishes the public signing key |
| Data API | https://api.growthvine.in |
Fund reference data for Portfolio_Analysis, Fintech_One_Pager, Fund_Analysis, and the MCP server |
| MCP Server | https://mcp.growthvine.in |
Exposes the Data API as MCP tools for Claude and other agents |
Every Data API endpoint requires a Bearer token obtained from the Auth Service first.
This document is the single source of truth for both services and the MCP server — every example below is real, verified output from production (scheme 308, 462, 218540, 455574, and ISIN INE002A01018, specifically), not invented data. There's no separate Postman collection to keep in sync; if you want one, the example values in the table below are enough to build it from this doc.
| Placeholder | Value | What it is |
|---|---|---|
example schemeId |
308 |
Aditya Birla SL Large & Mid Cap Fund (Reg IDCW) — has no schemereturns row, useful for seeing the "all null" shape |
second schemeId (for compare) |
462 |
Aditya Birla SL Government Securities Fund — a debt fund, for contrast in /funds/compare |
schemeId with real returns data |
455574 |
Used for returns-history examples, since 308 would just return [] |
example isin |
INE002A01018 |
Reliance Industries Limited — one of 308's real holdings |
example sector |
Software & Services |
|
example category |
Equity: Large & Mid Cap |
|
| example date range | from=2026-01-01&to=2026-07-08 |
Client credentials are provisioned by a Growthvine admin.
POST /admin/clients.client_id and one-time client_secret.POST /oauth/token.Authorization: Bearer <access_token>.| Scope | Grants access to |
|---|---|
fund:read |
/funds/{schemeId}, /funds/{schemeId}/overview, /funds/{schemeId}/risk-metrics, /funds/{schemeId}/returns, /funds/{schemeId}/sectors, /funds/{schemeId}/holdings, /funds/{schemeId}/nav-history, /funds/{schemeId}/returns-history, /funds/{schemeId}/factsheet-history, /funds/{schemeId}/sector-history, /funds/{schemeId}/holdings-history, /funds/compare |
schemes:read |
/schemes, /schemes/all |
portfolio:read |
/portfolio-analysis/{schemeId} |
categories:read |
/categories, /categories/{category}/stats |
client_id.All Auth Service errors and all Data API auth/data-handler errors use the same JSON shape:
{ "error": "invalid_request", "message": "Invalid schemeId format" }
Common codes:
| Code | Meaning |
|---|---|
| 400 | Bad request |
| 401 | Missing / expired / invalid token |
| 403 | Valid token, missing required scope; or disabled client |
| 404 | Resource not found |
| 429 | Rate limited (token requests only) |
| 500 / 503 | Server, DB, or dependency failure |
client_id and client_secret in config/secrets, never in source control.401 even though your cached token looks valid, clear the cache, fetch one fresh token, and retry once.403 insufficient_scope as a provisioning issue, not a transient failure.Quick smoke test:
curl -s -X POST https://auth.growthvine.in/oauth/token \
-d "grant_type=client_credentials&client_id=<id>&client_secret=<secret>"
curl -s https://api.growthvine.in/schemes \
-H "Authorization: Bearer <access_token>"
Base URL: https://auth.growthvine.in
GET /healthInput: none.
Output — 200 OK:
{ "status": "ok" }
GET /readyInput: none.
Output — 200 OK (database and signing-key store both reachable):
{ "status": "ready", "checks": { "db": "ok", "jwks": "ok" } }
Output — 503 Service Unavailable (a dependency check failed):
{ "status": "not_ready", "checks": { "db": "error", "jwks": "ok" } }
GET /.well-known/jwks.jsonInput: none.
Output — 200 OK — the public signing key set, standard JWKS format, for JWT verification by the Data API and any other consumer:
{
"keys": [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "key-2026-06-30",
"n": "xGOr-H7A-PWG...",
"e": "AQAB"
}
]
}
POST /oauth/tokenInput:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=<client_id>&client_secret=<client_secret>
| Field | Required | Notes |
|---|---|---|
grant_type |
yes | Must be exactly client_credentials. |
client_id |
yes | |
client_secret |
yes |
Output — 200 OK:
{ "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0yMDI2LTA2LTMwIiwidHlwIjoiSldUIn0...", "token_type": "Bearer", "expires_in": 3600 }
Output — 400 Bad Request (missing fields):
{ "error": "invalid_request", "message": "grant_type, client_id, and client_secret are required." }
Output — 401 Unauthorized (unknown client or wrong secret):
{ "error": "invalid_client", "message": "Invalid client credentials." }
Output — 403 Forbidden (client disabled):
{ "error": "client_disabled", "message": "Client is disabled." }
Output — 429 Too Many Requests (more than 10 token requests/minute for this client_id):
Response header Retry-After: 60, body:
{ "error": "rate_limited", "message": "Too many token requests." }
POST /admin/clientsAdmin only.
Input:
POST /admin/clients
X-Admin-Key: <admin key>
Content-Type: application/json
{ "client_id": "fund-analysis-svc", "client_name": "Fund Analysis Service", "scopes": "fund:read schemes:read categories:read" }
| Field | Required | Notes |
|---|---|---|
client_id |
yes | Must be unique. |
client_name |
yes | Human-readable label, shown in access logs. |
scopes |
yes | Space-separated list of scopes to grant. |
Output — 201 Created:
{ "client_id": "fund-analysis-svc", "client_secret": "gvs_h5NblZj4HnKwVlxgU5av66oQ0d-_sfCnY3PdQWrEYgg" }
The secret is only ever returned this once — it's stored as a hash server-side, not recoverable afterward. If lost, disable the client and create a new one.
Output — 401 Unauthorized (missing/wrong X-Admin-Key):
{ "error": "invalid_admin_key", "message": "Admin key is missing or invalid." }
Output — 400 Bad Request:
{ "error": "invalid_request", "message": "client_id, client_name, and scopes are required." }
DELETE /admin/clients/{client_id}Admin only. Disables a client — its existing tokens keep working until they expire, but it can no longer mint new ones.
Input: path parameter client_id; header X-Admin-Key. No body.
Output — 204 No Content on success (empty body).
Output — 401 Unauthorized:
{ "error": "invalid_admin_key", "message": "Admin key is missing or invalid." }
POST /admin/keys/rotateAdmin only. Generates a new signing key and publishes it alongside the old one at /.well-known/jwks.json (so tokens already issued under the old key remain verifiable until they expire).
Input: header X-Admin-Key. No body.
Output — 200 OK:
{ "kid": "key-2026-07-15" }
Output — 401 Unauthorized:
{ "error": "invalid_admin_key", "message": "Admin key is missing or invalid." }
Base URL: https://api.growthvine.in. Every endpoint below except /health and /ready requires Authorization: Bearer <access_token>.
GET /healthInput: none, no auth required.
Output — 200 OK:
{ "status": "ok" }
GET /readyInput: none, no auth required.
Output — 200 OK (both databases and JWKS reachable):
{ "status": "ready", "checks": { "db": "ok", "historyDb": "ok", "jwks": "ok" } }
db is Investwell (mint_scheme_transfer), historyDb is the append-only investwellhistory database backing the *-history endpoints below.
Output — 503 Service Unavailable:
{ "status": "not_ready", "checks": { "db": "ok", "historyDb": "error", "jwks": "ok" } }
Full composite payloads for the two page-shaped consumers. Kept as-is for those existing integrations; new consumers should compose the granular endpoints below instead.
GET /funds/{schemeId}Used by Fintech_One_Pager. Scope: fund:read.
Input:
| Param | In | Required | Notes |
|---|---|---|---|
schemeId |
path | yes | Numeric scheme ID. |
No query params — this endpoint no longer accepts from/to (see note below).
Output — 200 OK:
{
"overview": {
"schemeId": 308,
"schemeName": "Aditya Birla SL Large & Mid Cap Fund",
"schemePlanName": "Aditya Birla SL Large & Mid Cap Fund Reg IDCW",
"isinno": "INF209K01157",
"objective": "The scheme seeks to achieve long-term growth of capital, at relatively moderate levels of risk through a diversified research based investment in Large & Midcap companies.",
"fundManager": "Vishal Gajwani",
"category": "Equity: Large & Mid Cap",
"inceptionDate": "2018-10-18",
"aum": 5666,
"expenseRatio": 1.58,
"exitLoad": "1% for redemption within 90 days",
"investmentType": "X",
"dividendFrequency": "yearly",
"taxStat": "E",
"closeDate": null,
"nfoCloseDate": null,
"weekHigh": 143.86,
"weekLow": 118.16,
"bseCode": "B201D-DP",
"nseCode": "201D",
"groupId": "157",
"amcId": "7",
"benchmarkId": "56305",
"dirPlan": "0",
"active": true
},
"assetAllocation": {
"equity": 99.1, "debt": 0.9, "globalEquity": 0, "gold": 0, "other": 0,
"marketCap": { "large": 44.01, "mid": 43.37, "small": 12.62 }
},
"riskMetrics": {
"standardDeviation": 16.11, "sharpeRatio": 0.47, "sortinoRatio": 0.59,
"alpha": -0.02, "beta": 1.05, "peRatio": 29, "pbRatio": 4.16, "mean": 13.46,
"minInitialInv": null, "modifiedDuration": 0, "turnOver": null, "ytm": 0
},
"performance": {
"pointToPoint": { "1Month": null, "6Month": null, "1Year": null, "3Year": null, "5Year": null, "7Year": null, "10Year": null, "sinceInception": null },
"quartiles": { "1Year": null, "3Year": null, "5Year": null },
"sipReturns": { "1Year": null, "3Year": null, "5Year": null },
"rankings": { "1Year": null, "3Year": null }
},
"portfolio": {
"sectors": [
{ "sector": "Banking & Financial", "allocation": 14.11 },
{ "sector": "Automobile", "allocation": 10.17 }
],
"topHoldings": [
{ "isin": "INE090A01021", "name": "ICICI Bank Limited", "instrument": "Equity", "allocation": 4.37, "sector": "Banking & Financial" }
]
}
}
Output — 404 Not Found:
{ "error": "not_found", "message": "Fund not found" }
Output — 400 Bad Request (non-numeric schemeId):
{ "error": "invalid_request", "message": "Invalid schemeId format" }
Notes:
GET /funds/{schemeId}/nav-history for real history instead.topHoldings includes sector.GET /schemes/allLegacy lightweight dump of every scheme. Scope: schemes:read. New consumers should prefer GET /schemes.
Input: none.
Output — 200 OK (array, ~9,400 entries in production):
[
{ "schemeId": 308, "schemeName": "Aditya Birla SL Large & Mid Cap Fund", "isin": "INF209K01157", "category": "Equity: Large & Mid Cap", "dirPlan": "0" },
{ "schemeId": 218540, "schemeName": "HDFC Flexi Cap Fund", "isin": "INF179K01VL5", "category": "Equity: Flexi Cap", "dirPlan": "1" }
]
GET /portfolio-analysis/{schemeId}Used by Portfolio_Analysis for the Monthly Client Portfolio Report and Prospect Diagnostics Report. Scope: portfolio:read.
Input:
| Param | In | Required | Notes |
|---|---|---|---|
schemeId |
path | yes | Numeric scheme ID. |
No query params — includeNav has been removed, it no longer does anything.
Output — 200 OK:
{
"fundMetadata": {
"category": "Equity: Large & Mid Cap", "risk": 6,
"equityPercent": 99.1, "debtPercent": 0.9, "goldPercent": 0,
"fsid": "27", "aum": 5666, "fundManager": "Vishal Gajwani"
},
"riskMetrics": { "standardDeviation": 16.11, "sharpeRatio": 0.47, "sortinoRatio": 0.59, "alpha": -0.02, "beta": 1.05 },
"returns": {
"pointToPoint": { "1Year": null, "3Year": null, "5Year": null },
"quartiles": { "1Year": null, "3Year": null, "5Year": null },
"rankings": { "1Year": null, "3Year": null, "5Year": null },
"sipReturns": { "1Year": null, "3Year": null, "5Year": null }
},
"holdings": [
{ "isin": "INE090A01021", "name": "ICICI Bank Limited", "instrument": "Equity", "allocation": 4.37, "sector": "Banking & Financial" },
{ "isin": null, "name": "Clearing Corporation of India Limited", "instrument": "Trep", "allocation": 0.99, "sector": "Cash" }
],
"peerCount": 190
}
Note: holdings here is the complete list (not top-10, unlike GET /funds/{schemeId}/holdings's default) — the overlap matrix across a client's funds needs every position.
Output — 404 Not Found:
{ "error": "not_found", "message": "Fund not found" }
Notes:
/funds/{schemeId} above) — use GET /funds/{schemeId}/nav-history for real history instead.GET /schemesReal search/filter/sort/paginate endpoint. Scope: schemes:read.
Input (all query params, all optional):
| Param | Notes |
|---|---|
q |
Case-insensitive substring match against scheme name. |
category |
Case-insensitive substring match against category. |
dirPlan |
Exact match, "0" (regular) or "1" (direct). |
investmentType |
Exact match. |
taxStat |
Exact match. |
amcid |
Exact match on the opaque AMC ID. |
activeOnly |
true or false — filters to schemes with closeDate IS NULL. |
sortBy |
One of schemeName (default), category, aum, expenseRatio, 1Year, 3Year, 5Year, 10Year, sharpeRatio, standardDeviation. |
sortDir |
asc (default) or desc. |
limit |
1–200, default 50. |
offset |
Default 0. |
Output — 200 OK (real example: q=Flexi Cap, sortBy=1Year, sortDir=desc, limit=2):
{
"total": 212,
"limit": 2,
"offset": 0,
"schemes": [
{
"schemeId": 931,
"schemeName": "DSP Flexi Cap Fund",
"schemePlanName": "DSP Flexi Cap Fund Reg IDCW",
"isin": "INF740K01011",
"category": "Equity: Flexi Cap",
"dirPlan": "0",
"investmentType": "X",
"taxStat": "E",
"closeDate": null,
"amcId": "35",
"aum": 11798,
"expenseRatio": 1.48,
"1Year": null, "3Year": null, "5Year": null, "10Year": null,
"sharpeRatio": 0.51,
"standardDeviation": 15.42
},
{
"schemeId": 4515,
"schemeName": "HDFC Flexi Cap Fund",
"schemePlanName": "HDFC Flexi Cap Fund Reg IDCW",
"isin": "INF179K01582",
"category": "Equity: Flexi Cap",
"dirPlan": "0",
"investmentType": "X",
"taxStat": "E",
"closeDate": null,
"amcId": "70",
"aum": 101822,
"expenseRatio": 1.09,
"1Year": null, "3Year": null, "5Year": null, "10Year": null,
"sharpeRatio": 0.85,
"standardDeviation": 13.11
}
]
}
Output — 400 Bad Request:
{ "error": "invalid_request", "message": "unsupported sortBy value" }
All require fund:read and take one path param, schemeId (numeric).
GET /funds/{schemeId}/overviewInput: schemeId (path).
Output — 200 OK:
{
"overview": {
"schemeId": 308, "schemeName": "Aditya Birla SL Large & Mid Cap Fund",
"schemePlanName": "Aditya Birla SL Large & Mid Cap Fund Reg IDCW",
"isinno": "INF209K01157", "fundManager": "Vishal Gajwani",
"category": "Equity: Large & Mid Cap", "inceptionDate": "2018-10-18",
"aum": 5666, "expenseRatio": 1.58, "exitLoad": "1% for redemption within 90 days",
"active": true
},
"assetAllocation": {
"equity": 99.1, "debt": 0.9, "globalEquity": 0, "gold": 0, "other": 0,
"marketCap": { "large": 44.01, "mid": 43.37, "small": 12.62 }
},
"peerCount": 190
}
GET /funds/{schemeId}/risk-metricsInput: schemeId (path).
Output — 200 OK:
{
"standardDeviation": 16.11, "sharpeRatio": 0.47, "sortinoRatio": 0.59,
"alpha": -0.02, "beta": 1.05, "peRatio": 29, "pbRatio": 4.16, "mean": 13.46,
"minInitialInv": null, "modifiedDuration": 0, "turnOver": null, "ytm": 0
}
A small number of newer/thin-history funds show literal 0 instead of null for stats Investwell hasn't computed yet — that's source data passed through as-is.
GET /funds/{schemeId}/returnsInput: schemeId (path).
Output — 200 OK (this example scheme has no schemereturns row, so every value is null — a valid response, not an error):
{
"pointToPoint": { "1Day": null, "7Day": null, "15Day": null, "30Day": null, "3Month": null, "6Month": null, "1Year": null, "2Year": null, "3Year": null, "5Year": null, "7Year": null, "10Year": null, "15Year": null, "20Year": null, "25Year": null, "sinceInception": null },
"ranks": { "1Year": null, "3Year": null, "5Year": null, "10Year": null, "15Year": null, "20Year": null, "25Year": null },
"quartiles": { "1Year": null, "3Year": null, "5Year": null, "10Year": null, "15Year": null, "20Year": null },
"sipReturns": { "1Year": null, "3Year": null, "5Year": null, "10Year": null, "15Year": null, "20Year": null, "sinceInception": null }
}
GET /funds/{schemeId}/sectorsInput: schemeId (path).
Output — 200 OK:
[
{ "sector": "Banking & Financial", "allocation": 14.11 },
{ "sector": "Automobile", "allocation": 10.17 },
{ "sector": "Retail", "allocation": 7.36 },
{ "sector": "Industrial Products", "allocation": 7.04 },
{ "sector": "Pharma & Biotech", "allocation": 5.18 }
]
Empty array (not null) if the fund has no factsheet yet.
GET /funds/{schemeId}/holdingsInput:
| Param | In | Required | Notes |
|---|---|---|---|
schemeId |
path | yes | |
limit |
query | no | Positive integer (default 10), or the literal string all. |
Output — 200 OK (limit=3):
{
"totalReturned": 3,
"holdings": [
{ "isin": "INE090A01021", "name": "ICICI Bank Limited", "instrument": "Equity", "allocation": 4.37, "sector": "Banking & Financial" },
{ "isin": "INE062A01020", "name": "State Bank of India", "instrument": "Equity", "allocation": 3.51, "sector": "Banking & Financial" },
{ "isin": "INE949L01017", "name": "AU Small Finance Bank Limited", "instrument": "Equity", "allocation": 3.32, "sector": "Banking & Financial" }
]
}
Output — 400 Bad Request:
{ "error": "invalid_request", "message": "limit must be a positive integer or 'all'" }
All require fund:read and read from investwellhistory (the append-only history database), not live Investwell. from/to are optional YYYY-MM-DD bounds on every endpoint below, defaulting to the last 5 years.
GET /funds/{schemeId}/nav-historyInput:
| Param | In | Required |
|---|---|---|
schemeId |
path | yes |
from |
query | no |
to |
query | no |
Output — 200 OK:
[
{ "date": "2026-06-08", "nav": 128.47 },
{ "date": "2026-06-09", "nav": 129.95 },
{ "date": "2026-06-10", "nav": 129.02 },
{ "date": "2026-07-07", "nav": 136.15 }
]
Empty array [] for a date range with no captured data (e.g. before the backfill/ingest job started tracking this scheme).
GET /funds/{schemeId}/returns-historyInput:
| Param | In | Required | Notes |
|---|---|---|---|
schemeId |
path | yes | |
metric |
query | no | One of 1Day, 7Day, 15Day, 30Day, 3Month, 6Month, 1Year, 2Year, 3Year, 5Year, 7Year, 10Year, 15Year, 20Year, 25Year, sinceInception (default 1Year), or all. |
from |
query | no | |
to |
query | no |
Output — 200 OK (single metric, metric=1Year):
[
{ "date": "2026-07-08", "value": 5.7 }
]
Output — 200 OK (metric=all — one full row per snapshot date):
[
{
"date": "2026-07-08",
"pointToPoint": { "1Day": null, "7Day": null, "1Year": 5.7, "3Year": null, "sinceInception": null },
"ranks": { "1Year": null, "3Year": null },
"quartiles": { "1Year": null, "3Year": null },
"sipReturns": { "1Year": null, "3Year": null },
"assetMix": { "equity": 98.2, "debt": 1.8, "globalEquity": 0, "gold": 0, "other": 0 }
}
]
Output — 400 Bad Request:
{ "error": "invalid_request", "message": "Unknown metric; use one of the point-to-point return periods or 'all'" }
Note: returns_history only gets a new row when the underlying value actually changes (hash-compared in the 2-hourly ingest job), not one row per poll — this is a real change timeline, not artificial daily noise.
GET /funds/{schemeId}/factsheet-historyInput:
| Param | In | Required |
|---|---|---|
schemeId |
path | yes |
from |
query | no |
to |
query | no |
Output — 200 OK:
[
{
"date": "2026-07-08",
"fundManager": "Vishal Gajwani",
"expenseRatio": 1.58,
"exitLoad": "1% for redemption within 90 days",
"corpus": 5666,
"largeCap": 44.01,
"midCap": 43.37,
"smallCap": 12.62,
"standardDeviation": 16.11,
"sharpeRatio": 0.47
}
]
Only gets a new row when something in it actually changes — a fund-manager change or expense-ratio revision shows up as a clean step in the series.
GET /funds/{schemeId}/sector-historyInput:
| Param | In | Required | Notes |
|---|---|---|---|
schemeId |
path | yes | |
sector |
query | no | Omit for the full sector breakdown per snapshot date instead of one sector's trend (can be a lot of rows). |
from |
query | no | |
to |
query | no |
Output — 200 OK (sector=Software & Services):
[
{ "date": "2026-07-08", "sector": "Software & Services", "allocation": 4.54 }
]
GET /funds/{schemeId}/holdings-historyInput:
| Param | In | Required | Notes |
|---|---|---|---|
schemeId |
path | yes | |
isin |
query | yes | Holdings history is tracked per security, not as a whole-portfolio time series. Get it from GET /funds/{schemeId}/holdings first. |
from |
query | no | |
to |
query | no |
Output — 200 OK (isin=INE002A01018):
[
{ "date": "2026-07-08", "isin": "INE002A01018", "name": "Reliance Industries Limited", "instrument": "Equity", "sector": "Petroleum Products", "allocation": 0.73 }
]
Output — 400 Bad Request:
{ "error": "invalid_request", "message": "isin query param is required" }
GET /funds/compare?schemeIds=1,2,3Scope: fund:read.
Input:
| Param | In | Required | Notes |
|---|---|---|---|
schemeIds |
query | yes | Comma-separated, 2–10 unique scheme IDs. Duplicates silently deduped. |
metrics |
query | no | all for the full returns map per fund; omit for a compact default subset. |
Output — 200 OK (schemeIds=308,462):
{
"funds": [
{
"overview": { "schemeId": 308, "schemeName": "Aditya Birla SL Large & Mid Cap Fund", "fundManager": "Vishal Gajwani", "category": "Equity: Large & Mid Cap", "aum": 5666, "expenseRatio": 1.58, "active": true },
"assetAllocation": { "equity": 99.1, "debt": 0.9, "marketCap": { "large": 44.01, "mid": 43.37, "small": 12.62 } },
"riskMetrics": { "standardDeviation": 16.11, "sharpeRatio": 0.47, "alpha": -0.02, "beta": 1.05 },
"returns": {
"pointToPoint": { "1Year": null, "3Year": null, "5Year": null, "10Year": null, "sinceInception": null },
"ranks": { "1Year": null, "3Year": null, "5Year": null, "10Year": null },
"quartiles": { "1Year": null, "3Year": null, "5Year": null, "10Year": null },
"sipReturns": { "1Year": null, "3Year": null, "5Year": null, "10Year": null }
}
},
{
"overview": { "schemeId": 462, "schemeName": "Aditya Birla SL Government Securities Fund", "fundManager": "Bhupesh Bameta", "category": "Debt: Gilt Fund", "aum": 1425, "expenseRatio": 0.97, "active": true },
"assetAllocation": { "equity": 0, "debt": 100, "marketCap": { "large": null, "mid": null, "small": null } },
"riskMetrics": { "standardDeviation": 4.22, "sharpeRatio": -0.29, "alpha": 0, "beta": 0 },
"returns": {
"pointToPoint": { "1Year": null, "3Year": null, "5Year": null, "10Year": null, "sinceInception": null },
"ranks": { "1Year": null, "3Year": null, "5Year": null, "10Year": null },
"quartiles": { "1Year": null, "3Year": null, "5Year": null, "10Year": null },
"sipReturns": { "1Year": null, "3Year": null, "5Year": null, "10Year": null }
}
}
],
"notFound": []
}
IDs that don't exist are reported in notFound rather than failing the whole request.
Output — 400 Bad Request (fewer than 2 unique IDs):
{ "error": "invalid_request", "message": "schemeIds must contain at least 2 unique ids" }
Output — 400 Bad Request (more than 10 unique IDs):
{ "error": "invalid_request", "message": "schemeIds must contain at most 10 unique ids" }
GET /funds/history-compare?schemeIds=1,2,3Bulk real NAV history for multiple funds in one call — added specifically so MCP/agent analysis doesn't need to loop GET /funds/{schemeId}/nav-history per fund. Scope: fund:read.
Input:
| Param | In | Required | Notes |
|---|---|---|---|
schemeIds |
query | yes | Comma-separated, 2–10 unique scheme IDs. Duplicates silently deduped. Same validation as /funds/compare. |
from |
query | no | YYYY-MM-DD. |
to |
query | no | YYYY-MM-DD. |
Default lookback is 90 days, not the 5-year default used by every other history endpoint — deliberately shorter, since a multi-fund raw NAV series gets large fast (10 funds × 5 years is on the order of 12,000+ data points). Pass from/to explicitly for a longer window.
Output — 200 OK (schemeIds=308,462, default 90-day lookback):
{
"funds": [
{ "schemeId": 308, "navHistory": [ { "date": "2026-04-09", "nav": 128.16 }, { "date": "2026-07-07", "nav": 136.15 } ] },
{ "schemeId": 462, "navHistory": [ { "date": "2026-04-09", "nav": 10.81 }, { "date": "2026-07-07", "nav": 10.94 } ] }
],
"notFound": []
}
Output — 200 OK (one ID doesn't exist):
{
"funds": [ { "schemeId": 308, "navHistory": [ ] } ],
"notFound": [ 999999999 ]
}
Output — 400 Bad Request: same shape/messages as /funds/compare (schemeIds must contain at least 2 unique ids, etc).
GET /categoriesScope: categories:read.
Input: none.
Output — 200 OK:
[
{ "category": "Equity: Flexi Cap", "schemeCount": 212 },
{ "category": "Equity: Large & Mid Cap", "schemeCount": 191 },
{ "category": "Equity: Small Cap", "schemeCount": 166 }
]
Output — 403 Forbidden (client lacks categories:read):
{ "error": "insufficient_scope", "message": "The access token does not include the required scope." }
GET /categories/{category}/statsScope: categories:read.
Input:
| Param | In | Required | Notes |
|---|---|---|---|
category |
path | yes | Must match exactly (case-sensitive, URL-encode spaces/colons) — this is not a substring search, unlike /schemes?category=. |
Output — 200 OK:
{
"category": "Equity: Large & Mid Cap",
"schemeCount": 191,
"metrics": {
"1Year": { "average": 8.2, "median": 7.9, "min": -3.1, "max": 22.4 },
"3Year": { "average": 14.1, "median": 13.6, "min": 2.2, "max": 28.9 },
"5Year": { "average": null, "median": null, "min": null, "max": null },
"10Year": { "average": null, "median": null, "min": null, "max": null },
"expenseRatio": { "average": 1.31, "median": 1.42, "min": 0.3, "max": 2.5 },
"aum": { "average": 8452.6, "median": 4200.0, "min": 12.4, "max": 42000.1 },
"sharpeRatio": { "average": 0.61, "median": 0.58, "min": -0.2, "max": 1.4 },
"standardDeviation": { "average": 15.9, "median": 15.4, "min": 10.1, "max": 22.3 }
}
}
Output — 404 Not Found (category matches zero schemes — including typos/case mismatches):
{ "error": "not_found", "message": "Category not found" }
amcid and schbroadbenchmark are exposed only as opaque IDs for now./schemes over /schemes/all.Base URL: https://mcp.growthvine.in — a Python (FastMCP) server that exposes the Data API as MCP tools for Claude and other MCP-compatible agents, instead of requiring agents to know the REST API directly. It doesn't reimplement Growthvine's own auth — it just resolves credentials per-call and routes to the Data API.
Add a custom connector with server URL:
https://mcp.growthvine.in/mcp
The /mcp path is required — the bare domain (https://mcp.growthvine.in) returns 404 and Claude.ai will report "no MCP server was found at the provided URL" even though the OAuth handshake itself succeeds. This is the FastMCP library's default mount path, not something configurable here.
The connector flow uses OAuth2 (ENABLE_OAUTH=true in production) — Claude.ai discovers /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource automatically, redirects for authorization, and thereafter calls tools with the resulting token. No manual header configuration needed for a Claude.ai connector.
Each tool call resolves Growthvine credentials in this order:
X-Growthvine-Client-Id / X-Growthvine-Client-Secret request headers — for local testing or MCP clients that can set custom headers directly.DEFAULT_CLIENT_ID / DEFAULT_CLIENT_SECRET server config — the owner's own credential, so a personal/local connector needs zero per-request configuration.This is worth being explicit about: every tool below is callable by every connected client. There is no per-tool or per-client allowlist at the MCP layer. The MCP OAuth flow (the one Claude.ai's connector uses) issues its own token with its own scopes field for protocol compliance, but server.py never actually reads that field to gate anything — it's stored alongside the resolved growthvine_client_id/growthvine_client_secret and otherwise ignored.
The only access control that actually does anything is downstream, at the Data API: whatever Growthvine scopes (fund:read, schemes:read, portfolio:read, categories:read) are baked into the resolved credential's token is what determines whether a given call succeeds — not whether the tool is visible or invokable. If your credential lacks categories:read, list_categories/get_category_stats are still callable, they just fail with the same insufficient_scope error you'd get calling GET /categories directly. There's no way to hide a tool from a client while still connecting them to this server — access is entirely a property of which Growthvine client credential gets resolved for the call, not of the MCP connection itself.
19 tools total. All of them (except get_analysis_guide, which returns static guidance and makes no Data API call) call straight through to the Data API endpoints documented above — same scopes, same error semantics, same live-vs-history data split, same JSON shapes.
get_analysis_guide()No parameters, no Growthvine credential needed. Returns the structured methodology guide reproduced in full under "Analysis Methodology" below — call this first before analyzing/comparing/ranking multiple funds or computing anything derived (CAGR, rolling returns, drawdown, plan-type filtering).
list_schemes(category="", search="", limit=50)Maps to GET /schemes/all (cached, filtered client-side). Cheap id/name/ISIN/category/dirPlan lookup only — no returns/risk/AUM data. Scope: schemes:read.
Output:
{
"total_matches": 6,
"returned": 3,
"schemes": [
{ "schemeId": 4522, "schemeName": "HDFC Flexi Cap Fund", "isin": "INF179K01608", "category": "Equity: Flexi Cap", "dirPlan": "0" }
]
}
get_fund_details(scheme_id)Maps to GET /funds/{schemeId}. Full composite: overview, asset allocation, risk metrics, performance, sector/holdings breakdown. No NAV history — call get_nav_history separately. Scope: fund:read. Output: identical shape to GET /funds/{schemeId} above.
get_portfolio_analysis(scheme_id)Maps to GET /portfolio-analysis/{schemeId}. Fund metadata, risk metrics, returns/quartiles/rankings/SIP returns, complete holdings list, peer count. No NAV history, same reason. Scope: portfolio:read. Output: identical shape to GET /portfolio-analysis/{schemeId} above.
get_fund_overview(scheme_id)Maps to GET /funds/{schemeId}/overview. Scope: fund:read. Output: identical shape to that endpoint above (overview + assetAllocation + peerCount).
get_fund_risk_metrics(scheme_id)Maps to GET /funds/{schemeId}/risk-metrics. Scope: fund:read. Output: identical shape (standardDeviation, sharpeRatio, sortinoRatio, alpha, beta, peRatio, pbRatio, mean, minInitialInv, modifiedDuration, turnOver, ytm).
get_fund_returns(scheme_id)Maps to GET /funds/{schemeId}/returns. Full current period-keyed map; often all-null for schemes with no schemereturns row. Scope: fund:read. Output: identical shape (pointToPoint/ranks/quartiles/sipReturns maps).
get_fund_sectors(scheme_id)Maps to GET /funds/{schemeId}/sectors. Scope: fund:read. Output: array of {sector, allocation}.
get_fund_holdings(scheme_id, limit="10")Maps to GET /funds/{schemeId}/holdings. limit is a positive integer or the string "all". Scope: fund:read. Output: {totalReturned, holdings: [...]}.
get_nav_history(scheme_id, from_date="", to_date="")Maps to GET /funds/{schemeId}/nav-history. Real daily NAV series from investwellhistory, not the live single-point value. Scope: fund:read. Output: array of {date, nav}.
Example:
get_nav_history(scheme_id=218540, from_date="2026-06-01", to_date="2026-07-08")
→ [ { "date": "2026-06-01", "nav": 85.597 }, ..., { "date": "2026-07-07", "nav": 90.915 } ]
get_returns_history(scheme_id, metric="1Year", from_date="", to_date="")Maps to GET /funds/{schemeId}/returns-history. metric is one of the point-to-point periods, or "all" for every tracked field. Scope: fund:read. Output: array of {date, value} (single metric) or full-row objects (metric="all").
get_factsheet_history(scheme_id, from_date="", to_date="")Maps to GET /funds/{schemeId}/factsheet-history. Fund manager / expense ratio / AUM / market-cap trend. Scope: fund:read. Output: array of factsheet snapshot objects.
get_sector_history(scheme_id, sector="", from_date="", to_date="")Maps to GET /funds/{schemeId}/sector-history. Omit sector for the full breakdown per snapshot date. Scope: fund:read. Output: array of {date, sector, allocation}.
get_holdings_history(scheme_id, isin, from_date="", to_date="")Maps to GET /funds/{schemeId}/holdings-history. isin is required. Scope: fund:read. Output: array of {date, isin, name, instrument, sector, allocation}.
from_date/to_date on every single-fund history tool above are optional YYYY-MM-DD strings; omit for the API's default 5-year lookback.
search_schemes(q="", category="", dir_plan="", active_only=False, sort_by="schemeName", sort_dir="asc", limit=50, offset=0)Maps to GET /schemes. Bulk: returns up to 200 schemes per call, each with AUM/expense ratio/returns/Sharpe/std-dev already populated and sortable server-side — the tool to reach for when ranking or filtering funds by a metric, instead of looping list_schemes + get_fund_details. Scope: schemes:read. Output: identical shape to GET /schemes above ({total, limit, offset, schemes: [...]}).
compare_funds(scheme_ids, metrics_all=False)Maps to GET /funds/compare. Bulk: 2-10 funds' overview/asset-allocation/risk/returns side by side in one call. scheme_ids is a list of integers (not a comma string — the tool signature takes a real list). IDs that don't exist come back in notFound. Scope: fund:read. Output: identical shape to GET /funds/compare above.
list_categories()Maps to GET /categories. No parameters. Scope: categories:read. Output: array of {category, schemeCount}.
get_category_stats(category)Maps to GET /categories/{category}/stats. category must match exactly (case-sensitive) — get it from list_categories or a search_schemes/list_schemes result, don't guess the spelling. Scope: categories:read. Output: identical shape to GET /categories/{category}/stats above ({category, schemeCount, metrics: {...}}).
get_nav_history_compare(scheme_ids, from_date="", to_date="")Maps to GET /funds/history-compare. Bulk: real daily NAV for 2-10 funds in one call, 90-day default lookback (shorter than the single-fund tool's 5-year default — see "Efficient tool usage" below for why). scheme_ids is a list of integers. Scope: fund:read. Output: identical shape to GET /funds/history-compare above ({funds: [{schemeId, navHistory}], notFound: [...]}).
This is the full content returned by get_analysis_guide(), reproduced here for reference.
Efficient tool usage — prefer the one bulk tool over N calls to a single-fund tool:
| Question shape | Use | Not |
|---|---|---|
| Compare 2-10 specific funds' current state | compare_funds |
N × get_fund_details |
| Rank/filter many funds by a metric | search_schemes |
list_schemes + get_fund_details per result |
| "How's this whole category doing" | get_category_stats |
fetching every fund in the category |
| NAV trend/curve for 2-10 funds | get_nav_history_compare |
N × get_nav_history |
| Just need to rank performance, not see the curve shape | compare_funds / search_schemes |
any raw NAV history call — far cheaper |
Also: re-use list_schemes/search_schemes results already fetched earlier in a conversation instead of re-searching identical criteria, and don't re-diff consecutive factsheet-history rows yourself — that series is already change-detected server-side (one row per real change).
Plan-type resolution:
dirPlan directly: "0" = Regular, "1" = Direct. 100% reliable.schemePlanName text, with dividendFrequency as a secondary check only.schemePlanName — "(G)" or a trailing "Growth" suffix means Growth option; "IDCW", "Dividend", or "(D)" anywhere means Dividend/IDCW option.dividendFrequency (non-null, e.g. "yearly") corroborates IDCW — but ~2% of confirmed IDCW plans still have it null (a real data gap, empirically confirmed), so treat it as a tiebreaker only, never the sole signal.search_schemes(q="<fund name>", dir_plan="0"), then pick the result whose schemePlanName has a (G)/Growth qualifier and no IDCW/Dividend marker.Derived metrics methodology (none of these are precomputed by the API — compute them client-side from raw data fetched via the tools above):
(nav_end / nav_start - 1) * 100. Fine for periods ≤ 1 year.(nav_end / nav_start) ** (1/years) - 1 instead of simple return, where years = days_between / 365.25 — simple return overstates multi-year performance.from_date, since get_nav_history_compare defaults to only 90 days.get_fund_risk_metrics/get_fund_details already return Investwell's own pre-computed standardDeviation/sharpeRatio/sortinoRatio — prefer those over recomputing (Investwell's risk-free-rate assumption for Sharpe/Sortino isn't exposed, so an independently recomputed figure wouldn't reconcile with it). Only compute your own for a custom window: daily returns from consecutive NAV points, stdev of that series, annualize by ×√252.max over all t of (1 - nav_t / max(nav_0..t)).get_category_stats for the category's average/median/min/max, then compare one fund's own risk-metrics/returns against those — don't fetch every peer fund individually.Decision guide:
| If the question is... | Use |
|---|---|
| Tell me about fund X | get_fund_details or get_fund_overview |
| Fund X's risk stats / current returns / sectors / holdings | get_fund_risk_metrics / get_fund_returns / get_fund_sectors / get_fund_holdings |
| How has fund X's NAV moved | get_nav_history |
| Compare 2-10 specific funds right now | compare_funds |
| Compare NAV trend/curves for 2-10 funds | get_nav_history_compare |
| Rank/filter many funds by a metric | search_schemes |
| How's this whole category doing / is X expensive for its category | get_category_stats |
| What categories exist | list_categories |
| How has fund X's return/rank/quartile trended | get_returns_history |
| How has fund X's manager/expense ratio/AUM changed | get_factsheet_history |
| How has fund X's sector mix drifted | get_sector_history |
| How has fund X's weight in one holding changed | get_holdings_history (isin required, get it from get_fund_holdings first) |
| Find the regular/direct or growth/IDCW variant of a fund | search_schemes with dir_plan, then apply plan-type resolution above |
Runs as its own ECS service (growthvine-mcp-service, cluster growthvine-prod) from a separate image/ECR repo (growthvine-mcp-server) than the Data API — deploying one does not redeploy the other. See infrastructure.md for the full task definition, secrets, and target group setup.