Market Data API v1

A normalized, permission-aware API for market prices, SentimenTrader indicators, paired series, snapshots, recent bulk values, intraday history, and account-owned custom indicators.

Base URL
st-tools.sentimentrader.com/data/v1
Protocol
HTTPS · GET only
Authentication
Basic · Cookie · optional OIDC Bearer
Format
JSON envelope
No legacy compatibility for REST. This contract does not expose former legacy route names, numeric-key maps, or double-encoded JSON. New integrations should use /data/v1 or the canonical Market Data MCP tools; see the REST Data API page for the legacy application it replaces.
Every endpoint below pairs a copyable request with its response payload; parameters and behavior notes sit beside the code, and the full envelope contract is documented in Response Contract.

Endpoint directory

GroupEndpointPurpose
Catalog/data/v1/catalogSearch the entitled indicator catalog
Prices/data/v1/prices/{symbol}Complete daily OHLCV history; optional tail limit
Indicators/data/v1/indicators/{indicator}/seriesOne indicator with its market's OHLCV
Pairs/data/v1/pairsAlign one symbol with up to 10 indicators
Snapshots/data/v1/snapshotsLatest or dated point-in-time values
Recent/data/v1/recentBulk values for all or selected indicators over the last N trading days
Intraday/data/v1/intraday/{indicator}Paginated intraday history for one identifier
Custom/data/v1/custom-indicatorsList custom indicators owned by the account
Custom/data/v1/custom-indicators/{indicator_url}/seriesRead one owned custom indicator series

Authentication and authorization

HTTP calls use Basic Auth, a valid session cookie, or an OIDC Bearer access token when OAuth is enabled. Access is layered: an authenticated local account, the data_api app gate (including legacy REST Data API compatibility), then per-resource entitlement or custom-indicator ownership.

LayerBehavior
CredentialsBasic and session credentials resolve a SentimenTrader account directly. An enabled OIDC Bearer token resolves its exact issuer/subject mapping to the same local account.
App gateDuring the compatibility rollout, legacy REST Data API eligibility is the base grant. Eligible accounts are admitted when no data_api row exists, while an explicit enabled=false row blacklists the app. For other accounts, even data_api.enabled=true cannot currently create Data API entitlement; that capability is reserved for a future independently priced offer. An unavailable permission backend returns 503 instead of a silent pass-through.
Indicator tiersIndicator access keeps the legacy full, selected + base, and selected-only tiers on a per-indicator basis. For an eligible legacy account, no dataapi_user_selections row retains the old full/VIP behavior.
Entitlement read failureWhen the entitlement query itself fails, the request fails closed with 502 upstream_error — a data-tier outage, deliberately distinct from the 503 authorization_backend_unavailable used for the app gate. The cause is logged server-side; the body stays generic.
Single-series requestsEndpoints that address one indicator reject an unentitled identifier with 403 before any data is queried.
Multi-result requestsCatalog and snapshot endpoints filter their output to entitled records; pair requests query only the entitled subset and report omitted identifiers in warnings.
Custom indicatorsOwnership is enforced against the authenticated account. The account identity is derived from authentication and cannot be supplied by callers.
TransportAlways use HTTPS. Basic credentials are encoded, not encrypted by the header itself.

Response contract

Every successful endpoint returns one JSON object with three parts: data rows, descriptive meta, and a warnings list. Metadata is part of the contract — report it rather than recomputing it client-side.

