API Guide
Reference for the StocksFast HTTP API. Every route is documented under Routes with its permission, parameters, response and errors.
Base URL and request headers
https://c-staging.stocksfast.io/api/v1
These are the headers you send. For a header that came back on a response, see Response headers.
| Header | When | Value |
|---|---|---|
Authorization |
always | Bearer sfu_.... Any other scheme answers 401. |
Content-Type |
any route taking a body | application/json; a body sent without it answers 400 invalid_body. The TradingView import takes multipart/form-data instead. |
Idempotency-Key |
optional, the two execute routes | Your own unique string, 1-255 chars of [A-Za-z0-9._-], to make a timed-out retry safe. See Idempotency. |
If-None-Match |
optional, any route answering an ETag |
A validator, a comma-separated list, or *. A match answers 304. |
X-Request-Id |
optional, always | Your own correlation id, 1-128 chars of [A-Za-z0-9._-], echoed back unchanged. Anything else is replaced by a fresh one. |
Keys
- Minted at Account › API keys; the plaintext is shown once, only its hash is stored.
- Minting needs Premium, an active trial, or scan credits; listing and revoking need none of those.
- A key is personal to your account; team or organizational use needs a separate license from support@stocksfast.io.
Rotation:
- Mint the replacement key with the permissions your client needs.
- Move the client over to the new secret.
- Revoke the old key.
Quick start
1. Mint a key with read_market and execute_scan at Account › API keys, then export it:
export SF_KEY=sfu_9f2c1a04a7d34e1c8b5f...
2. Read the vocabulary — static reference data, fetch once and cache it:
curl -H "Authorization: Bearer $SF_KEY" \
https://c-staging.stocksfast.io/api/v1/catalog
3. Check an expression — costs no credits, reads the same body fields execute does:
curl -H "Authorization: Bearer $SF_KEY" \
-H "Content-Type: application/json" \
-d '{"expression": "close > sma(close, 50)", "timeframe": "1D"}' \
https://c-staging.stocksfast.io/api/v1/scans/validate
4. Run it:
curl -H "Authorization: Bearer $SF_KEY" \
-H "Content-Type: application/json" \
-d '{"expression": "close > sma(close, 50) and volume > 1000000",
"timeframe": "1D", "limit": 2}' \
https://c-staging.stocksfast.io/api/v1/scans/execute
Conventions
Envelope
{"data": ...}
{"data": [ ... ], "page": {"limit": 50, "offset": 0, "total": 812}}
{"error": {"code": "compile_error", "message": "unknown function 'smaa'"}}
One route answers outside the envelope: GET /watchlists/<id>/export-tradingview.
Paging
limit is how many results come back in one response. offset is how many to skip before the first one, so the second page of 50 is limit=50, offset=50.
| Field | Type | Default | Bounds |
|---|---|---|---|
limit | int | 50 | clamped to [1, 200] |
offset | int | 0 | floored at 0, no upper bound; loop while offset < total |
Two routes take limit/offset in the request body, with a higher ceiling:
| Route | Max limit |
|---|---|
/scans/execute | 2000 |
/saved-scans/<id>/execute | 2000 |
Timestamps
One spelling across this surface: Unix epoch seconds (UTC, no fractional part), as a JSON int or null. "created_at": 1741944413 is Mar 14, 2025 09:26:53 UTC.
Execution model
Scans run synchronously: the response carries the results. Size your client-side timeout to the expression you send.
Permissions
| Permission | Carries |
|---|---|
stocksfast.read_market | The function catalog, the universe, symbol search and detail, and expression checks. |
stocksfast.execute_scan | Running a scan, spending your scan credits. |
stocksfast.read_scans | Listing and reading your saved scans. |
stocksfast.write_scans | Creating, editing and deleting saved scans. |
stocksfast.read_tags | Listing and reading your tags. |
stocksfast.write_tags | Creating, editing and deleting tags. |
stocksfast.read_watchlists | Listing and reading your watchlists, and the TradingView export. |
stocksfast.write_watchlists | Creating, editing and deleting watchlists, adding and removing symbols, and the TradingView import. |
Rate limits
| Bucket | Routes | Per minute |
|---|---|---|
| execute | /scans/execute, /saved-scans/<id>/execute | 20 |
| validate | /scans/validate | 30 |
| read | /catalog, /universe, /symbols, /symbols/<symbol> | 120 |
| CRUD read | list and retrieve across saved-scans, tags and watchlists, plus export-tradingview | 90 |
| CRUD write | create, update and delete across saved-scans, tags and watchlists, plus add-symbols, remove-symbols and import-tradingview | 40 |
Every response that spends a bucket carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (see Response headers), served and refused alike, describing the bucket that request actually spent; absence means no budget was spent, not that it is exhausted.
Scan credits
A scan run debits one credit, once its results are built. Premium accounts are not metered, and a replayed response (see Idempotency below) debits nothing. Nothing else on this surface costs credits.
Idempotency
A scan that settles is billed whether or not you are still connected to read it. Send an Idempotency-Key header on either execute route and the retry is safe.
- Pick your own key, 1-255 chars of
[A-Za-z0-9._-]. It is scoped to your credential. - A repeat with the same key inside 1 hour replays the stored response, marked
Idempotency-Replayed: true, and debits nothing. - Under load a key can be dropped before that 1 hour is up.
- It is a retry safety net, not a result cache. Do not use it to re-fetch results.
| Situation | Answer |
|---|---|
| No header | Nothing is stored. |
| First use of a key | Runs, debits, and stores the response for the retention window. |
| Same key, same request, inside the window | The stored response, Idempotency-Replayed: true, no debit. |
| Same key, a different request | 409 idempotency_key_reused — a client bug, not a replay. |
| Same key while the first request is still running | 409 idempotency_in_flight with Retry-After. |
| Malformed or oversized key | 400 invalid_value, before anything runs or debits. |
| A request that debited nothing (any refusal) | The key is not stored; a retry with the same key runs as a first use. |
| Key no longer held, by age or under load | Treated as a first use, the same as a key never sent before. |
Stability
This surface is Beta. The marker is the X-API-Stability: beta response header, on every response, success and error alike. It drops out at 1.0, with no path change.
| Commits to | Withholds |
|---|---|
| The route set and its paths inside the prefix | New codes and fields, which ship unannounced |
| Response body shape and timestamp spellings | The rate-limit numbers |
The sfu_ Bearer credential and the eight permission codes | |
The status for each condition this page names, and the code token for each documented error | |
| Owner scoping: another account's row is absent, never forbidden |
No deprecation window, sunset header, changelog feed or migration guarantee ships with the X-API-Stability header. A client that pins a withheld shape today carries that risk until the surface graduates.
Read the RateLimit-* headers (see Rate limits) on each response rather than caching the numbers from this page: they describe the request in front of you, and the published numbers can move.
Routes
Each block carries its permission, parameters, response and reachable errors, split Request against Response. Paths are relative to the base URL at the top of this page.
Account
GET /me
GET /meGET https://c-staging.stocksfast.io/api/v1/me
Excluded from the conditional-GET scheme carried by catalog, universe, symbols and symbols/<symbol>: no ETag, no If-None-Match handling.
200 — Cache-Control: no-store
{
"data": {
"user_id": 7,
"is_premium": false,
"credit_balance": 42,
"permissions": ["stocksfast.read_market", "stocksfast.execute_scan"],
"key": {"id": 41, "label": "mcp-laptop", "prefix": "9f2c1a04",
"created_at": 1755683642, "expires_at": 1763453642,
"last_used_at": 1755690118},
"limits": {
"execute": {"limit": 20, "window_seconds": 60},
"validate": {"limit": 30, "window_seconds": 60},
"read": {"limit": 120, "window_seconds": 60},
"crud_read": {"limit": 90, "window_seconds": 60},
"crud_write": {"limit": 40, "window_seconds": 60}
}
}
}
| Field | Type | Note |
|---|---|---|
user_id | int | — |
is_premium | bool | — |
credit_balance | int | reads the ledger; spends nothing |
permissions[] | array | the effective set (frozen INTERSECT the owner's live grants), not the frozen grant the key was minted with |
key | object|null | id, label, prefix, created_at, expires_at, last_used_at; null for a non-key principal |
key.expires_at, key.last_used_at | int|null | null on no TTL / no prior use |
limits | object | every rate-limit bucket's configured ceiling, keyed by name; {} for a non-key principal, since credential_id 0 would name a bucket shared by every credential-less caller, not this one |
| Status | Codes |
|---|---|
401 | unauthorized |
429 | rate_limited |
500 | internal |
curl -H "Authorization: Bearer $SF_KEY" \
https://c-staging.stocksfast.io/api/v1/me
→ 200
{"data": {"user_id": 7, "is_premium": false, "credit_balance": 42, "permissions": [...], "key": {...}, "limits": {...}}}
Schema
GET /openapi.json
GET /openapi.jsonThe machine-readable description of this whole prefix: an OpenAPI 3.0 document generated from the same route-registration metadata every other block on this page is checked against, so a route added, removed or renamed here without its metadata following fails a test rather than shipping a document that lies.
GET https://c-staging.stocksfast.io/api/v1/openapi.json
200
{
"openapi": "3.0.3",
"info": { … },
"paths": { … },
"components": { … }
}
The body is the document itself — openapi/info/paths/components at the top level, not the {"data": …} envelope every other route on this page answers with. Compiled-in reference data, fixed for the life of the server process. Every 200 carries ETag and Cache-Control: public, max-age=3600; send the ETag back as If-None-Match and a match answers 304 with no body.
| Status | Codes |
|---|---|
401 | unauthorized |
429 | rate_limited |
500 | internal |
curl -H "Authorization: Bearer $SF_KEY" \
https://c-staging.stocksfast.io/api/v1/openapi.json
→ 200
{"openapi": "3.0.3", "info": {...}, "paths": {...}, "components": {...}}
Market data
GET /catalog
GET /catalogGET https://c-staging.stocksfast.io/api/v1/catalog
200
{
"data": {
"functions": [
{
"name": "sma",
"signature": "sma(source, length)",
"summary": "Simple moving average over the last `length` bars.",
"args": [{"name": "source", "type": "field", "required": true}],
"kwargs": [],
"examples": ["close > sma(close, 50);"]
}
],
"operators": [
{
"operator": ">",
"description": "Greater than",
"category": "comparison",
"example": "close > 50",
"function_equiv": "gt(a, b)"
}
],
"fields": [
{"name": "close", "category": "Price & Volume"}
],
"timeframes": [
{"code": "1D", "label": "Daily"}
]
}
}
Compiled-in reference data, fixed for the life of the server process. Every 200 carries ETag and Cache-Control: public, max-age=3600; send the ETag back as If-None-Match and a match answers 304 with no body.
| Field | Type | Note |
|---|---|---|
functions[] | array | name, summary, signature, return_type, return_nullable, declared_formula, range, notes, see_also, references, related_functions, args, kwargs, examples |
operators[] | array | function_equiv is null on an operator with no function spelling |
fields[] | array | name, category |
timeframes[] | array | code, label |
| Status | Codes |
|---|---|
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
500 | internal |
curl -H "Authorization: Bearer $SF_KEY" \
https://c-staging.stocksfast.io/api/v1/catalog
→ 200
{"data": {"functions": [...], "operators": [...], "fields": [...], "timeframes": [...]}}
GET /universe
GET /universeGET https://c-staging.stocksfast.io/api/v1/universe
200
{
"data": {
"universe_size": 8412,
"last_available_date": "2026-08-19",
"timeframes": [{"code": "1D", "label": "Daily"}]
}
}
A strong ETag derived from the publish generation (universe_size, last_available_date) and the compiled-in timeframes table, rather than the rendered bytes, and Cache-Control: public, max-age=60; send the ETag back as If-None-Match and a match answers 304 with no body. Neither header appears on the 503 or the 500.
| Field | Type | Note |
|---|---|---|
universe_size | int | — |
last_available_date | string|null | null on an empty corpus |
timeframes[] | array | code, label |
| Status | Codes |
|---|---|
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
503 | store_unavailable |
500 | internal |
curl -H "Authorization: Bearer $SF_KEY" \
https://c-staging.stocksfast.io/api/v1/universe
→ 200
{"data": {"universe_size": 8412, "last_available_date": "2026-08-19",
"timeframes": [{"code": "1D", "label": "Daily"}]}}
GET /symbols
GET /symbolsGET https://c-staging.stocksfast.io/api/v1/symbols
| Field | Type | Required | Default | Bounds |
|---|---|---|---|---|
q | string | no | none | case-insensitive ticker substring; absent or empty reads nothing |
limit | int | no | 50 | clamped to [1, 200] |
offset | int | no | 0 | floored at 0 |
200
{
"data": [
{"symbol": "AAPL", "name": "Apple Inc.",
"asset_type": "stock"}
],
"page": {"limit": 2, "offset": 0, "total": 1}
}
A strong ETag that is a digest of the rendered response bytes, so it moves whenever a row's own content changes even with q/limit/offset held fixed, and Cache-Control: public, max-age=60; send the ETag back as If-None-Match and a match answers 304 with no body. Neither header appears on the 500.
| Field | Type | Note |
|---|---|---|
symbol | string | ticker; rows ordered ascending by this field, so a window is stable across requests |
name | string|null | null where the row carries no value |
asset_type | string|null | "stock" or "etf"; null where the row matches neither |
| Status | Codes |
|---|---|
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
500 | internal |
curl -H "Authorization: Bearer $SF_KEY" \
"https://c-staging.stocksfast.io/api/v1/symbols?q=aap&limit=2"
→ 200
{"data": [{"symbol": "AAPL", "name": "Apple Inc.", "asset_type": "stock"}],
"page": {"limit": 2, "offset": 0, "total": 1}}
GET /symbols/<symbol>
GET /symbols/<symbol>GET https://c-staging.stocksfast.io/api/v1/symbols/<symbol>
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
symbol | string | yes | — | non-empty, matched case-insensitively |
200
{
"data": {"symbol": "AAPL", "name": "Apple Inc.",
"asset_type": "stock"}
}
A strong ETag that is a digest of the rendered response bytes, so it moves whenever the row's own content changes even with the symbol held fixed, and Cache-Control: public, max-age=60; send the ETag back as If-None-Match and a match answers 304 with no body. Neither header appears on the 404 or the 500.
| Field | Type | Note |
|---|---|---|
symbol | string | — |
name | string|null | null where the row carries no value |
asset_type | string|null | "stock" or "etf"; null where the row matches neither |
| Status | Codes |
|---|---|
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
404 | not_found |
500 | internal |
curl -H "Authorization: Bearer $SF_KEY" \
https://c-staging.stocksfast.io/api/v1/symbols/AAPL
→ 200
{"data": {"symbol": "AAPL", "name": "Apple Inc.", "asset_type": "stock"}}
Scans
POST /scans/validate
POST /scans/validatePOST https://c-staging.stocksfast.io/api/v1/scans/validate
| Field | Type | Required | Default | Bounds |
|---|---|---|---|---|
expression | string | yes | — | non-empty |
timeframe | string | no | 1D | one of the codes GET /catalog returns |
scan_date | string | no | most recent data date | YYYY-MM-DD; malformed → 400 compile_error; absent against an empty store → 503 store_unavailable |
200
{
"data": {
"expression": "close > sma(close, 50)",
"timeframe": "1D",
"scan_date": "2026-09-03"
},
"warnings": []
}
| Field | Type | Note |
|---|---|---|
expression, timeframe, scan_date | string | echoed/resolved; scan_date is the corpus default or the caller's own, snapped to the period start on a coarser timeframe |
warnings | array | root sibling of data; always present, empty when the compile raised none |
| Status | Codes |
|---|---|
400 | invalid_body · missing_field · compile_error |
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
503 | store_unavailable |
500 | internal · internal_compiler_error |
curl -H "Authorization: Bearer $SF_KEY" \
-H "Content-Type: application/json" \
-d '{"expression": "close > sma(close, 50)", "timeframe": "1D"}' \
https://c-staging.stocksfast.io/api/v1/scans/validate
→ 200
{"data": {"expression": "close > sma(close, 50)", "timeframe": "1D",
"scan_date": "2026-09-03"},
"warnings": []}
POST /scans/execute
POST /scans/executePOST https://c-staging.stocksfast.io/api/v1/scans/execute
| Field | Type | Required | Default | Bounds |
|---|---|---|---|---|
expression | string | yes | — | non-empty |
timeframe | string | no | 1D | one of the codes GET /catalog returns |
scan_date | string | no | most recent data date | YYYY-MM-DD; malformed → 400 compile_error; absent against an empty store → 503 store_unavailable |
symbols | array of string | no | no filter | at most 20000 entries, each non-empty; refused, not truncated, over cap |
watchlist_ids | array of int | no | no filter | at most 100 entries, each one of your own watchlists |
limit | int | no | 50 | clamped to [1, 2000]; wins over the query string |
offset | int | no | 0 | floored at 0 |
200
{
"data": [
{
"symbol": "AAPL",
"close": 231.40,
"volume": 54210033,
"dollar_volume": 12544400637.2
}
],
"page": {"limit": 2, "offset": 0, "total": 137},
"warnings": [
{"code": "W5001", "message": "Standalone nullable function 'pivothigh(...)' filters to non-NULL values. Use '== null' or '!= null' for explicit comparison.", "line": 1, "column": 25}
]
}
| Field | Type | Note |
|---|---|---|
data[] columns | varies | the fixed identity/OHLCV columns plus one per indicator call your expression names; every other engine column is dropped. open/high/low/close and a numeric indicator are numbers, volume is an integer, a boolean indicator (e.g. crossover(...)) is true/false, and symbol/name stay strings. A cell with no value is null regardless of the column's type |
dollar_volume | float | derived; result set ordered by this, descending |
page.total | int | the full match count; a set past 2000 rows is truncated to the first 2000 at limit: 2000 (fewer at a lower limit) — narrow the scan rather than paging past it with offset |
warnings | array of object | root sibling of data and page; always present, empty when the expression compiled clean; each element is {code, message, line?, column?}, with line/column omitted when the diagnostic carries no position; never absent or null, so a caller may branch on it unconditionally |
| Code | Meaning |
|---|---|
invalid_value | a filter resolving to no symbols — an unowned or unknown watchlist id, an empty watchlist, or symbols and watchlist_ids sharing no ticker |
invalid_value | a malformed or oversized Idempotency-Key (see Idempotency) |
No server-side cutoff bounds how long this call can take — see Execution model for the measured worst case and what a timeout does and does not protect against.
| Status | Codes |
|---|---|
400 | invalid_body · missing_field · invalid_value · compile_error |
401 | unauthorized |
403 | forbidden · insufficient_credits |
409 | idempotency_key_reused · idempotency_in_flight |
429 | rate_limited |
500 | engine_error · credit_error · internal · internal_compiler_error |
503 | store_unavailable |
curl -H "Authorization: Bearer $SF_KEY" \
-H "Content-Type: application/json" \
-d '{"expression": "close > sma(close, 50); pivothigh(high, 5, 5);",
"timeframe": "1D", "limit": 2}' \
https://c-staging.stocksfast.io/api/v1/scans/execute
→ 200
{"data": [{"symbol": "AAPL", "close": 231.40, "volume": 54210033,
"dollar_volume": 12544400637.2}],
"page": {"limit": 2, "offset": 0, "total": 137},
"warnings": [{"code": "W5001",
"message": "Standalone nullable function 'pivothigh(...)' filters to non-NULL values. Use '== null' or '!= null' for explicit comparison.",
"line": 1, "column": 25}]}
Saved scans
POST /saved-scans/<id>/execute
POST /saved-scans/<id>/executePOST https://c-staging.stocksfast.io/api/v1/saved-scans/<id>/execute
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | int | yes | — | one of your own; another account's id → 404 |
| Field | Type | Required | Default | Bounds |
|---|---|---|---|---|
timeframe | string | no | 1D | one of the codes GET /catalog returns |
scan_date | string | no | most recent data date | YYYY-MM-DD |
symbols | array of string | no | no filter | at most 20000 entries, each non-empty |
watchlist_ids | array of int | no | no filter | at most 100 entries |
limit | int | no | 50 | clamped to [1, 2000] |
offset | int | no | 0 | floored at 0 |
| Code | Meaning |
|---|---|
invalid_value | expression is not accepted here — the stored one always runs — and is refused like any other key outside the table above; use POST /scans/execute for a different expression |
invalid_value | a malformed or oversized Idempotency-Key (see Idempotency) |
No server-side cutoff bounds how long this call can take — see Execution model for the measured worst case and what a timeout does and does not protect against.
200
{
"data": [
{
"symbol": "AAPL",
"close": 231.40,
"volume": 54210033,
"dollar_volume": 12544400637.2
}
],
"page": {"limit": 2, "offset": 0, "total": 137},
"warnings": [
{"code": "W5001", "message": "Standalone nullable function 'pivothigh(...)' filters to non-NULL values. Use '== null' or '!= null' for explicit comparison.", "line": 1, "column": 28}
]
}
| Field | Type | Note |
|---|---|---|
data[] columns | varies | the fixed identity/OHLCV columns plus one per indicator call the stored expression names; every other engine column is dropped. open/high/low/close and a numeric indicator are numbers, volume is an integer, a boolean indicator (e.g. crossover(...)) is true/false, and symbol/name stay strings. A cell with no value is null regardless of the column's type |
dollar_volume | float | derived; result set ordered by this, descending |
page.total | int | the full match count; a set past 2000 rows is truncated to the first 2000 at limit: 2000 (fewer at a lower limit) — narrow the scan rather than paging past it with offset |
warnings | array of object | root sibling of data and page; always present, empty when the expression compiled clean; each element is {code, message, line?, column?}, with line/column omitted when the diagnostic carries no position; never absent or null, so a caller may branch on it unconditionally |
| Field | Effect |
|---|---|
last_executed_at | stamped to now on a run that settles; updated_at does not move with it |
| Status | Codes |
|---|---|
400 | invalid_body · invalid_value · compile_error |
401 | unauthorized |
403 | forbidden · insufficient_credits |
404 | not_found |
409 | idempotency_key_reused · idempotency_in_flight |
429 | rate_limited |
500 | engine_error · credit_error · internal · internal_compiler_error |
503 | store_unavailable |
curl -X POST -H "Authorization: Bearer $SF_KEY" \
-H "Content-Type: application/json" -d '{"limit": 2}' \
https://c-staging.stocksfast.io/api/v1/saved-scans/41/execute
→ 200
{"data": [{"symbol": "AAPL", "close": 231.40, "volume": 54210033,
"dollar_volume": 12544400637.2}],
"page": {"limit": 2, "offset": 0, "total": 137},
"warnings": [{"code": "W5001",
"message": "Standalone nullable function 'pivothigh(...)' filters to non-NULL values. Use '== null' or '!= null' for explicit comparison.",
"line": 1, "column": 28}]}
GET /saved-scans
GET /saved-scansGET https://c-staging.stocksfast.io/api/v1/saved-scans
| Field | Type | Required | Default | Bounds |
|---|---|---|---|---|
tag | string | no | none | a tag slug of your own; an unmatched slug answers an empty page |
limit | int | no | 50 | clamped to [1, 200] |
offset | int | no | 0 | floored at 0 |
200
{
"data": [
{"id": 41, "name": "Momentum breakout", "description": "",
"expression": "close > sma(close, 50)", "is_favorite": false, "is_public": false,
"created_at": 1755683642, "updated_at": 1755683642,
"last_executed_at": 1755690118, "tags": ["momentum"]}
],
"page": {"limit": 50, "offset": 0, "total": 3}
}
| Field | Type | Note |
|---|---|---|
id | int | server-assigned |
name | string | — |
description | string | — |
expression | string | — |
is_favorite, is_public | bool | — |
created_at, updated_at | int | epoch seconds; server-set |
last_executed_at | int|null | epoch seconds; null on a scan that has never run |
tags | array of string | created on demand in your own namespace |
| Status | Codes |
|---|---|
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
500 | internal |
curl -H "Authorization: Bearer $SF_KEY" \
https://c-staging.stocksfast.io/api/v1/saved-scans
→ 200
{"data": [{"id": 41, "name": "Momentum breakout", "expression": "close > sma(close, 50)", ...}],
"page": {"limit": 50, "offset": 0, "total": 3}}
POST /saved-scans
POST /saved-scansPOST https://c-staging.stocksfast.io/api/v1/saved-scans
| Field | Type | Required | Default | Bounds |
|---|---|---|---|---|
name | string | yes | — | at most 100 characters; unique among your scans |
expression | string | yes | — | must compile as StonQL |
description | string | no | "" | — |
is_favorite | bool | no | false | — |
is_public | bool | no | false | — |
tags | array of string | no | no tags | at most 50 entries; each must carry a letter or digit |
201
{
"data": {"id": 41, "name": "Momentum breakout", "description": "",
"expression": "close > sma(close, 50)", "is_favorite": false, "is_public": false,
"created_at": 1755683642, "updated_at": 1755683642,
"last_executed_at": null, "tags": ["momentum"]}
}
| Field | Type | Note |
|---|---|---|
id | int | server-assigned |
name | string | — |
description | string | — |
expression | string | — |
is_favorite, is_public | bool | — |
created_at, updated_at | int | epoch seconds; server-set |
last_executed_at | int|null | epoch seconds; null on a scan that has never run |
tags | array of string | created on demand in your own namespace |
| Status | Codes |
|---|---|
400 | invalid_body · missing_field · invalid_value |
401 | unauthorized |
403 | forbidden · upgrade_required (plan gate) |
429 | rate_limited |
409 | conflict (a name another of your rows already carries, named in details) |
500 | internal · internal_compiler_error |
curl -X POST -H "Authorization: Bearer $SF_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Momentum breakout", "expression": "close > sma(close, 50)"}' \
https://c-staging.stocksfast.io/api/v1/saved-scans
→ 201
{"data": {"id": 41, "name": "Momentum breakout", "expression": "close > sma(close, 50)", ...}}
GET /saved-scans/<id>
GET /saved-scans/<id>GET https://c-staging.stocksfast.io/api/v1/saved-scans/<id>
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | int | yes | — | one of your own; another account's id → 404 |
200
{
"data": {"id": 41, "name": "Momentum breakout", "description": "",
"expression": "close > sma(close, 50)", "is_favorite": false, "is_public": false,
"created_at": 1755683642, "updated_at": 1755683642,
"last_executed_at": 1755690118, "tags": ["momentum"]}
}
| Field | Type | Note |
|---|---|---|
id | int | server-assigned |
name | string | — |
description | string | — |
expression | string | — |
is_favorite, is_public | bool | — |
created_at, updated_at | int | epoch seconds; server-set |
last_executed_at | int|null | epoch seconds; null on a scan that has never run |
tags | array of string | created on demand in your own namespace |
| Status | Codes |
|---|---|
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
404 | not_found |
500 | internal |
curl -H "Authorization: Bearer $SF_KEY" \
https://c-staging.stocksfast.io/api/v1/saved-scans/41
→ 200
{"data": {"id": 41, "name": "Momentum breakout", "expression": "close > sma(close, 50)", ...}}
PATCH /saved-scans/<id>
PATCH /saved-scans/<id>PATCH https://c-staging.stocksfast.io/api/v1/saved-scans/<id>
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | int | yes | — | one of your own; another account's id → 404 |
| Field | Type | Required | Default | Bounds |
|---|---|---|---|---|
name | string | no | unchanged | at most 100 characters; unique among your scans |
expression | string | no | unchanged | must compile as StonQL |
description | string | no | unchanged | "" clears it |
is_favorite | bool | no | unchanged | false clears it |
is_public | bool | no | unchanged | false clears it |
tags | array of string | no | unchanged | at most 50 entries; [] clears the set |
| Field | Effect |
|---|---|
| Body | required — an absent or non-object body answers 400 |
200
{
"data": {"id": 41, "name": "Momentum breakout", "description": "",
"expression": "close > sma(close, 50)", "is_favorite": false, "is_public": false,
"created_at": 1755683642, "updated_at": 1755690118,
"last_executed_at": 1755690118, "tags": ["momentum"]}
}
| Field | Type | Note |
|---|---|---|
id | int | server-assigned |
name | string | — |
description | string | — |
expression | string | — |
is_favorite, is_public | bool | — |
created_at, updated_at | int | epoch seconds; server-set |
last_executed_at | int|null | epoch seconds; null on a scan that has never run |
tags | array of string | created on demand in your own namespace |
| Status | Codes |
|---|---|
400 | invalid_body · invalid_value |
401 | unauthorized |
403 | forbidden · upgrade_required (plan gate) |
429 | rate_limited |
404 | not_found |
409 | conflict (a name another of your rows already carries, named in details) |
500 | internal · internal_compiler_error |
curl -X PATCH -H "Authorization: Bearer $SF_KEY" \
-H "Content-Type: application/json" -d '{"is_favorite": true}' \
https://c-staging.stocksfast.io/api/v1/saved-scans/41
→ 200
{"data": {"id": 41, "name": "Momentum breakout", "is_favorite": true, ...}}
DELETE /saved-scans/<id>
DELETE /saved-scans/<id>DELETE https://c-staging.stocksfast.io/api/v1/saved-scans/<id>
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | int | yes | — | one of your own; another account's id → 404 |
204
| Field | Effect |
|---|---|
| — | the scan's tag links go with it |
| Status | Codes |
|---|---|
401 | unauthorized |
403 | forbidden · upgrade_required (plan gate) |
429 | rate_limited |
404 | not_found |
500 | internal |
curl -X DELETE -H "Authorization: Bearer $SF_KEY" \
https://c-staging.stocksfast.io/api/v1/saved-scans/41
→ 204
Tags
GET /tags
GET /tagsGET https://c-staging.stocksfast.io/api/v1/tags
| Field | Type | Required | Default | Bounds |
|---|---|---|---|---|
limit | int | no | 50 | clamped to [1, 200] |
offset | int | no | 0 | floored at 0 |
200
{
"data": [{"id": 12, "name": "Momentum", "slug": "momentum", "created_at": 1755683642}],
"page": {"limit": 50, "offset": 0, "total": 4}
}
| Field | Type | Note |
|---|---|---|
id | int | server-assigned |
name | string | — |
slug | string | derived from name; what makes two names the same tag; server-set |
created_at | int | epoch seconds; server-set |
| Status | Codes |
|---|---|
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
500 | internal |
curl -H "Authorization: Bearer $SF_KEY" \
https://c-staging.stocksfast.io/api/v1/tags
→ 200
{"data": [{"id": 12, "name": "Momentum", "slug": "momentum", "created_at": 1755683642}], "page": {"limit": 50, "offset": 0, "total": 4}}
POST /tags
POST /tagsPOST https://c-staging.stocksfast.io/api/v1/tags
| Field | Type | Required | Default | Bounds |
|---|---|---|---|---|
name | string | yes | — | at most 100 characters; must carry a letter or digit |
201
{
"data": {"id": 12, "name": "Momentum", "slug": "momentum", "created_at": 1755683642}
}
| Field | Type | Note |
|---|---|---|
id | int | server-assigned |
name | string | — |
slug | string | derived from name; what makes two names the same tag; server-set |
created_at | int | epoch seconds; server-set |
| Status | Codes |
|---|---|
400 | invalid_body · missing_field · invalid_value |
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
409 | conflict (a name another of your rows already carries, named in details) |
500 | internal |
curl -X POST -H "Authorization: Bearer $SF_KEY" \
-H "Content-Type: application/json" -d '{"name": "Momentum"}' \
https://c-staging.stocksfast.io/api/v1/tags
→ 201
{"data": {"id": 12, "name": "Momentum", "slug": "momentum", "created_at": 1755683642}}
GET /tags/<id>
GET /tags/<id>GET https://c-staging.stocksfast.io/api/v1/tags/<id>
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | int | yes | — | one of your own; another account's id → 404 |
200
{
"data": {"id": 12, "name": "Momentum", "slug": "momentum", "created_at": 1755683642}
}
| Field | Type | Note |
|---|---|---|
id | int | server-assigned |
name | string | — |
slug | string | derived from name; what makes two names the same tag; server-set |
created_at | int | epoch seconds; server-set |
| Status | Codes |
|---|---|
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
404 | not_found |
500 | internal |
curl -H "Authorization: Bearer $SF_KEY" \
https://c-staging.stocksfast.io/api/v1/tags/12
→ 200
{"data": {"id": 12, "name": "Momentum", "slug": "momentum", "created_at": 1755683642}}
PATCH /tags/<id>
PATCH /tags/<id>PATCH https://c-staging.stocksfast.io/api/v1/tags/<id>
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | int | yes | — | one of your own; another account's id → 404 |
| Field | Type | Required | Default | Bounds |
|---|---|---|---|---|
name | string | no | unchanged | at most 100 characters; must carry a letter or digit |
| Field | Effect |
|---|---|
| Body | required — an absent or non-object body answers 400 |
200
{
"data": {"id": 12, "name": "Momentum breakouts", "slug": "momentum-breakouts",
"created_at": 1755683642}
}
| Field | Type | Note |
|---|---|---|
id | int | server-assigned |
name | string | — |
slug | string | derived from name; what makes two names the same tag; server-set |
created_at | int | epoch seconds; server-set |
slug on rename | string | re-derived; a rename onto a slug you already hold → 409 conflict |
| Status | Codes |
|---|---|
400 | invalid_body · invalid_value |
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
404 | not_found |
409 | conflict (a name another of your rows already carries, named in details) |
500 | internal |
curl -X PATCH -H "Authorization: Bearer $SF_KEY" \
-H "Content-Type: application/json" -d '{"name": "Momentum breakouts"}' \
https://c-staging.stocksfast.io/api/v1/tags/12
→ 200
{"data": {"id": 12, "name": "Momentum breakouts", "slug": "momentum-breakouts", "created_at": 1755683642}}
DELETE /tags/<id>
DELETE /tags/<id>DELETE https://c-staging.stocksfast.io/api/v1/tags/<id>
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | int | yes | — | one of your own; another account's id → 404 |
204
| Field | Effect |
|---|---|
| — | the tag's links to saved scans go with it; the scans stay |
| Status | Codes |
|---|---|
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
404 | not_found |
500 | internal |
curl -X DELETE -H "Authorization: Bearer $SF_KEY" \
https://c-staging.stocksfast.io/api/v1/tags/12
→ 204
Watchlists
GET /watchlists
GET /watchlistsGET https://c-staging.stocksfast.io/api/v1/watchlists
| Field | Type | Required | Default | Bounds |
|---|---|---|---|---|
limit | int | no | 50 | clamped to [1, 200] |
offset | int | no | 0 | floored at 0 |
200
{
"data": [
{"id": 7, "name": "Swing candidates",
"created_at": 1755683642, "updated_at": 1755683642}
],
"page": {"limit": 50, "offset": 0, "total": 2}
}
| Field | Type | Note |
|---|---|---|
id | int | — |
name | string | — |
created_at, updated_at | int | epoch seconds |
| Field | Effect |
|---|---|
symbols | not included here; read GET /watchlists/<id> for membership |
| Status | Codes |
|---|---|
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
500 | internal |
curl -H "Authorization: Bearer $SF_KEY" \
https://c-staging.stocksfast.io/api/v1/watchlists
→ 200
{"data": [{"id": 7, "name": "Swing candidates", "created_at": 1755683642, "updated_at": 1755683642}],
"page": {"limit": 50, "offset": 0, "total": 2}}
POST /watchlists
POST /watchlistsPOST https://c-staging.stocksfast.io/api/v1/watchlists
| Field | Type | Required | Default | Bounds |
|---|---|---|---|---|
name | string | yes | — | non-blank, at most 100 characters |
symbols | array of string | no | empty membership | at most 1000 entries, each at most 10 bytes |
201
{
"data": {"id": 7, "name": "Swing candidates",
"created_at": 1755683642, "updated_at": 1755683642,
"symbols": ["AAPL", "MSFT"]}
}
| Field | Type | Note |
|---|---|---|
id, created_at, updated_at | int | server-set |
symbols | array of string | present when the body carried it; lists membership actually written — an unknown symbol is refused and absent from the array |
| Status | Codes |
|---|---|
400 | invalid_body · missing_field · invalid_value |
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
409 | conflict (a name another of your rows already carries, named in details) |
500 | internal |
curl -X POST -H "Authorization: Bearer $SF_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Swing candidates", "symbols": ["AAPL", "MSFT"]}' \
https://c-staging.stocksfast.io/api/v1/watchlists
→ 201
{"data": {"id": 7, "name": "Swing candidates", "symbols": ["AAPL", "MSFT"], ...}}
GET /watchlists/<id>
GET /watchlists/<id>GET https://c-staging.stocksfast.io/api/v1/watchlists/<id>
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | int | yes | — | one of your own; another account's id → 404 |
200
{
"data": {"id": 7, "name": "Swing candidates",
"created_at": 1755683642, "updated_at": 1755683642,
"symbols": ["AAPL", "MSFT"]}
}
| Field | Type | Note |
|---|---|---|
id, created_at, updated_at | int | — |
symbols | array of string | always present here, empty on a watchlist with no members |
| Status | Codes |
|---|---|
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
404 | not_found |
500 | internal |
curl -H "Authorization: Bearer $SF_KEY" \
https://c-staging.stocksfast.io/api/v1/watchlists/7
→ 200
{"data": {"id": 7, "name": "Swing candidates", "symbols": ["AAPL", "MSFT"], ...}}
PATCH /watchlists/<id>
PATCH /watchlists/<id>PATCH https://c-staging.stocksfast.io/api/v1/watchlists/<id>
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | int | yes | — | one of your own; another account's id → 404 |
| Field | Type | Required | Default | Bounds |
|---|---|---|---|---|
name | string | no | unchanged | non-blank, at most 100 characters |
symbols | array of string | no | membership unchanged | at most 1000 entries, each at most 10 bytes; a present array replaces the whole membership, [] clears it |
| Field | Effect |
|---|---|
| Body | required — an absent or non-object body answers 400 |
200
{
"data": {"id": 7, "name": "Swing candidates",
"created_at": 1755683642, "updated_at": 1755690118,
"symbols": ["AAPL", "NVDA"]}
}
| Field | Type | Note |
|---|---|---|
symbols | array of string | present when the body carried it; a membership replace that fails leaves the prior membership in place and the reply omits symbols |
| Status | Codes |
|---|---|
400 | invalid_body · invalid_value |
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
404 | not_found |
409 | conflict (a name another of your rows already carries, named in details) |
500 | internal |
curl -X PATCH -H "Authorization: Bearer $SF_KEY" \
-H "Content-Type: application/json" -d '{"symbols": ["AAPL", "NVDA"]}' \
https://c-staging.stocksfast.io/api/v1/watchlists/7
→ 200
{"data": {"id": 7, "name": "Swing candidates", "symbols": ["AAPL", "NVDA"], ...}}
DELETE /watchlists/<id>
DELETE /watchlists/<id>DELETE https://c-staging.stocksfast.io/api/v1/watchlists/<id>
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | int | yes | — | one of your own; another account's id → 404 |
204
| Field | Effect |
|---|---|
| — | the watchlist's symbols go with it |
| Status | Codes |
|---|---|
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
404 | not_found |
500 | internal |
curl -X DELETE -H "Authorization: Bearer $SF_KEY" \
https://c-staging.stocksfast.io/api/v1/watchlists/7
→ 204
POST /watchlists/<id>/add-symbols
POST /watchlists/<id>/add-symbolsPOST https://c-staging.stocksfast.io/api/v1/watchlists/<id>/add-symbols
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | int | yes | — | one of your own; another account's id → 404 |
| Field | Type | Required | Default | Bounds |
|---|---|---|---|---|
symbols | array of string | yes | — | at most 1000 entries, each at most 10 bytes; [] is accepted and adds nothing |
200 — the single-resource envelope
{
"data": {
"added_count": 2,
"skipped_count": 1,
"invalid_count": 0,
"watchlist": {
"id": 7, "user_id": 7, "name": "Swing candidates",
"created_at": 1755683642,
"updated_at": 1755690118,
"symbols": ["AAPL", "MSFT", "NVDA"]
}
}
}
| Field | Type | Note |
|---|---|---|
added_count | int | — |
skipped_count | int | already on the list |
invalid_count | int | market data does not carry it |
watchlist | object | epoch-second timestamps, matching the collection routes for the same columns |
| Status | Codes |
|---|---|
400 | invalid_body · invalid_value |
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
404 | not_found |
500 | internal |
curl -X POST -H "Authorization: Bearer $SF_KEY" \
-H "Content-Type: application/json" -d '{"symbols": ["AAPL", "MSFT", "NVDA"]}' \
https://c-staging.stocksfast.io/api/v1/watchlists/7/add-symbols
→ 200
{
"data": {
"added_count": 2,
"skipped_count": 1,
"invalid_count": 0,
"watchlist": {
"id": 7, "user_id": 7, "name": "Swing candidates",
"created_at": 1755683642,
"updated_at": 1755690118,
"symbols": ["AAPL", "MSFT", "NVDA"]
}
}
}
POST /watchlists/<id>/remove-symbols
POST /watchlists/<id>/remove-symbolsPOST https://c-staging.stocksfast.io/api/v1/watchlists/<id>/remove-symbols
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | int | yes | — | one of your own; another account's id → 404 |
| Field | Type | Required | Default | Bounds |
|---|---|---|---|---|
symbols | array of string | yes | — | at most 1000 entries, each at most 10 bytes; [] is accepted and removes nothing |
200 — the single-resource envelope
{
"data": {
"removed_count": 1,
"watchlist": {
"id": 7, "user_id": 7, "name": "Swing candidates",
"created_at": 1755683642,
"updated_at": 1755697451,
"symbols": ["AAPL", "MSFT"]
}
}
}
| Field | Type | Note |
|---|---|---|
removed_count | int | a symbol not on the list contributes nothing |
watchlist | object | epoch-second timestamps, matching the collection routes for the same columns |
| Status | Codes |
|---|---|
400 | invalid_body · invalid_value |
401 | unauthorized |
403 | forbidden |
429 | rate_limited |
404 | not_found |
500 | internal |
curl -X POST -H "Authorization: Bearer $SF_KEY" \
-H "Content-Type: application/json" -d '{"symbols": ["NVDA"]}' \
https://c-staging.stocksfast.io/api/v1/watchlists/7/remove-symbols
→ 200
{
"data": {
"removed_count": 1,
"watchlist": {
"id": 7, "user_id": 7, "name": "Swing candidates",
"created_at": 1755683642,
"updated_at": 1755697451,
"symbols": ["AAPL", "MSFT"]
}
}
}
GET /watchlists/<id>/export-tradingview
GET /watchlists/<id>/export-tradingviewGET https://c-staging.stocksfast.io/api/v1/watchlists/<id>/export-tradingview
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | int | yes | — | one of your own; another account's id → 404 |
200 — text/plain, outside the envelope, with Content-Disposition: attachment
NASDAQ:AAPL
NASDAQ:MSFT
NYSE:BRK.B
| Rule | — | Detail |
|---|---|---|
| one line per member | — | EXCHANGE:SYMBOL, no trailing newline; an empty watchlist answers an empty body |
| share-class suffix | — | dot form preserved |
| unresolved exchange | — | labelled NASDAQ |
| Status | Codes |
|---|---|
401 | unauthorized |
403 | forbidden · upgrade_required (plan gate, checked before the id is read) |
429 | rate_limited |
404 | not_found |
500 | internal |
curl -H "Authorization: Bearer $SF_KEY" \
https://c-staging.stocksfast.io/api/v1/watchlists/7/export-tradingview
→ 200
NASDAQ:AAPL
NASDAQ:MSFT
NYSE:BRK.B
POST /watchlists/import-tradingview
POST /watchlists/import-tradingviewPOST https://c-staging.stocksfast.io/api/v1/watchlists/import-tradingview
| Field | Type | Required | Default | Bounds |
|---|---|---|---|---|
file | file part | yes | — | .txt, at most 5 MB, UTF-8; at most 1000 symbols, each at most 10 bytes |
watchlist_id | string | no | none | one of your own watchlists; present means add to that list |
watchlist_name | string | no | uploaded filename, else Imported Watchlist | used when creating a new list |
200 (add-to-existing) or 201 (create-new) — the single-resource envelope
{
"data": {
"watchlist": {
"id": 9, "user_id": 7, "name": "tv-export",
"created_at": 1755683642,
"updated_at": 1755683642,
"symbols": ["AAPL", "MSFT"]
},
"added_count": 2, "skipped_count": 0, "invalid_count": 1,
"added_symbols": ["AAPL", "MSFT"], "invalid_symbols": ["ZZZZ"],
"unsupported_count": 1, "unsupported_symbols": ["LSE:VOD"],
"malformed_count": 1, "malformed_symbols": ["not a line"]
}
}
| Field | Type | Note |
|---|---|---|
watchlist | object | epoch-second timestamps |
invalid_* | int/array | symbols the market data does not carry |
unsupported_* | int/array | lines on an exchange this data does not cover |
malformed_* | int/array | lines that never split into EXCHANGE:SYMBOL |
| Field | Effect |
|---|---|
| upload checks | run in order — extension, size, encoding — and all three refuse before any watchlist is written; a file parsing to nothing is still 200, with the parse errors on the response |
| Status | Codes |
|---|---|
400 | missing_field · invalid_value (an upload check names file in details) |
401 | unauthorized |
403 | forbidden · upgrade_required (plan gate, runs ahead of the upload checks) |
429 | rate_limited |
404 | not_found |
409 | conflict (the create-new branch's watchlist_name already held by another of your watchlists, named in details) |
500 | internal |
curl -X POST -H "Authorization: Bearer $SF_KEY" \
-F "file=@watchlist.txt" -F "watchlist_name=tv-export" \
https://c-staging.stocksfast.io/api/v1/watchlists/import-tradingview
→ 201
{
"data": {
"watchlist": {
"id": 9, "user_id": 7, "name": "tv-export",
"created_at": 1755683642,
"updated_at": 1755683642,
"symbols": ["AAPL", "MSFT"]
},
"added_count": 2, "skipped_count": 0, "invalid_count": 1,
"added_symbols": ["AAPL", "MSFT"], "invalid_symbols": ["ZZZZ"],
"unsupported_count": 1, "unsupported_symbols": ["LSE:VOD"],
"malformed_count": 1, "malformed_symbols": ["not a line"]
}
}
Errors
| Code | Status | Cause |
|---|---|---|
bad_request | 400 | A malformed request refused below the routes, before any handler ran. |
invalid_body | 400 | The body did not parse: absent, not application/json, unparseable, or parsed but not a JSON object. |
missing_field | 400 | A required field is absent. |
invalid_value | 400 | A value the route refuses: a wrongly-typed field, an over-cap or malformed array entry, a filter resolving to no symbols, a blank or over-long name, a malformed path id, an unsupported TradingView upload, or an expression that will not compile on a saved-scan write with STONQL_CAT_INPUT -- any other compile category answers internal_compiler_error below instead. The saved-scan, tag and watchlist routes name the finer reason in details for a conflicting inline tag, an over-cap/malformed tag list entry, or a saved-scan expression that fails to compile with STONQL_CAT_INPUT; every other 400 the generic dispatch built, whether from its own type check or a resource's pre_validate hook, carries no details, reason in message only. |
compile_error | 400 | A scan validate/execute route's expression did not compile on the caller's own input. message is the compiler's own diagnostic. |
internal_compiler_error | 500 | The compiler or this app faulted on its own compiling an expression, on a scan validate/execute route or a saved-scan write. message is a generic string; the diagnostic is logged server-side only. |
unauthorized | 401 | The key is missing, malformed, unknown, revoked or expired. The answer never says which. |
forbidden | 403 | The key authenticated but does not carry the route's permission. The answer never says which permission. |
insufficient_credits | 403 | No scan credits and no Premium, on an execute route. The body carries balance and cost alongside code/message, naming the shortfall. |
upgrade_required | 403 | The saved-scan writes and the two TradingView actions refuse a caller whose plan does not cover the action: over the free-tier saved-scan quota, saved data is read-only, or the action itself is Premium-gated. |
not_found | 404 | No such row, including one belonging to another account; or an unknown symbol on the detail route. |
method_not_allowed | 405 | A registered path hit with a method it neither accepts nor gets implicitly. HEAD on a path with a registered GET is not a wrong verb -- it replays the GET handler and its gates exactly, body suppressed after the fact: same status, headers and Content-Length. OPTIONS on any registered path is a fourth outcome, always 204, and never reaches a handler. Either way the response carries an Allow header naming the accepted methods plus implicit HEAD/OPTIONS. |
conflict | 409 | A unique constraint refused the write: a saved-scan or watchlist name, a tag (user, slug), or the TradingView import's create-new watchlist_name, another of your rows already carries. details names the collided field. |
idempotency_key_reused | 409 | An execute route's Idempotency-Key was reused with a different request. See Idempotency. |
idempotency_in_flight | 409 | An execute route's Idempotency-Key is still reserved by a request in flight. Wait out Retry-After. See Idempotency. |
rate_limited | 429 | The route's per-key budget is spent. Wait out Retry-After. |
too_many_concurrent_executes | 429 | The execute routes share a cap on scans in flight, checked before the run starts. No rate window is involved and no credit is spent; the RateLimit-* headers still describe the execute bucket, which was already spent. Wait out Retry-After, which is short. |
internal | 500 | Something broke on our side. The request was fine. On the execute routes, a response page that fails to build is refused this way, and no credit is spent. If it persists, report it with X-Request-Id from the response. |
credit_error | 500 | The credit ledger write failed after the run. The built page is discarded; no results are served. If it persists, report it with X-Request-Id from the response. |
engine_error | 500 | The scan query failed in the engine. If it persists, report it with X-Request-Id from the response. |
client_error | any other 4xx | The class generic for a 4xx no row above names. Read the status. |
server_error | any other 5xx | The class generic for a 5xx no row above names. Retry. |
store_unavailable | 503 | The market data store would not open, is empty where a default scan date had to be resolved, or a read like universe reaches a store that refuses its query. Wait out Retry-After. An execute route also answers it when an Idempotency-Key reservation could not be claimed. No credit is spent on the execute routes. |
Response headers
Look a header up here when you see it on a response. Nothing in this table is something you send — for that, see Base URL and request headers.
| Header | On | Meaning |
|---|---|---|
X-API-Stability |
every response | beta, success and error alike. See Stability. |
X-Request-Id |
every response | The correlation id for this request, yours if you sent a well-formed one. Quote it when reporting internal, internal_compiler_error, engine_error or credit_error. |
WWW-Authenticate |
401 |
Bearer error="invalid_token" when a Bearer credential was presented and rejected; bare Bearer when none was presented. |
Idempotency-Replayed |
200 |
true when the response is a stored replay rather than a fresh run; absent otherwise. |
Retry-After |
429 |
Seconds left in the rate-limit window — except the execute routes' concurrency-cap refusal, a fixed short wait unrelated to any window. |
Retry-After |
409 |
An Idempotency-Key still reserved by a request in flight only; a short fixed hint. |
Retry-After |
503 |
30, a fixed hint; the condition clears on its own. |
RateLimit-Limit |
metered response | The spent bucket's ceiling. See Rate limits. |
RateLimit-Remaining |
metered response | Attempts left in that bucket after this request. |
RateLimit-Reset |
metered response | Seconds until that bucket's window rolls; matches Retry-After on a rate-limit 429, but not on the execute routes' concurrency-cap 429, whose bucket is spent ahead of that check. |
ETag |
GET /catalog, GET /openapi.json |
A strong validator over the rendered body, stable for the life of the process. Present on 200 and 304 alike. |
ETag |
GET /universe |
A strong validator that moves with the corpus's publish generation (last_available_date, universe_size) and the timeframes table, not the rendered bytes. Present on 200 and 304 alike; absent on every error status. |
ETag |
GET /symbols, GET /symbols/<symbol> |
A strong validator that is a digest of the rendered response bytes. Present on 200 and 304 alike; absent on every error status. |
Cache-Control |
GET /me |
no-store. credit_balance must never come from a shared or client cache. |
Cache-Control |
GET /catalog, GET /openapi.json |
public, max-age=3600, on 200 and 304 alike. |
Cache-Control |
GET /universe, GET /symbols, GET /symbols/<symbol> |
public, max-age=60, on 200 and 304 alike. |
Content-Disposition |
200 |
TradingView export only: attachment; filename="watchlist-<name>-YYYY-MM-DD.txt". |
Allow |
405, and 204 on OPTIONS |
The path's registered methods, plus implicit HEAD (when GET is registered) and implicit OPTIONS (always). See Errors. |
More
- StonQL reference — the expression language.
- Example library — expressions to start from.
- Account › API keys — mint, list and revoke keys.