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.
i
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
Group
Endpoint
Purpose
Catalog
/data/v1/catalog
Search the entitled indicator catalog
Prices
/data/v1/prices/{symbol}
Complete daily OHLCV history; optional tail limit
Indicators
/data/v1/indicators/{indicator}/series
One indicator with its market's OHLCV
Pairs
/data/v1/pairs
Align one symbol with up to 10 indicators
Snapshots
/data/v1/snapshots
Latest or dated point-in-time values
Recent
/data/v1/recent
Bulk 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-indicators
List custom indicators owned by the account
Custom
/data/v1/custom-indicators/{indicator_url}/series
Read 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.
Layer
Behavior
Credentials
Basic 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 gate
During 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 tiers
Indicator 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 failure
When 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 requests
Endpoints that address one indicator reject an unentitled identifier with 403 before any data is queried.
Multi-result requests
Catalog and snapshot endpoints filter their output to entitled records; pair requests query only the entitled subset and report omitted identifiers in warnings.
Custom indicators
Ownership is enforced against the authenticated account. The account identity is derived from authentication and cannot be supplied by callers.
Transport
Always 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.
Per-series provenance, for example stocks.eod_stocks for prices or catalog-resolved MySQL table for indicators.
fallback_used
True when the price leg fell back to the close-only index table.
data_as_of
Newest observation timestamp present in data.
retrieved_at
UTC timestamp of the server-side retrieval.
frequency / calendar / alignment / fill_policy / adjustment_policy
How 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_bytes
Serialized size accounting for the payload.
truncated
Whether 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_cursor
Published 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_window
Present on series endpoints when a trial account received only the most recent 90 observations.
warnings
Human-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
{
"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
Bound
Contract
Row limit
The 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 span
No 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 budget
An 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 size
Serialized 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 snapshot
Pairs accept 1–10 indicators; /data/v1/snapshots accepts up to 50. Recent bulk takes trading_days 1–90 and 1–50 indicators.
Trial accounts
Trial accounts receive the most recent 90 observations only, with a matching warning.
Why these numbers
The 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
Input
Contract
start / end
Strict 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.
Identifiers
Trimmed, 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.
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.
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.
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.
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"
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"
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.
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.
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.
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 tool
Shared capability
Purpose
search_market_data
/data/v1/catalog
Indicator discovery and entitlement visibility
get_market_history
price / indicator / pair
Intent-based facade over the three history endpoints
get_market_snapshot
/data/v1/snapshots
Current/latest values or a specific date
get_recent_market_data
/data/v1/recent
Bulk recent values with cursor pagination
get_intraday_history
/data/v1/intraday/{indicator}
Intraday history for one identifier
list_custom_indicators
/data/v1/custom-indicators
Discover account-owned custom indicators
get_custom_indicator_data
/data/v1/custom-indicators/{indicator_url}/series
Read one owned custom indicator series
get_indicator_workbench
Data API dashboard
Official 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 alias
Canonical replacement
get_price_history
get_market_history with mode="price"
get_indicator_history
get_market_history with mode="indicator"
get_pair_data
get_market_history with mode="pair"
Transport
Behavior
Mounted endpoint
Use 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 SSE
Standalone SSE omits these tools because it does not have a per-request identity to authorize against.
Error shape
Domain 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.
Status
Code
Meaning
401
authentication_required
Authentication is missing or invalid.
403
access_denied
The app, indicator, or custom resource is not allowed for this account.
404
not_found
An allowed resource does not exist in the requested range.
422
invalid_parameters
A parameter or the query budget is invalid.
429
—
The configured app quota is exceeded.
502
upstream_error
An upstream service failed, including a data-tier read while loading entitlements.
503
authorization_backend_unavailable
The app-access policy backend is unavailable; the request fails closed.
504
upstream_timeout
An 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.