API
Caching and conditional requests
Polling a snapshot? Send back the ETag from your last response. If nothing changed, the API answers 304 with no body, so you skip downloading and re-parsing the same data.
Why it matters
Most snapshots change on a schedule. The live GEX grid, for example, gets a new capture every 5 minutes, but clients often poll it every few seconds. Without caching, every one of those polls downloads and parses the whole snapshot again, even though it is identical to the last one.
A conditional request fixes that. When the snapshot has not changed, the API sends a tiny 304 Not Modified response instead of the data. You download almost nothing, parse nothing, and know for certain that what you already have is current.
A 304 is still a request: it saves bandwidth and parse time, not round trips. To make fewer requests, poll no faster than the data changes (see the table below), or use Streaming.
How it works
- Snapshot responses carry an
ETagheader: a fingerprint of the response body. The same body always has the same ETag. - Keep the body and its ETag together.
- On your next poll, send that ETag back in an
If-None-Matchheader. - If the snapshot is unchanged, you get
304 Not Modifiedwith an empty body, and you keep using what you have. If it changed, you get a normal200with the new body and a new ETag.
Worked example
The first poll returns the data and its ETag:
curl -sS -i -G "https://api.itmatrixhq.com/v2/gex/SPY/grid" \
-H "Authorization: Bearer $ITM_API_KEY" \
-d "top=20"HTTP/2 200
content-type: application/json
etag: "9b2f0c…"
{"data": {"spot": 764.18, "captured_at": 1790256713559, …}, "meta": {…}}A minute later, send the same request with that ETag in If-None-Match:
curl -sS -i -G "https://api.itmatrixhq.com/v2/gex/SPY/grid" \
-H "Authorization: Bearer $ITM_API_KEY" \
-H 'If-None-Match: "9b2f0c…"' \
-d "top=20"HTTP/2 304
etag: "9b2f0c…"No body: the grid has not changed since your first call. After the next capture lands, the same request gets a 200 with the new grid and a new ETag.
The same loop in code. It keeps the last body and sends its ETag each time:
import os
import time
import httpx
url = "https://api.itmatrixhq.com/v2/gex/SPY/grid"
headers = {"Authorization": f"Bearer {os.environ['ITM_API_KEY']}"}
etag, grid = None, None
while True:
sent = {**headers, **({"If-None-Match": etag} if etag else {})}
response = httpx.get(url, params={"top": 20}, headers=sent, timeout=30)
if response.status_code == 200:
etag, grid = response.headers.get("etag"), response.json()["data"]
print("new capture", grid["captured_at"])
elif response.status_code != 304:
response.raise_for_status()
time.sleep(60)const url = new URL("https://api.itmatrixhq.com/v2/gex/SPY/grid");
url.search = new URLSearchParams({ top: "20" }).toString();
let etag: string | null = null;
let grid: unknown = null;
for (;;) {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.ITM_API_KEY}`,
...(etag ? { "If-None-Match": etag } : {}),
},
});
if (response.status === 200) {
etag = response.headers.get("etag");
grid = (await response.json()).data;
console.log("new capture", grid);
} else if (response.status !== 304) {
throw new Error(`HTTP ${response.status}`);
}
await new Promise((resolve) => setTimeout(resolve, 60_000));
}Only send If-None-Match when you kept the body that ETag belongs to. A 304 carries no data, so without the saved body there is nothing to show.
Which reads carry an ETag
- GEX grid and GEX reference books
- GEX history, for past sessions only (today's session has no ETag)
- Option chain and expirations
- Quotes, and the symbol registry: list, lookup, one symbol and spot
- Movers and sectors
- Treasury yields and reference rates
Only 200 JSON answers carry one. A very large body (an unfiltered chain for the biggest symbols) can come back without an ETag. Narrow the read with expiry or a strike range and it gets one again.
How often each read changes
| Read | Changes | Useful polling interval |
|---|---|---|
| GEX grid (live) | A new capture every 5 minutes during market hours | 5 minutes |
| GEX history, past session | Never | Fetch once and keep it |
| GEX reference book | Never, once captured (the open is written once; the prior close is frozen) | Fetch once and keep it |
| Option chain (live) | Re-captured at most every 30 seconds per symbol | 30 seconds |
| Expirations | Refreshed at most every 5 minutes | 5 minutes |
| Movers and sectors | Recomputed at most once a minute | 1 minute |
| Quotes and spot | Follow the current session's daily bar as it builds | As often as you need |
| Treasury yields | Published once a day, after the close | Hourly at most |
| Symbol registry | When coverage changes, not intraday | Daily |
Polling faster than a read changes just returns the same snapshot (a 304 if you send its ETag). For tick-level updates, use Streaming instead of polling.
In the SDKs
Both SDKs keep the ETag on every result. They do not cache for you: a 304 result has metadata and no new data, and deciding what to reuse is up to your code.