Strasmore API Get an API key

Strasmore API · options

A clean NBBO per contract, every N seconds.

One HTTP call returns one row per option contract per interval bucket, from 09:30 to 16:00 ET: bid, ask, both sizes, the mid, and the timestamp of the quote that was standing. No SQL, no window functions, no forward-fill of your own.

The raw quote tape (cache_options_quotes, roughly 7 trillion rows) carries no underlying, expiry, type or strike column — all four are encoded inside the OCC ticker — and no column telling you which quote was last in a given second. So the straightforward version of this question is contract discovery, plus per-interval sampling, plus quote cleaning, before you see a single usable row. These two endpoints do all three server-side.

The first look needs no key and no signup. The same two endpoints with a key read at your own plan's window — including the full tape back to 2022 on Pro.

Quickstart — no key

Two calls: find out what was quoted, then ask for the snapshots. The dates below are filled in for you: 2026-10-30 is the most recent session the no-key window reaches.

1. What contracts were quoted?

curl -s "https://ai.strasmore.com/v1/demo/options/contracts?underlying=SPY&date=2026-10-30&expiry_from=2026-11-20&expiry_to=2026-11-20&type=call"

Each row is a contract with its expiry, type, strike, how many quotes it printed in the session, and the first and last of them — enough to pick the ones worth sampling.

2. One clean quote per minute, for one contract:

curl -s "https://ai.strasmore.com/v1/demo/options/quote-snapshots?contract=O:SPY261120C00500000&date=2026-10-30&interval=60"

A row looks like this:

{
  "contract": "O:SPY261120C00500000",
  "bucket_end": "2026-10-30T09:31:00-0400",
  "bid": 276.97,
  "ask": 280.78,
  "bid_size": 1,
  "ask_size": 1,
  "mid": 278.875,
  "quote_time": "2026-10-30T09:30:50-0400"
}

Add &format=csv for the same columns as CSV, with the column list as the header row.

Ask your assistant

Paste this into ChatGPT, Claude or any assistant that can make HTTP requests. It names the endpoint and its parameters, so the assistant does not have to guess:

Strasmore has a free, no-key endpoint that returns a clean options NBBO
per contract per interval:

  GET https://ai.strasmore.com/v1/demo/options/quote-snapshots
      ?underlying=SPY&date=YYYY-MM-DD&interval=60
      (or &contract=O:SPY261016C00500000 for a single contract)

Optional: expiry_from, expiry_to, type=call|put, strike_min, strike_max,
format=csv, cursor. Each row is contract, bucket_end (ET), bid, ask,
bid_size, ask_size, mid, quote_time.

List what was quoted on a date first:
  GET https://ai.strasmore.com/v1/demo/options/contracts?underlying=SPY&date=YYYY-MM-DD

Without a key: send one contract, or an expiry range of at most
31 days, and pick a date inside the last 5 days.
Use it to answer my question, and show me the numbers you got.

GET /options/contracts

Every contract with at least one quote in the regular session on date.

Returns contract, expiry, type, strike, quotes, first_quote, last_quote.

GET /options/quote-snapshots

One row per contract per interval bucket from 09:30 to 16:00 ET. Each row carries the last valid quote at or before the end of its bucket.

A bucket with no update is not left out. An NBBO is a step function — the quote stands until something replaces it — so a quiet minute repeats the quote that was still standing, and quote_time tells you when it was set. Buckets before a contract's first quote of the session carry no row at all.

Parameters

ParameterMeaning
underlyingUS equity or ETF symbol, e.g. SPY. Required unless contract is given.
dateTrading date, YYYY-MM-DD. Required.
intervalBucket length in seconds, 1–3600. Default 60. Snapshots only.
contractOne OCC option ticker as the tape stores it, e.g. O:SPY261120C00500000 — O: + underlying + YYMMDD + C/P + strike×1000 padded to 8 digits. Replaces every other filter.
expiry_from, expiry_toExpiry range, inclusive, YYYY-MM-DD.
typecall or put.
strike_min, strike_maxStrike range, inclusive.
cursorThe next_cursor of the previous page.
formatcsv for CSV; JSON otherwise.

Columns

ColumnMeaning
contractThe OCC option ticker.
bucket_endEnd of the interval bucket, ISO 8601 in ET with the UTC offset. The last bucket ends at 16:00:00 even when the interval does not divide the session evenly.
bid, askThe standing quote at bucket_end.
bid_size, ask_sizeSizes on that quote.
mid(bid + ask) / 2, to 4 decimals.
quote_timeWhen that quote was set — nanosecond SIP timestamp, ET. Earlier than bucket_end whenever the quote was carried forward.

What counts as a quote

A quote is used only when both sides quoted (bid_size > 0 and ask_size > 0), neither price negative, and ask_price >= bid_price. Everything else — a one-sided quote, a crossed quote, a negative price — is skipped, and the previous valid quote stands. The rule is stated back to you in every response under request.valid_quote, so a result can always be reproduced.

Paging

Rows come back ordered by contract, then by bucket. When more exist than the row cap returns, truncated is true and next_cursor is the position to continue from — pass it as &cursor= and you get the rows after it, with nothing repeated and nothing skipped.

A truncated page also carries narrow: the filters that would make the answer fit in one call. Paging a whole chain one page at a time works, but asking for one expiry or one strike band is usually what you actually wanted.

Python

import csv, io, urllib.parse, urllib.request

BASE = "https://ai.strasmore.com/v1/demo/options"          # no key needed


def get(path, **params):
    url = f"{BASE}/{path}?" + urllib.parse.urlencode(params)
    with urllib.request.urlopen(url, timeout=60) as r:
        return r.read(), r.headers.get("content-type", "")


# 1. what was quoted
import json
body, _ = get("contracts", underlying="SPY", date="2026-10-30",
              expiry_from="2026-11-20", expiry_to="2026-11-20", type="call")
contracts = json.loads(body)["rows"]
print(len(contracts), "contracts;", contracts[0]["contract"])

# 2. one clean quote a minute, as CSV, following the cursor to the end
rows, cursor = [], None
while True:
    params = dict(contract="O:SPY261120C00500000", date="2026-10-30", interval=60,
                  format="csv")
    if cursor:
        params["cursor"] = cursor
    body, ctype = get("quote-snapshots", **params)
    page = list(csv.DictReader(io.StringIO(body.decode())))
    rows += page
    # The JSON form carries next_cursor; ask for it when you need to page CSV.
    meta = json.loads(get("quote-snapshots", **{**params, "format": "json"})[0])
    cursor = meta.get("next_cursor")
    if not cursor:
        break
print(len(rows), "snapshots;", rows[0])

Limits & cost

Without a key:

With a key, the row cap is 20,000 and the window is your plan's. The full options NBBO tape from 2022 is one of the datasets Pro includes.

One honest note on breadth. The cost of these calls is the tape scan, and it scales with how many contracts you ask for, not how many rows come back. A single contract answers in a fraction of a second and one expiry in a few seconds; a month of expiries on the most heavily quoted underlyings is tens of seconds and will hit the no-key timeout. When that happens the answer names the narrowing that works rather than just reporting a timeout.

The SQL underneath

Every response carries the exact statement it came from, in sql. It runs unchanged on POST https://api.strasmore.com/v1/query and on the no-key SQL endpoint, so you can take it, change it, and keep going. Nothing here is a second path to the warehouse: these endpoints build that statement and hand it to the same gate, the same executor and the same limits every other query goes through.