Skip to main content

Official Python client for the CuteMarkets options market-data API

Project description

cutemarkets

The official Python client for the CuteMarkets options market-data API.

cutemarkets wraps every documented endpoint of the CuteMarkets v1 REST API in a namespaced, typed, Pythonic interface. Sync and async clients share the same method surface, response models are pydantic v2 classes that preserve the raw payload on .raw, every list endpoint ships with both one-page and auto-paginating variants, and every error path maps to a specific exception class so you can handle plan gating, rate limiting, and missing resources cleanly.


Table of contents

  1. Features
  2. Installation
  3. Quick start
  4. Authentication
  5. Client options
  6. Resource reference
  7. Models
  8. Pagination
  9. Filters and range queries
  10. Dates, enums, booleans
  11. Errors
  12. Rate limits
  13. Async usage
  14. Recipes
  15. Testing your integration
  16. Versioning and compatibility
  17. Development
  18. License

Features

  • Sync (CuteMarkets) and async (AsyncCuteMarkets) clients with identical surfaces.
  • Namespaced methods that mirror the docs: client.options.chain(...), client.options.aggs.range(...), client.options.indicators.sma(...), client.tickers.search(...), ...
  • Typed pydantic v2 models for every response, with .raw preserving the original JSON so new server fields never block you.
  • Short-name preservation on abbreviated wire payloads (LastTrade.T/p/s/..., Aggregate.o/h/l/c/v/vw/t/n) plus readable property aliases (.ticker, .price, .open, .close, ...).
  • One-shot list(...) returns a Page[T] (results, next_url, request_id, rate_limit, .next()). Auto-paginating iter_list(...) / iter_*(...) generators walk every page for you.
  • Automatic date / datetime / bool / Enum coercion and underscore-to-dot rewriting for range filters (strike_price_gte=100strike_price.gte=100).
  • Typed exception hierarchy (AuthenticationError, ForbiddenError, LookbackExceededError, NotFoundError, RateLimitError, ...) selected from the error.code envelope field.
  • Rate-limit header introspection via page.rate_limit and client.last_rate_limit (coming soon).
  • Opt-in exponential-backoff retries on 429, 5xx, and transient network errors.
  • Custom httpx.Client / httpx.AsyncClient / httpx.BaseTransport injection for testing.

Installation

pip install cutemarkets

Requires Python 3.9 or newer. The only runtime dependencies are httpx and pydantic.

Development extras:

pip install "cutemarkets[dev]"

Quick start

from cutemarkets import CuteMarkets

client = CuteMarkets(api_key="cm_...")

chain = client.options.chain("NFLX", limit=5)
for contract in chain:
    print(contract.details.ticker, contract.break_even_price, contract.greeks.delta if contract.greeks else None)

print("request id:", chain.request_id)
print("remaining this minute:", chain.rate_limit.remaining_minute)

Async equivalent:

import asyncio
from cutemarkets import AsyncCuteMarkets

async def main():
    async with AsyncCuteMarkets(api_key="cm_...") as client:
        chain = await client.options.chain("NFLX", limit=5)
        for contract in chain:
            print(contract.details.ticker)

asyncio.run(main())

Authentication

API keys are prefixed cm_... and are passed as a Bearer token on the Authorization header.

Three ways to provide your key, evaluated in this order:

  1. Constructor argument:
    client = CuteMarkets(api_key="cm_...")
    
  2. CUTEMARKETS_API_KEY environment variable:
    export CUTEMARKETS_API_KEY=cm_xxxxxxxxxxxxxxxx
    
    client = CuteMarkets()  # auto-picks up the env var
    
  3. Runtime setter:
    client.set_api_key("cm_...")
    

Get a free key at cutemarkets.com/signup. Every request's request_id is echoed back in responses and errors, so include it in support conversations.

Unauthenticated endpoint

client.status() hits /v1/status/ and does not require an API key. It works even before you configure credentials, so it's handy for readiness probes.


Client options

CuteMarkets(
    api_key: str | None = None,
    *,
    base_url: str = "https://api.cutemarkets.com",
    timeout: float = 30.0,
    max_retries: int = 2,
    headers: dict[str, str] | None = None,
    transport: httpx.BaseTransport | None = None,
    http_client: httpx.Client | None = None,
)
Argument Purpose
api_key Override or supply the API key. Overrides CUTEMARKETS_API_KEY.
base_url Point the client at a different host (e.g. a staging or proxy URL).
timeout Per-request timeout in seconds (applies to connect + read).
max_retries Retry attempts on 429, 5xx, and transient network errors. 0 disables retries.
headers Extra headers merged onto every request (useful for tracing or proxy auth).
transport Pass an httpx.MockTransport (or any httpx.BaseTransport) for tests.
http_client Bring your own httpx.Client when you want custom transport, event hooks, or connection pooling.

