API
One API. 43 POST routes.
TanukiTrade PRO exposes derived options analytics over HTTP and over MCP. Every market route is POST /api/v1/… with a flat JSON body. The same body is the MCP tool argument. There is one version.
New to the API? Start with the five-minute quickstart. This page lists every route.
P1, pT, HVL, cT, C1), net exposure totals, and full-chain per-strike slices in cumulative or standalone scope.Explore Gamma Every route
43 POST /api/v1 routes: 40 market and 3 account. Each row is one MCP tool. The path opens that route. The group name opens its page.
Authorization
- Sign in and open API Access.
- Copy the bearer token from that page. REST and MCP use the same token.
- Store it in a password manager or secret store. Do not leave it in a chat or a committed file.
Send one header on every call:
Authorization: Bearer 12345.YOUR_API_KEYThe token is api_user_id, a dot, then the key. The id is 8 lowercase letters or digits. The key is 16 to 128 letters or digits. The whole token is at most 160 characters. It is not trimmed and it is not lowercased for you. 12345.YOUR_API_KEY is a placeholder, not a working token: it fails the grammar on purpose.
API and MCP access are included with an active TanukiTrade PRO subscription. The chart login is not the bearer token. Copy the token from the signed-in API Access page. REST and MCP share that key and the same per-user rate limit. Keep it in an environment variable, secret store, or MCP host header field. Do not put it in a URL, query string, log line, or tool argument.
export TANUKI_BASE_URL="https://api.tanukitrade.com" export TANUKI_TOKEN="12345.YOUR_API_KEY"
The call
Start with discovery. This verifies the URL, token, JSON body, and response envelope before you add symbol-specific arguments:
curl --compressed \ --request POST \ --url "${TANUKI_BASE_URL}/api/v1/discovery/symbols" \ --header "Authorization: Bearer ${TANUKI_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --data '{}'
- Method is
POST. Another method is 405 andAllow: POST. Content-Type: application/json. The body is one flat object, at most 4 KiB. A nested object, a repeated key, unknown keys, and bad JSON are 422. A larger body is 413.- An integer outside signed 64-bit is 422.
- Send
Accept-Encoding: gzip.curl --compressedinflates the body. - Timestamps are ISO 8601 with an America/New_York offset, to milliseconds. A
Zsuffix does not come back. Dates areYYYY-MM-DD. - A non-integer number is at most 4 decimal places (half-even). A whole number stays an integer. A value that is not finite, or an integer outside the signed 64-bit range, is
null.
Envelope
A success is ok, data, and meta. Lists put rows in data.items. meta.request echoes every argument except cursor. A missing optional argument is null in that echo. Defaults you did not send, such as method: cumulative, are filled in.
| meta field | Meaning |
|---|---|
count | Rows in this page. |
total | Rows that matched the filter, across pages. |
page / page_count | This page, and how many pages total splits into at the route’s fixed page size. |
next_cursor | Opaque string for the next page, or null on the last page. |
generation | Id of the data generation. It changes when the inputs change. |
as_of | Source times that apply to this route. Often empty. |
built_at | When this payload was assembled, ET, with an offset. |
omitted | Symbol strings left out of the rows. Always a list. Empty is []. |
notes | Server text on the 0DTE list when a futures or pause sentence applies. Other routes do not send the key. |
Paging
You do not choose the page size. Pass cursor from the previous meta.next_cursor with the same other arguments. A cursor that does not match the request, or a generation that moved, is 422: Cursor does not match this request or the data changed; start again without cursor.
The benchmark list is the route that actually paged on the captured stack (200 of 261 rows). The live cursor is a long opaque string. The example uses eyJ... because that value is bound to the server domain and is not something to copy out of a doc.
{ "cursor": "eyJ..." }
Null, and a missing key
- A missing earnings move block is
null. The key stays. - A 0DTE session that was not stored (
eod,pre, oropen) isnull. The key stays. meta.omittedis always a list. Empty is[]. When SPY is dropped from relative strength,omittedis["SPY"].- A chain count is
0when nothing is stored. A call/put ratio isnullwhen the put side is0. A share percent isnullwhen the symbol total is0.
Expiries
An alias is 0, 1, w1–w4, M1–M3, or ALL. Letter case folds before the cache key. Weekly aliases keep a lowercase w. Monthly aliases and ALL stay uppercase.
An unknown expiry sent with a symbol is 422 UNKNOWN_EXPIRY. The same unknown expiry sent alone is HTTP 200 and an empty list. The strike header does not include is_monthly. Expiry rows elsewhere do.
Where each question goes
| Question | Page |
|---|---|
| Which tickers exist, and which expiries does this one have? | Discovery |
| Profile, correlation, earnings. | Fundamentals |
| IV rank, skew, term structure. | Volatility |
| 0DTE box and expected-move distance. | Range |
| State, walls, net GEX, strikes. | Gamma |
| Open interest and volume. | Chain |
| Percent change, realized vol, relative strength. | Technical |
| The same readings on a past ET day. | History |
| How many calls did this token make, and from where? | Account |
A default PRO token can call every group in this contract, and MCP tools/list includes them. HTTP 403 ENDPOINT_NOT_ENABLED happens only when the token is limited to an explicit group list and the route is not on it. See Connect.
Account is 3 more POST routes for this token’s own usage, recent clients, and service status. They are not market data, the group filter does not hide them, and they are also MCP tools: get_account_usage, get_account_clients, and get_account_status. The full list is every route.
What this API does not return
- No raw OPRA chain, no per-contract quote, and no tick feed.
spot_priceis returned on every route that lists it infields, and on the 0DTE box snapshots. On a gamma or strike snapshot it is that snapshot’s underlying price, not a live quote. On the expected-move distance rows it is that expiry’s average opex spot. The current price-change row is the price atsnapshot_at. The history price-change row is that day’s close. A 0DTE snapshot’sspot_priceis the peg the box is centred on.widest.spot_priceis the peg of the latest snapshot that arrived, and a history change carries the peg of the snapshot that triggered it. A missing, zero, or negativespot_priceis null. Box edges (em_upper,em_lower,sigma_upper,sigma_lower) are expected-move bounds, not a quote. A state sentence that uses the word “price” says where the underlier sits relative to a level. That sentence is not a price field. A strike row does not repeatspot_priceoris_spot; the header carriesspot_price. On a strike routemeta.as_of.priceis the chain snapshot time, ET with an offset, the same instant assnapshot_at.- No orders and no trade signals.
Next: errors, rate limits, or the Python client.
TanukiTrade API and MCP. Derived options analytics for your code and for agents. Not investment advice. tanukitrade.com