Envelope
{
  "data": [
    { "DT": "2026-09-10", "close": 55.2 }
  ],
  "meta": {
    "schema_version": "1.0",
    "source_per_series": { "optix_spx": "catalog-resolved MySQL table" },
    "fallback_used": false,
    "data_as_of": "2026-09-10",
    "retrieved_at": "2026-09-14T04:00:00Z",
    "timezone": "UTC",
    "calendar": "business-day",
    "frequency": "daily",
    "alignment": "none",
    "fill_policy": "none",
    "adjustment_policy": "not_applicable",
    "row_count": 1,
    "cell_count": 2,
    "truncated": false,
    "response_bytes": 412
  },
  "warnings": []
}
Meta fieldContract
schema_versionEnvelope contract version. v1 is "1.0".
source_per_seriesPer-series provenance, for example stocks.eod_stocks for prices or catalog-resolved MySQL table for indicators.
fallback_usedTrue when the price leg fell back to the close-only index table.
data_as_ofNewest observation timestamp present in data.
retrieved_atUTC timestamp of the server-side retrieval.
frequency / calendar / alignment / fill_policy / adjustment_policyHow the rows were produced: source-native or requested frequency, trading calendar, join/resample alignment, fill policy, and price adjustment policy.
row_count / cell_count / response_bytesSerialized size accounting for the payload.
truncatedWhether another page or an entitlement window contains additional rows. Follow next_cursor whenever it is present; price, indicator, and pair history omit it because omitting limit returns their complete series.
next_cursorPublished by catalog, snapshots, recent bulk, intraday, custom-indicator catalog, and custom-indicator series. It is opaque: return it unchanged as cursor to continue, and stop when it is null. It is signed and bound to the query and caller; snapshot continuation uses the stable indicator identifier so ordinary value refreshes do not strand a page.
truncated_to_trial_windowPresent on series endpoints when a trial account received only the most recent 90 observations.
warningsHuman-readable notices: omitted unentitled indicators, indicators with no data in range, and trial-window notices.

Pagination walkthrough

Collection endpoints — catalog, snapshots, recent bulk, intraday, and both custom-indicator endpoints — page with the same three-step exchange: request a page, pass meta.next_cursor back unchanged as cursor, and stop when it is null. Keep the filter parameters (query, group, kind, market_symbol) identical across the pages of one enumeration; limit may differ per page. Price, indicator, and pair history do not use cursors: omit limit for the complete series, or pass it to receive the most recent N rows with meta.truncated set.

Worked example: enumerate every entitled catalog entryStep by step
Step 1 — first page (no cursor)
curl -u "[email protected]:your-password" \
  "https://st-tools.sentimentrader.com/data/v1/catalog?limit=2"
Response 200 · meta excerpt
{
  "data": [ /* 2 catalog rows */ ],
  "meta": {
    "count": 31156,      // total entries matched
    "offset": 0,
    "truncated": true,
    "next_cursor": "eyJ2IjoxLCJzY29wZ…In0.x7Q"  // opaque, signed; display shortened
  }
}
Step 2 — next page (cursor from step 1, verbatim)
curl -u "[email protected]:your-password" \
  "https://st-tools.sentimentrader.com/data/v1/catalog?limit=2&cursor=eyJ2IjoxLCJzY29wZ…In0.x7Q"
Response 200 · meta excerpt
{
  "data": [ /* rows 3–4 */ ],
  "meta": {
    "count": 31156,
    "offset": 2,
    "truncated": true,
    "next_cursor": "eyJ2IjoxLCJzY29wZ…In0.k9W"  // a different value every page
  }
}
Last page — the stop condition
{
  "data": [ /* final rows */ ],
  "meta": {
    "count": 31156,
    "offset": 31154,
    "truncated": false,
    "next_cursor": null  // stop — no more pages
  }
}
The whole loop
import requests

s = requests.Session()
s.auth = ("[email protected]", "your-password")
params = {"limit": 50000}                 # no filters — the whole catalog in one page
records = []

while True:
    page = s.get(
        "https://st-tools.sentimentrader.com/data/v1/catalog",
        params=params,
    ).json()
    records.extend(page["data"])
    cursor = page["meta"].get("next_cursor")
    if not cursor:
        break
    params["cursor"] = cursor             # hand it back verbatim

print(len(records))                       # 31156 — every entitled entry, one call

A rejected continuation returns 422 with {"detail": "cursor is invalid; pass back the next_cursor value unmodified."} — restart the enumeration from the first page. Catalog and custom-indicator cursors freeze their result snapshot, so a page whose underlying data changed between requests is rejected rather than silently shifted; snapshot, recent-bulk, and intraday cursors bind only the query and the account, so ordinary value refreshes never invalidate a page.