AsyncCuteMarkets accepts the same kwargs, with transport: httpx.AsyncBaseTransport and http_client: httpx.AsyncClient.

Both classes support the context-manager protocol:

with CuteMarkets(api_key="cm_...") as client:
    ...

async with AsyncCuteMarkets(api_key="cm_...") as client:
    ...

Resource reference

Every method below links to the matching page under docs/. Every response is a pydantic model — dotted attribute access, .model_dump() for a dict, .raw for the untouched payload.

client.status()

Poll the public health endpoint. No API key required.

status = client.status()
status.status            # "ok" or "degraded"
status.is_ok             # bool
status.services["api"].status
status.services["database"].latency_ms

client.tickers.search(query=..., limit=...)

docs/ticker-search.md

for row in client.tickers.search(query="NFLX", limit=8):
    print(row.symbol, row.name)

client.tickers.expirations(ticker)

docs/expirations.md

exp = client.tickers.expirations("NFLX")
exp.results  # ["2026-04-02", "2026-04-10", ...]

client.options.chain(ticker, **filters)

docs/option-chain.md

Returns a Page[ContractSnapshot]. Accepts strike_price, expiration_date, contract_type, the range filters (strike_price_gte, expiration_date_lt, ...), sort, order, limit, and page.

chain = client.options.chain(
    "NFLX",
    contract_type="call",
    strike_price_gte=90,
    strike_price_lte=110,
    sort="strike_price",
    order="asc",
    limit=50,
)
for c in chain:
    print(c.details.ticker, c.break_even_price, c.implied_volatility)

client.options.iter_chain("NFLX", ...) auto-follows next_url.

client.options.snapshot(underlying, option_contract)

docs/option-contract-snapshot.md

snap = client.options.snapshot("NFLX", "O:NFLX260410C00060000")
snap.greeks.delta
snap.underlying_asset.price

client.options.contracts.list(**filters) / .get(options_ticker, as_of=None) / .iter_list(**filters)

docs/contracts.md

# Paged list
page = client.options.contracts.list(
    underlying_ticker="NFLX",
    expiration_date_gte="2026-04-01",
    limit=1000,
)

# Walk every contract across pages
for c in client.options.contracts.iter_list(underlying_ticker="NFLX"):
    print(c.ticker)

# Detail for one contract, optionally as-of a historical date
from datetime import date
detail = client.options.contracts.get("O:NFLX260402C00075000", as_of=date(2026, 1, 15))

client.options.trades.list(ticker, **filters) / .last(ticker) / .iter_list(...)

docs/trades.md

# Historical trades
page = client.options.trades.list(
    "O:NFLX260402C00075000",
    timestamp_gte="2026-03-01",
    timestamp_lte="2026-03-31",
    sort="timestamp",
    order="desc",
    limit=1000,
)

# Compact "last trade" shape (abbreviated keys + readable aliases)
last = client.options.trades.last("O:NFLX260410C00060000")
last.price       # property alias for last.p
last.size        # property alias for last.s
last.ticker      # property alias for last.T
last.raw         # the untouched {"T": ..., "p": ..., "s": ..., ...}

client.options.quotes.list(ticker, **filters) / .iter_list(...) — Expert Plan only

docs/quotes.md

Non-Expert keys receive HTTP 403, surfaced as ForbiddenError:

from cutemarkets.errors import ForbiddenError

try:
    quotes = client.options.quotes.list("O:NFLX260402C00075000", limit=500)
except ForbiddenError as exc:
    print("upgrade required:", exc.message)

client.options.aggs.range(ticker, multiplier, timespan, from_date, to_date, **opts)

docs/aggregates.md

from datetime import date
page = client.options.aggs.range(
    "O:NFLX260402C00075000",
    multiplier=1,
    timespan="day",
    from_date=date(2026, 1, 1),
    to_date=date(2026, 4, 1),
    adjusted=True,
    sort="desc",
    limit=1000,
)
for bar in page:
    print(bar.timestamp, bar.open, bar.high, bar.low, bar.close, bar.volume)

