# CryptoStruct HTTP API — buying market data without MCP

> The same flow the MCP server (https://cryptostruct.com/mcp) runs, as plain HTTP calls: search → coverage → quote → checkout → poll → download.
> Machine-readable description: https://cryptostruct.com/openapi.json (OpenAPI 3.1). Site map for agents: https://cryptostruct.com/llms.txt. Terms: https://cryptostruct.com/terms.md · Data license: https://cryptostruct.com/license

Facts that do not change per call: one file = one instrument × one UTC day, **€1 per instrument-day** (prices are tax-inclusive EUR); day bundles (`kind: series_day`) are priced per series, mostly €1 per bundle-day; no account, subscription or API key is needed — a guest checkout only needs an email for the receipt. Card payment ALWAYS happens in the user's browser on a Stripe page: an agent never collects card details. Every endpoint is rate-limited per IP; on a 429 wait for the `Retry-After` seconds.

## 1. Find the instrument

`GET https://cryptostruct.com/api/search?q=<text>&venue=<code|family>&class=<class>&base=<asset>&limit=25`

- `q`: ticker (`BTCUSDT`), pair, base asset, or a numeric instrument id. Venue and class WORDS inside `q` are understood (`binance btcusdt perp`, `BTCUSDT-PERP`, `ETH options`) and echoed in `interpretation`.
- `venue`: a venue code (`binance_swap`) or a family (`binance` = every Binance venue, `kraken`, `okx`, `gate`, `htx`).
- `class`: `spot | perpetual | future | call | put | prediction` — aliases `perp`, `swap`, `futures`, `option` (= call + put) are accepted; an unknown value is a 400 with the vocabulary.
- Response: `{query, filters, total, results[{instrument_id, code, type, exchange{code,name}, state, data{days, first_day, last_day, total_bytes}}], interpretation?, did_you_mean?}`. Zero hits come with `did_you_mean` (nearest venue codes, what to drop). Use `exchange.code` + `code` + `type` to confirm you have the right market before buying — `BTCUSDT` exists on many venues.
- Free: 100 results per call. The `instrument_id` is stable and identical across MCP, shop and analytics pages.

Example: `curl -s 'https://cryptostruct.com/api/search?q=BTCUSDT&venue=binance&class=perp'` → Binance USDT-M perpetual `BTCUSDT` = instrument_id `67824`.

## 2. Which days exist

- `GET https://cryptostruct.com/api/instrument/{id}/coverage` → `{first_day, last_day, days_with_data, coverage_pct, gaps[{from,to,days}], total_bytes}` (one call, use it to pick a window).
- `GET https://cryptostruct.com/api/instrument/{id}/days?limit=100&before=YYYY-MM-DD` → `{days[{date, size}], next_cursor}`, newest first — "last week" = the first 7 rows. `size` is the compressed (zstd) file size in bytes.
- `GET https://cryptostruct.com/api/instrument/{id}/calendar?month=YYYY-MM` → the days of one month.
- Day bundles (prediction series, option chains, futures curves): `GET https://cryptostruct.com/api/bundles` lists every sellable series with `bundle_id`, `series_key`, `price_cents`, coverage; `GET https://cryptostruct.com/api/bundle/{series_key}/days?limit=&before=` lists its days. In quote and checkout a bundle day is `{instrument_id: <bundle_id>, date, kind: "series_day"}`.
- Unknown and hidden ids answer 404 `{error:{code:"NOT_FOUND"}}` alike.

Days are UTC; the newest sellable day is normally yesterday (UTC). Only days that exist can be bought — the quote tells you which ones do not.

## 3. Quote

`POST https://cryptostruct.com/api/quote` with `{"items":[{"instrument_id":67824,"date":"2026-09-30","kind":"tick"}, …]}` (1–365 items, ≤ 20 distinct instruments/bundles, `kind` defaults to `tick`; no other keys).

Response: `{items_requested, items_available, total_cents, total_eur, currency:"EUR", price_eur_per_day, available[{instrument_id,date,kind}], missing[{instrument_id,date,kind,reason}], bundles?, bundle_days?, vat_note, terms_url, checkout{method,url,headers,body_example,then[]}}`. `available` is the checkout-ready item list; `checkout` is the next step spelled out. Keyless, 30 calls per minute per IP.

## 4. Checkout (guest)

`POST https://cryptostruct.com/v1/checkout/session`

- Headers: `Content-Type: application/json`, `Idempotency-Key: <uuid>` (reuse the same key when retrying the same basket — never for a new one), `X-CryptoStruct-Client: http` (marks the order as an HTTP-agent order in our statistics; optional, never affects price or delivery).
- Body: `{"email": "<buyer email>", "items": [<the available[] items from the quote>], "success_url": "https://cryptostruct.com/order/thanks?session_id={CHECKOUT_SESSION_ID}", "cancel_url": "https://cryptostruct.com/shop"}` — return URLs must be on https://cryptostruct.com (anything else is a 400).
- Response `201`: `{order_id, checkout_url, session_id, currency, item_count, subtotal_cents}`. **Open `checkout_url` for the user** (show it, don't fetch it): that is the Stripe page where they pay by card. The order is pending until then.
- Errors: 400 `INVALID_INPUT`, 422 `FILE_NOT_AVAILABLE` (`details.missing` lists the items — drop them and retry), 429 (checkout is limited per IP), 409 `ALREADY_OWNED` (signed-in only).
- Signed-in buyers (Clerk session / OAuth bearer) get credits and owned-file dedupe automatically; that path is the MCP server's job (https://cryptostruct.com/docs/mcp) — plain HTTP agents use the guest flow above.

## 5. Poll until paid

`GET https://cryptostruct.com/v1/checkout/session/{session_id}` → `{status:"pending"}` while the user is on the Stripe page, then `{status:"paid", order_id, download_token, invoice_hosted_url, invoice_pdf_url}`. Poll every 10–15 seconds; a session the user abandons stays pending and simply expires — create a new checkout for a new attempt. 404 = unknown session.

## 6. The order

`GET https://cryptostruct.com/v1/orders/{order_id}` with `Authorization: Bearer <download_token>` → `{order_id, status, paid_at, currency, total_cents, item_count, items[{instrument_id, code, exchange_code, type, date, kind, bytes, price_cents}], invoice_hosted_url, invoice_pdf_url}`. A wrong token and an unknown order both answer 404 (no enumeration). After 30 days a guest order answers 410 `ORDER_EXPIRED` — download within that window, or buy with a signed-in account (never expires). The `download_token` is the only credential: treat it like a password, keep it out of logs and prompts.

## 7. Download

Per tick file: `GET https://cryptostruct.com/api/download/order/{order_id}/{instrument_id}/{date}?t=<download_token>` streams the `.txt.zst` file (zstd-compressed JSON lines; line 1 is the instrument master data; `zstd -d` to inflate). Files are 0.3–2 GB per day for the busiest markets — stream to disk, support `Range` resumes (`curl -C -`). States before the bytes: `&status=1` answers `{state: "ready" | "pending" | "cold"}`; a `cold` day sits in deep archive (days older than ~1 month) — `&restore=1` starts the restore (~12 h), then poll `&status=1`. Derived flat exports of the same day: `&format=trades.csv.gz | bbo.csv.gz | liquidations.csv.gz | trades.parquet | liquidations.parquet` — exports are prepared server-side first: the first request answers `202` + `Retry-After` (`{state:"pending", reason:"converting"}`) while the file is built (minutes for a flagship day), later requests serve the finished file with `Content-Length` and `Range` support; `&format=…&probe=1` reports `ready | pending | empty | failed` without bytes.

Day bundles (`kind: series_day`): the same route with `&kind=series_day` plus `&zip=1` (every contract file of that day as ONE archive — never one request per contract, a day can hold thousands), `&manifest=1` (member list), `&status=1` (aggregated `{ready,pending,cold}`), `&restore=1`, `&zip=1&probe=1` (readiness; poll it until `state:"ready"` — the probe copies the day's files onto our server, and `zip=1` answers `202 pending` until every file is local; `{state:"too_large"}` for days over 4 GB), `&complete=1` (409 instead of a partial archive). Whole histories: `GET https://cryptostruct.com/api/download/order/{order_id}/{bundle_id}/range?kind=series_day&from=YYYY-MM-DD&to=YYYY-MM-DD&t=<token>` streams every owned day of the window as one resumable tar (≤ 100 days per stream; `&status=1` / `&restore=1` / `&manifest=1` companions). The full delivery contract is in https://cryptostruct.com/llms.txt.

## 8. Try before buying

`GET https://cryptostruct.com/api/samples` → curated free full-day files in the exact shop format, each with a direct `href` (`/api/download/sample/{id}/{date}`, no auth, `?format=` supported). Parser, format reference and recipes: https://cryptostruct.com/skills/cryptostruct-market-data/SKILL.md.

## 9. No scripting? Hand the user a link

Buy link (no API call needed): https://cryptostruct.com/shop?i=<instrument_id>&d=<from>..<to> opens the shop with those UTC days of the instrument in the cart; d also takes a comma list or a mix (d=2026-09-01..2026-09-07,2026-09-15). Bundles: https://cryptostruct.com/shop/bundles?bundle=<series_key>&d=…. Only archived days are added (gaps and days still being merged are skipped), at most 366 days of span and 1000 lines per cart; the user pays on the site. Example: https://cryptostruct.com/shop?i=67824&d=2026-09-29..2026-10-05 = seven days of Binance BTCUSDT perpetual ticks. The page lists what landed in the cart.

## Limits and conduct

- Rate limits per IP: search and coverage share the general API bucket; `/api/quote` 30/min; checkout 10/min; order reads 30/min. Back off on 429.
- Never ask the user for card details; never paste a `download_token` into a chat transcript; keep the Stripe URL for the user.
- The MCP server offers the same flow with sign-in, credits, owned-file dedupe and live statistics: https://cryptostruct.com/docs/mcp.