Query budget

BoundContract
Row limitThe catalog accepts 1–50,000 rows per page (default 1,000) — large enough for the whole production catalog in one call. Snapshots, recent bulk, intraday, and custom endpoints use 1–5,000 rows per page. Neither is an overall result cap: every collection returns next_cursor when more rows exist. Price, indicator, and pair history endpoints have no fixed row maximum: omit limit for the complete series, or supply any positive tail length subject to the response budgets below.
Date spanNo endpoint imposes an arbitrary date-span cap. start and end are optional filters, not prerequisites for retrieving all pages. Use a narrower range only when it answers the question more efficiently.
Cell budgetAn explicit history limit must keep limit × columns within 100,000 response cells; a pair request counts every indicator it carries. Paginated endpoints retain their 5,000-row page size.
Response sizeSerialized payloads must stay within the 12,000,000 byte response budget on every response; oversized requests fail with 422. Responses under /data/v1 are transparently gzip-compressed in transport (Content-Encoding: gzip) when the client sends Accept-Encoding: gzip — standard HTTP clients decompress automatically, and meta.response_bytes still reports the uncompressed serialized size.
Pair and snapshotPairs accept 1–10 indicators; /data/v1/snapshots accepts up to 50. Recent bulk takes trading_days 1–90 and 1–50 indicators.
Trial accountsTrial accounts receive the most recent 90 observations only, with a matching warning.
Why these numbersThe catalog's 50,000-row page and the 12 MB budget let a whole catalog (~31k rows, ≈7.6 MB serialized, ≈0.5 MB gzipped) travel in one call; 5,000 remains the page size for value-heavy collections. The 100,000-cell budget protects wide history responses; 90 recent observations reflects the Redis store's retention; 10 pair indicators limits joined columns on top of the market's OHLCV.

Date and identifier formats

InputContract
start / endStrict YYYY-MM-DD for prices, indicators, pairs, and custom series. Catalog has no date filter; snapshots use a single date.
start / end (intraday)ISO-8601 datetimes. Timezone awareness must be consistent: both aware or both naive.
IdentifiersTrimmed, printable, length-capped (64 characters for price symbols, 255 for indicators, 2048 for custom indicator URLs). Discover canonical identifiers from the catalog first; do not manufacture them from display names.

Indicator catalog

Discover indicator identifiers before requesting history or pair data. Catalog rows are metadata; indicator (identical to url, kept for legacy callers) is the identifier used by every other endpoint.

GET/data/v1/catalog

Searches the indicator catalog, filtered to the caller's entitlement tier before pagination.

Request
curl -u "[email protected]:your-password" \
  "https://st-tools.sentimentrader.com/data/v1/catalog?query=optix&limit=5"
Response 200 · application/json
{
  "data": [
    {
      "chart_name": "SPY Optix",
      "indicator": "etf_spy",
      "url": "etf_spy",
      "market_symbol": "SPX",
      "type": "etf",
      "sub_type": "optix",
      "table_name": "OPTIX",
      "updated": "2026-09-10"
    }
  ],
  "meta": {
    "schema_version": "1.0",
    "query": "optix",
    "count": 1,
    "entitlement_tier": "full",
    "source_per_series": { "catalog": "users.url_map" },
    "truncated": false
    // + standard envelope fields: row_count, cell_count,
    // retrieved_at, next_cursor, response_bytes
  },
  "warnings": []
}
Response fields & metadataReference
chart_namestringindicatorstringurlstringmarket_symbolstringtypestringsub_typestringtable_namestringupdateddate string

Envelope type Envelope<CatalogRecord[]>. Meta carries query, count (entries matched), catalog_size (entries searchable by this account), entitlement_tier, and suggestions (populated only when the query misses or matches an identifier alone).

Price history

Complete OHLCV history for one tradable symbol, daily by default. The whole available range is returned unless you pass limit, which returns the most recent N rows and reports meta.truncated.

GET/data/v1/prices/{symbol}

