Strasmore API · v1
The market, in one schema.
Run SQL directly against a full US market-data warehouse. Equities minute bars and tick data for exchange-listed stocks and ETFs from September 2003 (no OTC prints), options trades from 2014, options NBBO quotes from 2022, plus corporate actions, SEC filings, short interest, fundamentals, Treasury yields and macro series. It all lives in one schema, so you can join across datasets in a single query.
- 22 years
- US equities, since Sep 2003
- 12 years
- options trades, since 2014
- 36
- tables, one schema
- 1
- endpoint to learn
One request, one query
There is a single data endpoint. You send SQL, you get rows. There is no per-dataset endpoint to learn, no pagination cursor scheme, and no client library required.
Quickstart
Create a key in the terminal (https://ai.strasmore.com → API keys), then send a query. The key is displayed once: we store only its SHA-256 hash and cannot show it to you again.
The response is JSON. Rows come back as objects keyed by column name.
Try it — no key
Run read-only SQL against global_markets right here. This posts to
https://ai.strasmore.com/api/demo/sql, the public demo endpoint: no key, no signup,
500 rows, 20s, 1 year of history, no tick trades, and 5 days of the NBBO quote tapes. It is the same gate and the same warehouse your key would query.
Authentication
Every endpoint except /v1/health takes a bearer token:
Authorization: Bearer sk_live_…
Keys resolve to an organization, and limits are applied per organization, not per key. Minting extra keys does not raise your rate limit. Revoking a key takes effect immediately.
Run a query
Execute one read-only SELECT (or WITH … SELECT) against global_markets.
Body
| Field | Description |
|---|---|
| sqlrequired string |
One read-only statement. Maximum 100,000 characters. |
Response
| Field | Description |
|---|---|
| columns string[] |
Column names, in result order. |
| rows object[] |
One object per row, keyed by column name. Not positional arrays. |
| row_count integer |
Rows returned, after any truncation. |
| truncated boolean |
True if the result hit the 20,000-row cap. The result is truncated, not
errored — narrow the query, aggregate, or page with LIMIT/OFFSET. |
| elapsed float |
Server-side execution time, in seconds. |
| generated_sql string | null |
Always null today. Reserved so that adding natural-language querying later is
additive rather than a breaking change. |
Fetch the schema
Every table and column you can query, read from the warehouse itself at request time — so it
can never drift from what your query will actually see. It reflects your tier:
tables you cannot query are flagged, each table names the column its history window filters on,
columns with known data defects carry a caveat, and columns whose meaning the name
does not give away carry a note. plan_name beside your_tier
is the pricing-page name of the slug (quotes is Pro).
Health
Liveness only. Returns {"status": "ok"}. Does not check the warehouse.
SQL restrictions
Your SQL runs under a read-only database user, behind a gate that requires:
- Exactly one statement, and it must be a
SELECTor aWITH … SELECT. Multiple statements are rejected with a 400 — we will not silently execute the first and drop the rest. - No
SETTINGSclause. It is stripped: it could otherwise override the server-side caps. - No table functions (
remote,url,merge,file, …), anywhere, including inside subqueries. - No
system.*, and no database other thanglobal_markets. - No DDL or DML of any kind.
A semicolon inside a string literal is fine (WHERE name = 'Smith; Jones'), and so is a single
trailing semicolon. The engine is ClickHouse, so its SQL dialect and functions apply.
Limits
| Limit | Value |
|---|---|
| Result rows | 20,000 per query. Beyond this the result is truncated, not errored —
check the truncated flag. |
| Query timeout | 60 seconds, enforced server-side. |
| Request body | 100,000 characters of SQL. Larger returns 422. |
| Free-tier daily cap | 100 queries/day, in addition to the rate limit. |
Rate limits
Requests per minute, applied per organization. Exceeding it returns 429. Separately, each organization can run a limited number of queries at the same time; see Concurrency.
| Plan | Requests / minute |
|---|---|
| Free | 10 |
| Developer | 60 |
| Pro | 600 |
| Enterprise | 1200 |
Writing fast queries
Your account can run a limited number of queries at the same time, so a query that finishes sooner gives you more throughput. The tick-level tables are very large (the options quote tape alone holds about 7.5 trillion rows), so on them the way a query is written matters most. The advice below comes from measuring real customer queries against the warehouse.
Filter on the sort order
The tick and minute tables (cache_options_quotes, cache_stocks_quotes,
stocks_trades, options_trades, options_minute_aggs) are stored in one
partition per day and sorted by ticker, then time. A query that names its tickers and a time range
reads only the rows it needs:
WHERE ticker IN ('O:SPY230207P00400000', 'O:SPY230207P00401000')
AND sip_timestamp >= '2023-02-03 14:30:00'
AND sip_timestamp < '2023-02-03 21:00:00'
- Name tickers exactly (
=orIN), or by a prefix (LIKE 'O:SPY230207P%'orstartsWith(ticker, 'O:SPY230207P')). On one day of options quotes, the two exact tickers above read 14 blocks of rows and the prefix reads 978. - Do not wrap
tickerin a function (substring(ticker, …)) or match it with a pattern that does not start at the beginning (LIKE '%P004%'). Either one reads every contract that traded that day: 828,045 blocks, about 6.8 billion rows. - Always give a time range. A plain start and end on the timestamp column is the tightest form;
toDate(sip_timestamp) = '2023-02-03'also skips the other days.
Ask for more in each query
Every query has a fixed start-up cost, so many small queries are slower than a few larger ones. Put all the contracts you need for a day in one query, up to the 20,000-row result limit. A full US session of 1-minute bars is 390 rows per contract, so about 50 contracts fit in one query. In our tests, one query for 20 contracts ran 3 times faster than 20 one-contract queries, and one query for 100 contracts ran about 1.5 times faster than five queries of 20.
Keep the per-row work simple
Work done for every row adds up over millions of rows. Prices are exact decimals and sizes are
integers, so checks such as isFinite(toFloat64(bid_price)) or
toUInt64OrNull(toString(bid_size)) never fail and only cost time; compare the columns directly.
When you need several fields from the same row, such as the last quote in each minute, take them in one
argMax over a tuple instead of one argMax per field. On a customer's 1-minute NBBO
query, these two changes returned identical results about 20% faster:
WITH bid_price >= 0 AND ask_price >= bid_price AND bid_size >= 0 AND ask_size >= 0 AS valid
SELECT ticker,
toStartOfMinute(sip_timestamp) AS minute,
argMaxIf((sip_timestamp, bid_price, ask_price, bid_size, ask_size),
(sip_timestamp, sequence_number), valid) AS last_quote,
countIf(valid) AS quotes
FROM global_markets.cache_options_quotes
WHERE ticker IN ('O:SPY230207P00400000', 'O:SPY230207P00401000')
AND sip_timestamp >= '2023-02-03 14:30:00' AND sip_timestamp < '2023-02-03 21:00:00'
GROUP BY ticker, minute
ORDER BY ticker, minute
Concurrency
Besides the per-minute rate limit, each organization can run a limited number of queries at the
same moment. The number is not fixed: it rises and falls with how busy the warehouse is, and larger plans get a
larger share. A query over the limit waits a few seconds for a free slot; if none opens, it returns
429 THROTTLED with a Retry-After header.
- Throughput is queries at once divided by seconds per query: 5 at once at 0.5 seconds each is 600 per minute, and 5 at once at 10 seconds each is 30. Faster queries raise it; more parallel requests do not.
- Start with 4 or 5 workers. More workers than your share only produce 429s.
- On a 429, wait the
Retry-Afterseconds, then send the same request again.
Error contract
Every error is a JSON object with detail, a stable error_code and a
request_id. A query that returns zero rows is a 200, not a 404 — unless it
asks only for dates before your plan's history window, which is a 402.
| Status | Meaning |
|---|---|
| 400 | The SQL was rejected by the gate (not a single read-only SELECT, a forbidden table, a
dataset your tier cannot access), or it failed to execute (syntax, unknown column).
When a plan would run it (TIER_RESTRICTED_TABLE), the body names it in
upgrade_plan and carries checkout_url, a link that opens checkout. |
| 401 | Missing, malformed, unknown, or revoked API key. |
| 402 | HISTORY_WINDOW_EXCEEDED — the query asks only for dates before your plan's history
window, so it can return nothing on this plan. The body names the window's start, the dates you
asked for, the cheapest plan that reads them (upgrade_plan) and a
checkout_url that opens checkout for it. |
| 422 | Malformed request body — sql missing, or over 100,000 characters. |
| 429 | RATE_LIMITED (the per-minute limit), DAILY_QUOTA_EXCEEDED (the free-tier
daily cap), or THROTTLED (more queries running at once than your share of the warehouse;
see Concurrency). Every 429 carries a Retry-After header in
seconds: wait that long, then send the same request again. |
| 503 | WAREHOUSE_UNREACHABLE — the problem is on our side, not in your query. Retry after the
Retry-After seconds. |
| 500 | Internal error. Details are logged server-side and never returned to you. |
History windows
Each plan sees a uniform depth of history across all datasets. The window is enforced
server-side, on the column named by history_column in the schema catalogue.
Windows count whole calendar years, not a rolling number of days. An N-year window
starts on 1 January, N years before the current year: in 2026, a 1-year window starts on
1 January 2025 and a 3-year window on 1 January 2023. The start stays
fixed all year and moves forward by one year each 1 January. /v1/schema reports your
exact start as history_window_start.
Older rows are withheld, and the response says so
Rows older than your window are absent from results, not an error. Every response on a
windowed plan carries a history_window object: start, how far back your SQL
requested_from, and clipped (true when older rows were withheld).
A query that asks only for dates before the window is a 402
HISTORY_WINDOW_EXCEEDED, because no edit to the SQL can produce rows on that plan.
| Plan | History | Earliest date today |
|---|---|---|
| Free | 1 year | 1 January 2025 |
| Developer | 3 years | 1 January 2023 |
| Pro | Full history | Start of each dataset |
| Enterprise | Full history | Start of each dataset |
Schema catalogue
36 tables in global_markets, read live from the warehouse.
Query them as global_markets.<table>. Tables marked
paid are unavailable on the free tier.
Equities — prices
delayed_stocks_minute_aggs 9 columns Minute OHLCV bars for US equities. The main price table. `window_start` is UTC.
history window applies to: window_start
stocks_daily_aggs 11 columns Daily OHLCV bars for US equities, with `vwap` and `transactions`. The end-of-day counterpart to delayed_stocks_minute_aggs; use this for anything longer than a few sessions.
history window applies to: date
Equities — tick data
stocks_trades 12 columns paid tier Tick-level equity trades (every print, with condition codes) for exchange-listed US stocks and ETFs: the consolidated CTA + UTP tape from 2003-09-10, current to the prior session. No OTC prints, so a delisted name ends on its last listed session. Paid tiers.
history window applies to: sip_timestamp
cache_stocks_quotes 15 columns 5-day preview · full history on Pro Tick-level equity NBBO quotes. Paid tiers. From 2003-09-10.
history window applies to: sip_timestamp
Options
options_greeks 18 columns paid tier Daily Black-Scholes IV + greeks per contract (EOD close, delta/gamma/vega/theta/rho). One row per contract per session, 2014 onward, refreshed nightly. Paid tiers.
history window applies to: date
options_trades 13 columns paid tier Tick-level options trades (OPRA). Paid tiers. From 2014.
history window applies to: sip_timestamp
cache_options_quotes 10 columns 5-day preview · full history on Pro Tick-level options NBBO quotes. Paid tiers. From 2022. The largest table by far.
history window applies to: sip_timestamp
options_minute_aggs 9 columns Minute OHLCV bars per options contract.
history window applies to: window_start
Corporate actions
stocks_dividends 13 columns Declared dividends: ex-date, record date, pay date, cash amount.
history window applies to: ex_dividend_date
stocks_splits 8 columns Stock splits (forward and reverse) with execution date and ratio.
history window applies to: execution_date
stocks_ipos 23 columns IPO / new listing records: issue price, shares offered, listing date.
history window applies to: listing_date
Short data
stocks_short_interest 6 columns Exchange-reported short interest, by settlement date (twice monthly).
history window applies to: settlement_date
stocks_short_volume 16 columns Daily FINRA short volume (can carry duplicate rows: GROUP BY + max()).
history window applies to: date
Fundamentals
stocks_income_statements 35 columns Quarterly/annual income statements.
history window applies to: filing_date
stocks_balance_sheets 39 columns Quarterly/annual balance sheets.
history window applies to: filing_date
stocks_cash_flow_statements 33 columns Quarterly/annual cash-flow statements.
history window applies to: filing_date
stocks_ratios 24 columns Valuation and financial ratios (P/E, P/B, EV/EBITDA, yields, ROE, market cap) — a CURRENT SNAPSHOT: one row per ticker, replaced whole at each load. The vendor publishes the latest session only, so there is no history here; derive a past ratio from the statements and stocks_daily_aggs.
history window applies to: date
Ownership & insiders
stocks_13f_filings 21 columns Institutional holdings from 13F-HR filings — one row per position per manager per quarter (`filer_cik`, `cusip`, `market_value`, `shares_or_principal_amount`, voting authority). Back to 2000. Join to a quarter with `period`, to the filing with `filing_date`.
stocks_form4 41 columns Insider transactions (SEC Form 4): open-market buys and sells, grants and exercises by officers, directors and 10% owners. `transaction_code` is the action, `transaction_shares`/`transaction_price_per_share` the size. `tickers` is an ARRAY — filter with has(tickers, 'AAPL'). Back to 2003.
stocks_form3 30 columns Initial statements of beneficial ownership (SEC Form 3) — filed when someone first becomes an insider. The starting position that Form 4 transactions then move. `tickers` is an ARRAY.
SEC filings
stocks_sec_edgar_index 8 columns The EDGAR filing index: every filing, its form type and filing date.
history window applies to: filing_date
stocks_10k_sections 8 columns Parsed sections of 10-K filings.
history window applies to: filing_date
stocks_8k_disclosures 10 columns 8-K material-event filings, categorised (`primary_category` … `tertiary_category`) with the supporting text. `tickers` is an ARRAY. See stocks_8k_text for the full filing body.
stocks_8k_text 8 columns 8-K filing text.
history window applies to: filing_date
stocks_13f_text 7 columns Raw 13F filing text. NOT YET POPULATED — the table exists and is empty; use stocks_13f_filings for the parsed positions.
stocks_risk_factors 8 columns Parsed risk-factor sections.
history window applies to: filing_date
stocks_risk_categories 6 columns Reference: risk-factor category labels.
News
stocks_news 13 columns News article metadata (headline, publisher, tickers). Not full article text.
history window applies to: published_utc
Macro & rates
treasury_yields 13 columns US Treasury yield curve by maturity, daily.
history window applies to: date
inflation 8 columns CPI / PCE series.
history window applies to: date
inflation_expectations 9 columns Inflation expectation series.
history window applies to: date
labor_market 6 columns Labor-market series.
history window applies to: date
Reference
stocks_market_holidays 7 columns Exchange calendar: holidays and early closes.
stocks_market_status 8 columns Reference: market status codes.
stocks_exchanges 11 columns Reference: exchange codes and names.
stocks_condition_codes 12 columns Reference: trade/quote condition codes (needed to read the tick tables).
Trade tape field guide
What the columns of stocks_trades mean, and the four things a point-in-time
simulator has to get right. Every statement here was measured on the warehouse itself
(mid-March sessions of every year 2003–2026, plus two full sessions), not copied from a vendor
page. The same facts ride on each column as a note in GET /v1/schema.
Coverage
The consolidated SIP tape (CTA and UTP) for exchange-listed US stocks and ETFs, from
2003-09-10, every trading session since, current to the prior session with a one to
two day ingest lag. Delisted names are present through their last listed session. OTC and pink-sheet
prints are not included, so a post-delisting symbol such as SIVBQ returns nothing.
To read a zero-row result: if stocks_daily_aggs has a bar with volume for that ticker
and session and the tape has no rows, that is a gap — tell us. No bar either means the symbol did
not trade on an exchange that day.
Timestamps
| Period | participant_timestamp | trf_timestamp | sip_timestamp precision |
|---|---|---|---|
| 2003-09-10 → 2005 | epoch placeholder on every row | epoch placeholder | whole seconds on most rows |
| 2006 → 2015-07-24 | epoch placeholder on every row | epoch placeholder | milliseconds |
| 2015-07-27 → | populated (the exchange's own stamp) | populated on off-exchange prints only | nanoseconds from 2015-08-03 |
The SIP stamp is dissemination time: it trails the exchange stamp by a fraction of a millisecond on
exchange prints and by up to seconds on late off-exchange reports. The execution time to use is
therefore participant_timestamp from 2015-07-27 and sip_timestamp before —
one expression covers both:
if(toUnixTimestamp64Nano(participant_timestamp) = 0, sip_timestamp, participant_timestamp) AS executed_at
Venue and tape
exchange is the reporting venue (join stocks_exchanges): 10 NYSE, 12 Nasdaq,
11 NYSE Arca, 1 NYSE American; every off-exchange (TRF) print carries 4, in every year.
tape is the SIP feed that carried the print — 1 for CTA, 3 for UTP — and CTA covers
both Tape A and Tape B, so SPY, IWM and GLD are stored as 1. Use exchange, not
tape, to find the listing venue.
Opening and closing auctions
The condition codes are one set for the whole history (stocks_condition_codes carries the
CTA/UTP letter for each and an update_rules column saying whether a code counts toward
open, close, high, low and volume). What varies by year is whether the SIP sent the flag.
| Code | Meaning | NYSE-listed (exchange 10) | Nasdaq-listed (exchange 12) |
|---|---|---|---|
| 17 | opening auction print | from August 2009; [17, 41] from 2020 | [17, 9] from at least 2008; 41 added 2020 |
| 8 | closing auction print | from June 2009; [8, 41] from 2020 | [8, 9] from at least 2008; 41 added 2020 |
| 16 | official open message | from 2021 | from 2005 |
| 15 | official close message | from 2017, re-sent 3–4× a session since 2020 | from 2003 |
Two rules follow. Codes 15 and 16 are messages, not executions — they repeat the auction print's price and size — so exclude them from executions and from volume. And before mid-2009 the NYSE auction prints carry no flag at all: on exchange 10 they are the first print at or after 09:30 and the largest print after 16:00, both unflagged. Through 2020 the NYSE closing print was regularly reported one to two and a half minutes after the bell, and from 2015-07-27 the exchange's own stamp confirms that is when the auction completed, not a dissemination delay.
-- The closing auction print, NYSE-listed name, one session (the 9 and 41 flags ride along, so test with has()).
SELECT toString(toDateTime64(sip_timestamp, 3, 'America/New_York')) AS printed_et,
toString(toDateTime64(participant_timestamp, 3, 'America/New_York')) AS exchange_stamp_et,
price, size, exchange, conditions
FROM global_markets.stocks_trades
WHERE ticker = 'IBM' AND exchange = 10
AND sip_timestamp >= toDateTime('2023-03-15 15:55:00', 'America/New_York')
AND sip_timestamp < toDateTime('2023-03-15 16:15:00', 'America/New_York')
AND has(conditions, 8) AND correction = 0
ORDER BY sip_timestamp
Corrections and cancellations
| correction | Meaning | In the final tape? |
|---|---|---|
| 0 | regular print, never amended | yes |
| 1 → 12 | original later corrected → the correction record (the corrected fields) | keep the 12 |
| 8 → 10 | original later cancelled → the cancel record | drop both |
| 7 → 11 | original later marked erroneous → the error record (last seen 2014) | drop both |
A 10, 11 or 12 record carries the original print's sequence_number, exchange and
participant stamp, with a later sip_timestamp: the moment the amendment was
disseminated. Sequence numbers restart each session, so the key is (ticker, session,
sequence_number). Amendments are rare — roughly 0.001% to 0.03% of prints on a session — and the
counts pair exactly. For a point-in-time replay keep the originals and apply the 10 or 12 at its
own SIP time.
-- The final tape for one symbol-session: drop amended and cancelled originals, keep corrections, drop the open/close messages.
SELECT sip_timestamp, participant_timestamp, sequence_number, price, size, exchange, conditions, correction
FROM global_markets.stocks_trades
WHERE ticker = 'AAPL'
AND sip_timestamp >= toDateTime('2023-03-15 04:00:00', 'America/New_York')
AND sip_timestamp < toDateTime('2023-03-15 20:00:00', 'America/New_York')
AND correction IN (0, 12)
AND NOT hasAny(conditions, [15, 16])
ORDER BY sip_timestamp, sequence_number
Symbol identity
ticker is the symbol as it traded on that day, never renamed after the fact:
FB prints through 2022-06-08 and META from 2022-06-09. Symbols are reused
across issuers with nothing on the tape to tell them apart — TWTR was Tweeter Home
Entertainment until 2007-06-20 and Twitter from 2013-11-07; GM and DELL
both have gaps between two different companies. There is no permanent security identifier on the
tape (a CIK exists only on the SEC-derived tables). For a multi-year study, build a
(ticker, first date, last date, security id) map from an external master and join on ticker and
session.
Data caveats
Known defects in the underlying data. We publish them rather than let you discover them: a
silently wrong number is worse than a missing one. They are also attached to the affected
columns in GET /v1/schema.
options_greeks.iv_converged
Filter iv_converged = 1: rows where the IV solve did not converge carry NULL greeks (deep ITM/OTM or stale closes).
options_greeks.implied_volatility
Solved from the EOD option close by bisection. Screen implied_volatility > 0.02 to drop degenerate fits.
options_trades.expiration_date
Unreliable. Parse the expiry from the option ticker instead.
stocks_13f_filings.period
334 rows carry an out-of-range period (as filed — they range to 1987 and to 2149). Tiny against 160m rows, but enough to poison the obvious idiom: max(period) returns 2149-06-06 and a 'latest quarter' query then matches one row. Bound it — max(period) WHERE period <= today().
stocks_balance_sheets.filing_date
PARTLY UNRELIABLE (~43% of rows filed >200d after period_end). Prefer period_end.
stocks_cash_flow_statements.filing_date
UNRELIABLE (same vendor defect as stocks_income_statements). Use period_end.
stocks_form4.transaction_price_per_share
Carries a small tail of mis-parsed values as filed — ~0.3% of open-market purchases price above $100,000/share, and in some rows the SHARE COUNT is repeated into this field. Harmless to ignore until you ORDER BY value, which puts them first. Bound it (e.g. BETWEEN 1 AND 10000) for any ranking. transaction_value is derived from this column and inherits the defect.
stocks_income_statements.filing_date
UNRELIABLE. Vendor defect: ~89% of quarterly rows carry a filing_date more than 200 days after period_end (older fiscal years are stamped with recent years' dates). Use period_end as the time key.
stocks_short_volume.date
This table can contain duplicate rows for a ticker/date. Dedupe with GROUP BY + max().
Data usage
Can I keep the data locally, and what happens if I cancel?
We do not issue individual written confirmations regarding market-data rights, storage, retention, or the specific requirements of exchanges, SIPs, and other third-party providers. What a Strasmore subscription permits is governed by our Terms of Service, with Section 4.4 addressing third-party data specifically. Review them against your intended use; if they do not provide the certainty your project requires, we recommend consulting market-data counsel. Read the Terms.