client.options.aggs.iter_range(...) auto-follows next_url.

client.options.aggs.previous(ticker, adjusted=None)

prev = client.options.aggs.previous("O:NFLX260402C00075000")
print(prev.close, prev.vwap, prev.trade_count)

client.options.aggs.open_close(ticker, date, adjusted=None) / client.options.open_close(...)

Note: grouped under aggs for discoverability, but the underlying route is /v1/options/open-close/.... The payload is flat (no results envelope).

from datetime import date
oc = client.options.aggs.open_close("O:NFLX260402C00075000", date(2026, 3, 10))
oc.open, oc.close, oc.high, oc.low, oc.volume
oc.pre_market            # property alias for oc.preMarket
oc.after_hours           # property alias for oc.afterHours
oc.from_date             # property alias for the reserved-keyword "from"

client.options.indicators.sma(ticker, ...) / .ema(...) / .macd(...) / .rsi(...)

docs/indicators-sma.md, docs/indicators-ema.md, docs/indicators-macd.md, docs/indicators-rsi.md

sma = client.options.indicators.sma(
    "O:NFLX260402C00075000",
    timespan="day",
    window=20,
    series_type="close",
    limit=100,
)
for point in sma.values:
    print(point.timestamp, point.value)

macd = client.options.indicators.macd(
    "O:NFLX260402C00075000",
    timespan="day",
    short_window=12,
    long_window=26,
    signal_window=9,
)
for point in macd.values:
    print(point.timestamp, point.value, point.signal, point.histogram)

Pass expand_underlying=True to populate result.underlying with the aggregate bars used in the calculation, plus an absolute URL to the same contract's aggregates range.


Models

Every response model:

  • Inherits from cutemarkets.CuteBase (a pydantic.BaseModel with extra="allow").
  • Preserves the untouched JSON payload on .raw, so new or undocumented fields are always reachable:
    lt = client.options.trades.last("O:NFLX260410C00060000")
    lt.raw["future_field_we_didnt_know_about"]
    
  • Supports .model_dump(by_alias=True) / .model_dump_json() for round-tripping.
  • Keeps abbreviated wire keys as the primary field names (matching the docs) and exposes readable property aliases:
    • LastTrade: T/p/s/t/x/c/y/f/r/i/q/e/z/dsticker/price/size/sip_timestamp/exchange/conditions/participant_timestamp/trf_timestamp/trf_id/trade_id/sequence_number/correction/tape/decimal_size.
    • Aggregate: o/h/l/c/v/vw/t/nopen/high/low/close/volume/vwap/timestamp/trade_count.
  • OpenClose is a flat envelope (no results key): the top-level payload is the model. from is mapped to from_date because from is a Python keyword.

Pagination

Page[T] holds one server page plus the metadata needed to fetch more:

page = client.options.contracts.list(underlying_ticker="NFLX", limit=1000)

page.results       # list[Contract]
page.next_url      # full URL for the next page, or None
page.request_id    # server-assigned request id
page.rate_limit    # RateLimitInfo parsed from headers
page.has_next      # bool
next_page = page.next()   # refetches next_url verbatim with the same Authorization

# Walk every item across every page:
for contract in page.iter_all():
    ...

Every list endpoint also ships an iter_<name>(...) generator that does the pagination for you from the first request:

for contract in client.options.contracts.iter_list(underlying_ticker="NFLX"):
    ...

The client follows next_url verbatim, with the same Authorization header. Don't reconstruct the URL yourself.


Filters and range queries

Range filters in the CuteMarkets API use a <field>.<op> naming convention (strike_price.gte=100). Python doesn't allow dots in keyword arguments, so this client accepts an underscore spelling and rewrites it server-side:

You write Sent on the wire
strike_price_gte=100 strike_price.gte=100
strike_price_lt=500 strike_price.lt=500
expiration_date_gte="2026-04-01" expiration_date.gte=2026-04-01
timestamp_lte=1770872400000 timestamp.lte=1770872400000

You can combine them freely:

client.options.chain(
    "NFLX",
    contract_type="call",
    strike_price_gte=90,
    strike_price_lte=110,
    expiration_date_gte="2026-04-01",
    expiration_date_lt="2026-07-01",
    sort="strike_price",
    order="asc",
    limit=100,
)

Any kwarg whose name does not end in _gte / _gt / _lte / _lt is passed through verbatim. That way future API filters work without a client upgrade.