OHLCV rows ordered oldest to newest, resampled to the requested frequency; index symbols are served close-only from the index table.

Request
curl -u "[email protected]:your-password" \
  "https://st-tools.sentimentrader.com/data/v1/prices/SPY?start=2026-08-01&end=2026-09-10&limit=5"
Response 200 · application/json
{
  "data": [
    {
      "DT": "2026-09-10",
      "Open": 5450.12,
      "High": 5471.31,
      "Low": 5444.20,
      "Close": 5468.55,
      "Volume": 4213300000
    }
  ],
  "meta": {
    "schema_version": "1.0",
    "symbol": "SPY",
    "source_per_series": { "SPY": "stocks.eod_stocks" },
    "fallback_used": false,
    "frequency": "daily",
    "calendar": "business-day",
    "adjustment_policy": "raw_close_excluding_split_rows",
    "truncated": false
    // + standard envelope fields
  },
  "warnings": []
}
Response fields & metadataReference
DTdate stringOpennumberHighnumberLownumberClosenumberVolumenumber

Envelope type Envelope<PriceRecord[]>. Meta reports symbol, fallback_used, frequency: daily, calendar: business-day, and the explicit adjustment policy raw_close_excluding_split_rows.

Indicator series

One entitled SentimenTrader indicator over its full available range, returned with the OHLCV of the market the indicator belongs to. The indicator is forward-filled onto the daily grid and carried as {indicator}_close (and {indicator}_sentiment when requested), the same naming pair requests use. Pass limit for the most recent N rows.

GET/data/v1/indicators/{indicator}/series

Observations ordered oldest to newest, with optional sentiment readings.

Request
curl -u "[email protected]:your-password" \
  "https://st-tools.sentimentrader.com/data/v1/indicators/etf_spy/series?include_sentiment=true&limit=5"
Response 200 · application/json
{
  "data": [
    { "DT": "2026-09-10", "close": 55.2, "sentiment": 64 }
  ],
  "meta": {
    "schema_version": "1.0",
    "indicator": "etf_spy",
    "market_symbol": "SPY",
    "source_per_series": { "SPY": "stocks.eod_stocks", "etf_spy": "catalog-resolved MySQL table" },
    "frequency": "daily",
    "truncated": false
    // + standard envelope fields
  },
  "warnings": []
}
Response fields & metadataReference
DTdate stringclosenumbersentimentnumber · optional

Envelope type Envelope<IndicatorRecord[]>. Trial accounts receive the most recent 90 observations, reported via truncated_to_trial_window. When the catalog has one, meta.description carries the indicator's plain-text explanation, with paragraphs separated by a blank line.

Paired series

Align one tradable symbol with up to 10 indicators for comparison, charting, or downstream analysis. Prices are aggregated with OHLCV semantics; each indicator becomes a {indicator}_close column. indicators is optional: omit it to receive the market's own OHLCV series. The full series is returned — pass limit only when you want the most recent N rows, and note that meta.truncated then reports true.

GET/data/v1/pairs

Price series joined with each indicator on a shared grid, then resampled to the requested frequency.

Request
curl -u "[email protected]:your-password" \
  "https://st-tools.sentimentrader.com/data/v1/pairs?symbol=SPX&indicators=etf_spy&frequency=daily&limit=5"
Response 200 · application/json
{
  "data": [
    {
      "DT": "2026-09-11",
      "Close": 5468.55,
      "Volume": 4213300000,
      "etf_spy_close": 55.2
    }
  ],
  "meta": {
    "schema_version": "1.0",
    "symbol": "SPX",
    "indicators": ["etf_spy"],
    "source_per_series": {
      "SPX": "stocks.INDEXES_ST",
      "etf_spy": "catalog-resolved MySQL table"
    },
    "frequency": "daily",
    "alignment": "outer_join_then_resample",
    "fill_policy": "forward_fill_only",
    "truncated": false
    // + standard envelope fields
  },
  "warnings": []
}
Response fields & metadataReference
DTdate stringOpen/High/Low/Close/Volumeprice columns{indicator}_closeone per indicatormeta.indicator_market_symbolsindicator → market symbol

