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}Bounded daily OHLCV history for one tradable symbol
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}Bounded 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 gateThe data_api app is fail-closed. An explicit Market API row grants or denies access; when no row exists, accounts already authorized for the legacy REST Data API by a current Data plan (or the legacy RGNCM exception) are admitted automatically. An unavailable permission backend returns 503 instead of a silent pass-through.
Indicator tiersIndicator access keeps the existing full, selected + base, and selected-only tiers on a per-indicator basis.
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 the result was cut by limit. When it is true, narrow the date range or drop limit for the complete series.
next_cursorPublished by /data/v1/recent only, because that is the one source that can continue: it returns an opaque cursor when another page exists and null on the last page. Every other endpoint omits the field, so a truncated history response can never be paged — narrow the range instead.
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.

Query budget

BoundContract
Row limitCannot exceed 5,000. Omitted, the list-shaped endpoints return 1,000 rows, while /prices, /indicators/…/series, and /pairs return the complete series. Continue /data/v1/recent with meta.next_cursor; for other truncated results, narrow the date range.
Date spanOnly /data/v1/custom-indicators/{url}/series rejects a range longer than 7,305 days (20 years + 5). Every other endpoint accepts any span, so bound the range yourself.
Cell budgetWhen a limit is supplied, limit × columns must stay within 100,000 response cells per request; a pair request counts every indicator it carries.
Response sizeSerialized payloads must stay within the 5,000,000 byte response budget on every response; oversized requests fail with 422.
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 numbers5,000 rows ≈ twenty years of daily data (one research window per call); 100,000 cells is that row cap across ~20 columns; the 5 MB budget leaves ~2× margin over the largest realistic payload (ten indicators × 5,000 rows ≈ 2.5 MB); 90 observations and 1–50 indicators are what the pre-generated recent store holds; 10 pair indicators is a column budget on top of the market's OHLCV.

Date and identifier formats

InputContract
start / endStrict YYYY-MM-DD for catalog, prices, indicators, pairs, snapshots, and custom series.
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 "you@example.com: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 "you@example.com: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 "you@example.com: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.

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 "you@example.com: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 "you@example.com: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 "you@example.com: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}

Bounded intraday records for one entitled identifier, ordered oldest to newest.

Request
curl -u "you@example.com: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 "you@example.com: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 "you@example.com: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. 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

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 endpointThe application-mounted MCP endpoint uses per-request Basic Auth or, when enabled, OIDC Bearer. Both reuse the same local account and app-gate checks, and 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.

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.

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

Cache catalog responses, and prefer a pair request, recent-bulk request, or snapshot over many repeated single-series calls. Pair and history series come back in full; when you do pass a limit, meta.truncated tells you a cap applied. Continue recent-bulk pages only with the returned opaque meta.next_cursor, and page other ranges by narrowing the date range.

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.