Dates, enums, booleans

Values are auto-coerced to what the API expects:

Python value Wire form
datetime.date(2026, 1, 15) "2026-01-15"
datetime.datetime(2026, 1, 15, 9, 30) "2026-01-15"
True / False "true" / "false"
MyEnum.VALUE (subclass of enum.Enum) .value then coerced
list[int] / tuple[str, ...] emitted as repeated key=value&key=value
None dropped — useful for defaulting to server behavior

Millisecond / nanosecond timestamps should be passed as int (or a decimal string); the client does not guess the unit. Timestamp filters (timestamp, timestamp_gte, ...) accept either a date or an int/string Unix timestamp depending on the endpoint — see the doc page linked in each resource section.


Errors

All exceptions inherit from cutemarkets.CuteMarketsError.

CuteMarketsError
├── ConfigurationError        # missing API key, bad settings
├── TransportError            # network / timeout / connection error
└── APIError                  # any non-2xx response from the API
    ├── BadRequestError       # 400 bad_request
    │   └── InvalidPageTokenError
    ├── AuthenticationError   # 401 unauthorized
    ├── ForbiddenError        # 403 forbidden
    │   └── LookbackExceededError
    ├── NotFoundError         # 404 not_found
    └── RateLimitError        # 429 rate_limit_exceeded

The specific subclass is chosen by HTTP status and the error.code field from the response envelope:

HTTP error.code Exception
400 bad_request BadRequestError
400 invalid_page_token InvalidPageTokenError
401 unauthorized AuthenticationError
403 forbidden ForbiddenError
403 lookback_exceeded LookbackExceededError
404 not_found NotFoundError
429 rate_limit_exceeded RateLimitError

Every APIError exposes:

  • status_code (HTTP status)
  • code (machine-readable code from the envelope)
  • message (human-readable message)
  • request_id (for support)
  • response (the decoded JSON body, for diagnostics)
  • rate_limit (RateLimitInfo parsed from headers)
from cutemarkets.errors import LookbackExceededError, RateLimitError, ForbiddenError

try:
    client.options.contracts.list(underlying_ticker="SPY", as_of="2015-01-01")
except LookbackExceededError as exc:
    print(f"{exc.code}: {exc.message} (request_id={exc.request_id})")

try:
    client.options.quotes.list("O:SPY260402C00500000")
except ForbiddenError as exc:
    print(f"Upgrade required: {exc.message}")

Rate limits

Free keys are capped at 10 requests/minute; Developer and Expert plans are unlimited. Each response includes rate-limit headers that this client parses into a RateLimitInfo object:

page = client.options.chain("NFLX", limit=5)
page.rate_limit.plan                # "Free" | "Developer" | "Expert"
page.rate_limit.limit_minute        # "10" or "unlimited"
page.rate_limit.remaining_minute    # int
page.rate_limit.limit_day
page.rate_limit.remaining_day

The same object is attached to APIError.rate_limit on failures, so you can inspect remaining quota after a 429.

When max_retries is non-zero the client retries 429 responses (as well as 5xx and transient network errors) with exponential backoff — but note that retries share the same plan quota, so aggressive retries on Free keys can compound 429s. For Free keys you usually want max_retries=0 and your own backoff.


Async usage

AsyncCuteMarkets mirrors the sync surface; methods that perform I/O are async def and iterators are async:

import asyncio
from cutemarkets import AsyncCuteMarkets

async def main():
    async with AsyncCuteMarkets(api_key="cm_...") as client:
        status = await client.status()
        chain = await client.options.chain("NFLX", limit=5)
        async for contract in client.options.iter_chain("NFLX", limit=5):
            print(contract.details.ticker)

        last = await client.options.trades.last("O:NFLX260410C00060000")
        print(last.price)

asyncio.run(main())

Async pagination:

page = await client.options.contracts.list(underlying_ticker="NFLX", limit=1000)
async for contract in page.iter_all():
    ...

Don't share a single AsyncCuteMarkets across event loops.


Recipes

Fetch the full option chain across pages

calls = []
for contract in client.options.iter_chain("NFLX", contract_type="call", limit=100):
    calls.append(contract)

Build a daily OHLC series and compare to the previous day

from datetime import date, timedelta

ticker = "O:NFLX260402C00075000"
today = date.today()

bars = list(
    client.options.aggs.iter_range(
        ticker, 1, "day", today - timedelta(days=365), today, sort="asc"
    )
)
prev = client.options.aggs.previous(ticker)
print(f"last daily close: {bars[-1].close}, previous session close: {prev.close}")