Envelope type Envelope<PairRecord[]>. Resample rules: business days (daily), Friday weeks (weekly), business month ends (monthly), business quarter ends (quarterly).

Snapshots

Latest or dated point-in-time values. Prefer a snapshot over fetching history and taking the last row. Multi-record results are filtered to the caller's entitled indicators.

GET/data/v1/snapshots

One row per indicator — latest stored values, or the point-in-time values for one calendar date.

Request
# Latest values (omit date), or a specific date:
curl -u "[email protected]:your-password" \
  "https://st-tools.sentimentrader.com/data/v1/snapshots?indicators=etf_spy&date=2026-09-10"
Response 200 · application/json
{
  "data": [
    {
      "name": "SPY Optix",
      "indicator": "etf_spy",
      "last_close": 55.2,
      "update_date": "2026-09-11",
      "sentiment_value": 42,
      "market_symbol": "SPX"
    }
  ],
  "meta": {
    "schema_version": "1.0",
    "date": null,
    "count": 1,
    "entitlement_filtered": true,
    "source_per_series": { "snapshot": "users.url_map" },
    "truncated": false
    // + standard envelope fields
  },
  "warnings": []
}
Response fields & metadataReference
namestringindicatorstringlast_closenumberupdate_datedate stringsentiment_valuenumbermarket_symbolstringlast_change_typedated snapshots

Envelope type Envelope<SnapshotRecord[]>. Meta reports date, count, and entitlement_filtered.

Recent bulk data

Tall-row bulk reads over the SPX trading calendar: every observation of every entitled indicator for the last N trading days. Use this instead of looping per-indicator history calls.

GET/data/v1/recent

One row per indicator observation (DT, indicator, close, sentiment) inside the trading-day window.

Request
# Last 5 trading days for all entitled indicators:
curl -u "[email protected]:your-password" \
  "https://st-tools.sentimentrader.com/data/v1/recent?trading_days=5&limit=1000"
Response 200 · application/json
{
  "data": [
    {
      "DT": "2026-09-11",
      "indicator": "etf_spy",
      "close": 55.2,
      "sentiment": 42
    }
  ],
  "meta": {
    "schema_version": "1.0",
    "trading_days": 5,
    "resolved_as_of_date": "2026-09-11",
    "indicator_count": 312,
    "offset": 0,
    "truncated": false,
    "next_cursor": null
    // + standard envelope fields
  },
  "warnings": []
}
Response fields & metadataReference
DTdate stringindicatorstringclosenumbersentimentnumber

Envelope type Envelope<RecentReading[]>. Meta reports trading_days, resolved_as_of_date, indicator_count, offset, and next_cursor.

Intraday history

A separate, higher-volume capability. Use it only when intraday resolution is material; daily price or indicator history answers most questions with less data.

GET/data/v1/intraday/{indicator}

Paginated intraday records for one entitled identifier; each page is ordered oldest to newest and continuation walks backward through history.

Request
curl -u "[email protected]:your-password" \
  "https://st-tools.sentimentrader.com/data/v1/intraday/SPX?start=2026-09-11T09:30:00&end=2026-09-11T16:00:00&limit=13"
Response 200 · application/json
{
  "data": [
    {
      "DT": "2026-09-11T10:00:00",
      "close": 5468.55,
      "market_symbol": "SPX"
    }
  ],
  "meta": {
    "schema_version": "1.0",
    "symbol": "SPX",
    "source_per_series": { "SPX": "stocks.intraday" },
    "frequency": "intraday",
    "truncated": false
    // + standard envelope fields
  },
  "warnings": []
}
Response fields & metadataReference
DTISO datetime stringclosenumbermarket_symbolstring

Envelope type Envelope<IntradayRecord[]>. Provenance is stocks.intraday.

Custom indicators

Indicators created in the SentimenTrader web application, scoped to the authenticated account. Discover them with the list endpoint before requesting data; ownership is enforced by the server.

GET/data/v1/custom-indicators