Pull the last trade for every contract in a chain

for contract in client.options.iter_chain("NFLX", contract_type="call", limit=100):
    last = client.options.trades.last(contract.details.ticker)
    print(contract.details.strike_price, last.price)

Stream tick-level trades for a day

for trade in client.options.trades.iter_list(
    "O:NFLX260402C00075000",
    timestamp_gte="2026-03-10",
    timestamp_lt="2026-03-11",
    sort="timestamp",
    order="asc",
    limit=10000,
):
    ...

Detect an SMA/EMA crossover

sma = client.options.indicators.sma(ticker, timespan="day", window=50, limit=200)
ema = client.options.indicators.ema(ticker, timespan="day", window=20, limit=200)

sma_by_ts = {p.timestamp: p.value for p in sma.values}
for p in ema.values:
    s = sma_by_ts.get(p.timestamp)
    if s is not None and p.value is not None:
        print(p.timestamp, "above" if p.value > s else "below")

Resolve an expiration + strike to a contract ticker

page = client.options.chain(
    "NFLX",
    expiration_date="2026-04-10",
    strike_price=60,
    contract_type="call",
    limit=1,
)
if page.results:
    print(page.results[0].details.ticker)

Testing your integration

Every network call goes through httpx, so you can swap in an httpx.MockTransport for unit tests:

import httpx
from cutemarkets import CuteMarkets

def handler(request: httpx.Request) -> httpx.Response:
    if request.url.path == "/v1/status/":
        return httpx.Response(200, json={"status": "ok", "request_id": "cm_test", "services": {}})
    return httpx.Response(404, json={"status": "ERROR", "error": {"code": "not_found", "message": "x"}})

client = CuteMarkets(api_key="cm_test", transport=httpx.MockTransport(handler))
assert client.status().is_ok

The test suite in this repo uses the same pattern via a make_client fixture — see tests/conftest.py for a worked example.


Versioning and compatibility

  • SemVer: minor versions may add new resources and fields; breaking changes bump the major.
  • Supports Python 3.9, 3.10, 3.11, 3.12, 3.13.
  • Requires pydantic>=2.6 (pydantic v1 is not supported).
  • Requires httpx>=0.27.

Development

git clone https://github.com/cutemarkets/cutemarkets-python
cd cutemarkets-python
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

pytest
ruff check src tests
mypy src

Running the live smoke test

cp .env.example .env
# fill in CUTEMARKETS_API_KEY=cm_...
python examples/smoke_test.py

The smoke test runs one minimal call against every resource group against https://api.cutemarkets.com, spaced out to stay within the Free plan's 10 req/min limit. It tolerates ForbiddenError on quotes (Expert Plan only) so Free / Developer keys can still use it.


License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cutemarkets_python-0.3.1.tar.gz (28.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cutemarkets_python-0.3.1-py3-none-any.whl (40.2 kB view details)

Uploaded Python 3

File details

Details for the file cutemarkets_python-0.3.1.tar.gz.

File metadata

  • Download URL: cutemarkets_python-0.3.1.tar.gz
  • Upload date:
  • Size: 28.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cutemarkets_python-0.3.1.tar.gz
Algorithm Hash digest
SHA256 aca8bdee9e081e3d9656727358b81cd2ecc7c774ed7db0ac35a3c6de6eb9576d
MD5 964e808b97de4109407f0abfbb9bf70b
BLAKE2b-256 b5bab486c1e08a11a0c47becd8155bf74afb8742a4a289a832787f6b10d55551

See more details on using hashes here.

Provenance

The following attestation bundles were made for cutemarkets_python-0.3.1.tar.gz:

Publisher: release.yml on cutemarkets/cutemarkets-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cutemarkets_python-0.3.1-py3-none-any.whl.

File metadata

File hashes

Hashes for cutemarkets_python-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 df3a7e7dee7f1db3d145334ee8c16354828469ef769788453bac1e5eb385c8a7
MD5 21836462d87eed0b50374644d5e26d1b
BLAKE2b-256 e70faf4e69511a5f73ceae34c684839c3c8d48b98273d1d0aa3252ae283e1d52

See more details on using hashes here.

Provenance

The following attestation bundles were made for cutemarkets_python-0.3.1-py3-none-any.whl:

Publisher: release.yml on cutemarkets/cutemarkets-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page