Lists custom indicators owned by the authenticated account, ordered newest first.

Request
curl -u "[email protected]:your-password" \
  "https://st-tools.sentimentrader.com/data/v1/custom-indicators"
Response 200 · application/json
{
  "data": [
    {
      "name": "XLE Crude ROC1 Spread",
      "url": "sy9192-sy9218-1roc_1roc_1-0-0-sh0_c|sh0_c",
      "timeframe": "daily",
      "calculate": "spread",
      "create_date": "2026-07-14T12:12:19Z"
    }
  ],
  "meta": {
    "schema_version": "1.0",
    "source_per_series": { "custom_catalog": "custom-indicator-service" },
    "count": 1,
    "truncated": false
    // + standard envelope fields
  },
  "warnings": []
}
Response fields & metadataReference
namestringurlstringtimeframestringcalculatestringcreate_dateISO datetime string

Envelope type Envelope<CustomIndicator[]>. Records may also include calculation settings such as indicator_1_url, indicator_2_url, upper_extreme_value, lower_extreme_value, ma, and benchmark.

GET/data/v1/custom-indicators/{indicator_url}/series

Calculates or retrieves the series for one custom indicator owned by the authenticated account.

Request
curl -u "[email protected]:your-password" \
  "https://st-tools.sentimentrader.com/data/v1/custom-indicators/sy9192-sy9218-1roc_1roc_1-0-0-sh0_c|sh0_c/series?limit=5"
Response 200 · application/json
{
  "data": [
    {
      "DT": "2026-09-10",
      "close": -2.6607,
      "symbol": "XLE Crude ROC1 Spread"
    }
  ],
  "meta": {
    "schema_version": "1.0",
    "url": "sy9192-sy9218-1roc_1roc_1-0-0-sh0_c|sh0_c",
    "is_sample": false,
    "source_per_series": { "...": "custom-indicator-service" },
    "truncated": false
    // + standard envelope fields
  },
  "warnings": []
}
Response fields & metadataReference
DTdate stringclosenumbersymbolstring · indicator name

Envelope type Envelope<CustomIndicatorReading[]>. Trial accounts receive the most recent 90 observations only.

MCP tools

The mounted MCP endpoint exposes intent-oriented workflows over the same application service. MCP and Tool API share one tool_api master permission that can be explicitly granted or revoked. While TOOL_API_SUBSCRIPTION_ENFORCE=false, all legacy Data API subscribers—including Light Data and RGNCM—retain their old MCP access. Once Recurly synchronization and production plan codes are confirmed, enabling the switch makes Light Data and RGNCM REST-Data-API-only unless explicitly granted. Market Data tools additionally enforce the existing Data API app gate and indicator entitlements. Its seven canonical tools aggregate related reads where useful, preserve the normalized envelope, and attribute every call to the authenticated user.

Canonical toolShared capabilityPurpose
search_market_data/data/v1/catalogIndicator discovery and entitlement visibility
get_market_historyprice / indicator / pairIntent-based facade over the three history endpoints
get_market_snapshot/data/v1/snapshotsCurrent/latest values or a specific date
get_recent_market_data/data/v1/recentBulk recent values with cursor pagination
get_intraday_history/data/v1/intraday/{indicator}Intraday history for one identifier
list_custom_indicators/data/v1/custom-indicatorsDiscover account-owned custom indicators
get_custom_indicator_data/data/v1/custom-indicators/{indicator_url}/seriesRead one owned custom indicator series
get_indicator_workbenchData API dashboardOfficial dashboard link and launch steps

Deprecated aliases

Three aliases remain registered for one compatibility cycle. New clients should use get_market_history, which takes mode plus that mode's parameters directly.

Deprecated aliasCanonical replacement
get_price_historyget_market_history with mode="price"
get_indicator_historyget_market_history with mode="indicator"
get_pair_dataget_market_history with mode="pair"
TransportBehavior
Mounted endpointUse OAuth/OIDC browser sign-in first when the client can discover it; Basic Auth is the fallback when OAuth is unavailable or unsupported. Both reuse the same local account, shared tool_api master gate, and fail-closed secondary app checks; every tool call is attributed to that user.
Standalone SSEStandalone SSE omits these tools because it does not have a per-request identity to authorize against.
Error shapeDomain failures surface as {"error": {"code", "status", "message"}} with the same codes as the HTTP status table below.

Optional Data API Dashboard

Open it in one line: tell the connected agent Open my SentimenTrader Data API Dashboard. MCP Apps hosts render the read-only dashboard inline and need nothing installed. The former phrase Open the SentimenTrader Indicator Workbench remains supported for compatibility. Open the Data API Dashboard guide for the full description and the optional standalone build.

Subscribers can browse their permitted indicators in the optional, read-only My Data API Dashboard. The in-chat view reuses the mounted MCP connection; the standalone build runs only on 127.0.0.1. Either way the catalog and readings use the same signed-in account, tool permission, and indicator entitlements as search_market_data. It is not a website-favorites list and does not show account-owned custom indicators.

Standalone local build: if it is already running on its default port and http://127.0.0.1:4174/_health identifies it, open the local Data API Dashboard directly. Otherwise ask the connected agent to Open my SentimenTrader Data API Dashboard; it calls get_indicator_workbench and follows the verified setup guide. Opening it is optional.

In the standalone build, when no installed dashboard is available, that explicit request automatically runs the fixed official bootstrap launcher. It automatically downloads the runtime bundle and its checksum, verifies the checksum before extraction, rejects unsafe archive entries, installs it into a dedicated private location, starts it on 127.0.0.1:4174 (another free loopback port when 4174 is busy), and returns its actual dashboard link—without another confirmation. The page asks you to sign in on the first visit. Automatic startup always uses browser OAuth + PKCE and remembers only its rotating OAuth refresh session in the operating-system credential store; a manually started bridge can use a readable Basic MCP configuration. Do not put credentials in a browser, command, project file, or chat.

Errors, access, and limits

Domain errors carry a stable status code and error code. Permission denials and backend failures are distinct: a user without entitlement sees 403, one whose entitlement cannot be read sees 502, and an unavailable app-access policy backend fails closed with 503.

StatusCodeMeaning
401authentication_requiredAuthentication is missing or invalid.
403access_deniedThe app, indicator, or custom resource is not allowed for this account.
404not_foundAn allowed resource does not exist in the requested range.
422invalid_parametersA parameter or the query budget is invalid.
429—The configured app quota is exceeded.
502upstream_errorAn upstream service failed, including a data-tier read while loading entitlements.
503authorization_backend_unavailableThe app-access policy backend is unavailable; the request fails closed.
504upstream_timeoutAn upstream service timed out.

MCP tools return every failure as a result with isError: true and the body {"error": {"code", "status", "message", "details"}}, so a client can branch on the flag and on the code. Rejected arguments report invalid_parameters with error.details.fields naming the offending field, its constraint, and the value received; an unsupported parameter name is reported rather than ignored. Pydantic-internal text (generated model names, error-type tags, documentation links) is never returned.

REST endpoints render the same failures as a plain {"detail": "<message>"} body with the status from the table above; the structured error envelope is the MCP form. The pagination walkthrough shows the rejected-cursor case and its recovery.

Trial access

Trial accounts receive the most recent 90 observations only, on every series endpoint. The response reports truncated_to_trial_window in meta and adds a warning so callers can state the limitation instead of treating the tail as full history.

Request volume

Reuse catalog pages within one task, and prefer a pair request, recent-bulk request, or snapshot over many repeated single-series calls. Pair and ordinary history series come back in full when limit is omitted; an explicit tail length is checked against the response budgets. Continue catalog, snapshot, recent-bulk, intraday, and custom-data pages only with the returned opaque meta.next_cursor; a date range is optional rather than a pagination mechanism.

Budgets

The query budget table above lists every request-level bound, and the failure semantics are the same for all of them: a violated bound fails the call with invalid_parameters rather than returning a partial series, while an applied default page sets meta.truncated instead of cutting the data off silently.