Loading…

Market data

Cross-market listings, deals, flips and crafting costs across the seventeen markets we track.

Per-market price history, aggregated or raw

GET/api/marketplace/{market}/prices/history

Per-market price history — the transpose of GET /api/item/{id}/prices/history: this endpoint fixes the market and returns every item.

Each item is one aggregate row by default: mean, minimum and maximum listing price, and the day count behind them. Pass points=true for the raw daily series instead, paginated by item. avgListingPrice averages one point-in-time cheapest ask per day — not volume-weighted, not intraday, and not a sale price; for what items actually sold for, use GET /api/marketplace/{market}/sales-summary. Read avgDays and observedDays on every response: coverage is unequal between markets and often shorter than the window asked for, so an average over 3 days is otherwise indistinguishable from one over 30.

windows=, fields=, stats=, since= and dataPoints= each change the shape of the response and are documented individually below.

For Steam’s own sale volume there is a far deeper series than this archive holds: GET /api/item/{id}/sales/market?maxDays=0 returns one point per day back to the first sale Steam recorded for the item, where the archive behind this endpoint begins when we started capturing each market. It is a sale volume rather than a listing price, so the two answer different questions.

Warning: the newest daily bucket is provisional and self-heals overnight, because capture resumes from the start of the newest already-counted day. A window that includes today runs low unless the last bucket is excluded.

Warning: bids and asks are different quantities and must never be compared, averaged together or substituted. price and avgListingPrice are what you pay; bidPrice and avgBidPrice are what you would receive, and the difference is the spread you would cross to sell into this market. Only Steam, DMarket, Tradeit, RustSkins and Mannco publish a bid this API can read, so null is the normal answer and never means nobody is bidding. It is null on the other twelve markets, and that is a gap in what we can reach rather than a gap in the market: five of them are trade bots, which hold their own stock and run no order book, but a trade bot still quotes a price it will buy at, and Tradeit publishes exactly that. Read null as not published here, never as nobody bidding.

Warning: soldTotal is null whenever the counts cannot be summed honestly. Ten of the seventeen markets publish no sold count at all, and a trailing 24h or 30d figure is not a daily one. A further two — Mannco and RustSkins — publish one we do not collect nightly. The archive also holds a historical series imported from three vendors’ own trade histories: RustTM up to six thousand sales per item, RustSkins the last fifty sales per item and CS.Deals the last five thousand sales per item, the whole ledger on quieter items. For one of them — RustSkins — that series is not maintained, so past its newest dated point soldTotal returns to null: that means not collected, never nothing sold. null never means zero.

Parameters

NameInRequiredDescription
marketpathyesMarket name, e.g. Skinport. A market that is not published returns 404 — the same answer an unknown market gets.
periodquerynoTime window. Defaults to 30d. An unrecognised value falls back rather than erroring.

One of: 7d, 30d, 60d, 90d, 1y, all

pointsquerynotrue returns the raw daily series instead of aggregates. It is the only shape here whose size grows with history depth — the aggregate is one row per item at any window, while a raw 90-day series runs to tens of megabytes per market — so prefer the default unless you need the shape of the move, and bound it with dataPoints=.
pagequeryno1-based page index. Raw mode only.
pageSizequerynoItems per page (max 500). Raw mode only. It also decides the resolution of each series: a raw page reads at most 12,000 observations and shares them equally between the items on it, so each item may return 12000 / pageSize of them — 120 at the default pageSize=100, 24 at the maximum of 500, and never more than 400. Every raw response reports the figure as maxPointsPerItem; lower pageSize to buy more points per item. An item holding more than the ceiling is sampled evenly across the window with the oldest and newest kept, so the ceiling bounds the resolution of a series and never its window.
windowsquerynoComma-separated list of windows to aggregate in one response, e.g. 30d,60d,90d — one request instead of one per window. Each item then carries a windows map keyed by the window label, and the flat avgListingPrice, minPrice, maxPrice and sold fields are replaced by it, because a number whose window you have to infer is the defect avgDays exists to prevent. Every block carries its own avgDays, and you should read it before comparing windows: a 60-day and a 90-day average backed by the same 17 observed days are the same number twice, and the day count is the only thing on the wire that says so. In this mode period reports the widest window you asked for, because that is the window scanned and the one that decides which items appear at all; a window an item has no data in comes back with avgDays: 0 and null prices rather than being omitted. Accepts the same vocabulary as period, and a period= you pass as well is included rather than dropped. Unrecognised entries are ignored, and the parameter is ignored entirely when points=true.
statsquerynoComma-separated subset of avg,min,max,sold,bid limiting what each window block carries. An opt-out: the default is all of them, and an unrecognised list falls back to all of them rather than to none. avgDays is always present and cannot be excluded. bid adds the market’s best standing buy order to each block beside the ask. Multi-window mode only — it narrows the per-window blocks, where fields= narrows the flat row; the two are siblings rather than alternatives, and each is ignored in the other one’s mode.
sincequerynoISO 8601 timestamp. Returns only the items observed strictly after it — pass the nextSince from your previous response, which every response carries whether or not you sent since. This feed updates once a day, so polling more often is cheap but usually returns an empty items: that is the parameter working, not a fault. The comparison is strict, so re-sending the same cursor returns nothing rather than repeating the last day. It selects which items come back and does not change the window they are aggregated over — a period=30d&since=<yesterday> row is still a 30-day average, of the items that moved yesterday. Applies in every mode. An unparseable value is a 400 rather than a silent full response, because a cursor quietly dropped would look exactly like a working one while you re-downloaded the catalogue on every poll.
fieldsquerynoComma-separated subset of avg,min,max,last,sold,bid,liquidity limiting what the flat aggregate row carries. An opt-in: the default is every group except liquidity, and an unrecognised list falls back to that same default rather than to none. Use it with since= and a poll costs almost nothing. liquidity is the one group outside the default, because it is the only field here that does not come from our price history: it scores Steam’s live order book and 30-day sales volume, so asking for it adds a second question to the request. id, marketHashName and avgDays can never be excluded — avgDays because it is the denominator that makes every other number readable. last and sold are groups rather than single fields: lastPrice and lastSupply travel together because a supply without the observation it was measured at is not interpretable, and soldTotal, avgSoldPerDay, soldDays and soldWindow because a sold total without its window cannot be read at all. bid adds the market’s best standing buy order — what it would pay you — beside the ask. An excluded field is absent from the row, while a field that is present and null means no such observation exists; the two are not the same. Aggregate mode only — the per-window blocks are narrowed by stats=.
dataPointsquerynoThin each item’s raw series to at most this many observations. The first and last are always kept and the rest are spread evenly between them, which is what a chart needs: the shape of the move at N points rather than every row plus a page cursor. The spread is even across the series rather than across the calendar, so a market that stopped reporting for a fortnight has that gap in its sample too — read each point’s own timestamp rather than assuming a fixed spacing. Raw mode only (points=true) and ignored otherwise, exactly as stats= is ignored outside multi-window mode; an unparseable value is ignored rather than rejected, and you then get the full series and no dataPoints key. Clamped to 400, the per-item ceiling this endpoint already enforces, and echoed back as the ceiling applied rather than as a per-item count. A soldCount in a thinned series cannot be summed: the days between the samples were dropped, not observed as zero.
priceKindquerynoWhich quantity the raw series carries, one of ask, sale or all. Defaults to ask, which is what every observation archived before 2026-09-03 is and what this endpoint has always returned. An ask is the cheapest listing a market was offering that day. A sale is a transaction — what somebody actually paid — and depth differs wildly by market: sale rows exist where a market publishes its own trade history, and reach years further back than our own capture does. Every point carries its own priceKind, so a mixed response is always readable. Where a market publishes only a trailing aggregate rather than individual trades, a sale point is that aggregate as read at capture time — a rolling 24 hours, offset from the calendar day it is filed under by the hour the capture runs. Raw mode only (points=true), and ignored otherwise — including in multi-window mode, exactly as dataPoints= is. The default shape of this endpoint is an aggregate, and an average over asks and transactions together is not a worse number but an unreadable one: the gap between the two is the market’s spread, not noise. observedDays, total and nextSince describe the kind you asked for, so a sale-mode cursor is a sale-mode cursor. Note: an unrecognised value falls back to ask rather than to everything, which is the opposite of how fields and stats behave on this same endpoint. A typo there costs you some fields; here it would hand you two quantities that cannot be compared.

One of: ask, sale, all

Request

curl "https://api.scmm.app/api/marketplace/<market>/prices/history?period=7d"

Response 200 · application/json · object

  • market*enum
    One of: Unknown, SteamStore, SteamCommunityMarket, Skinport, LootFarm, SwapGGTrade, TradeitGG, CSDeals, SkinBaron, RustSkins, RustTM, RustyTrade … +15 more

    CSTrade, iTradegg, SkinsMonkey, SkinSwap, DMarket, Waxpeer, ShadowPay, ManncoStore, RapidSkins, SkinSerpent, Rustyloot, SnipeSkins, LISSkins, PirateSwap, AvanMarket

  • period*string

    The window as requested. In multi-window mode it reports the widest window you asked for, because that is the window scanned and therefore the one that decides which items appear; every window you asked for is listed under windows.

  • windowDays*number | null

    Days the requested window spans. null for all.

  • observedDays*number

    Distinct UTC days this market actually has data for inside the window. This is the honest ceiling on every average in the response and is frequently lower than windowDays.

  • points*boolean

    true when this response carries raw points rather than aggregates.

  • page*number

    1-based page index. Always 1 in aggregate mode, which is not paginated.

  • pageSize*number

    How many items this response carries, and it means two different things in the two modes. In raw mode (points=true) it is the page size you asked for, clamped to 500, and an empty page still reports one. In aggregate and multi-window mode the endpoint is not paginated — one call returns the whole roster — so page/pageSize are echoed to keep one envelope shape and pageSize is the number of items in the response, which is total unless since= narrowed it. Passing pageSize=2 in aggregate mode therefore comes back as the item count, not as 2.

  • total*number

    Items this market has history for in the window, before paging.

  • array<object>optional

    The windows you requested and their coverage. Present only when you passed windows=, and its presence is also how you know which items shape you got, the same way points reports the raw or aggregate split.

  • statsarray<string>optional

    Which statistics the per-window blocks carry, echoed back so an absent field is never ambiguous between “you opted out” and “the server does not serve it”. Multi-window mode only.

  • sincestringoptional

    The delta cursor this response was filtered by, echoed back. Present only when you passed since=, so a genuinely empty delta is never confused with a cursor the server ignored.

  • fieldsarray<string>optional

    Which field groups the flat aggregate row carries, echoed back. Present only when you passed fields=, so an absent key means you got all of them. Applies to the flat row only — the per-window blocks are narrowed by stats=.

  • dataPointsnumberoptional

    The point ceiling each item’s raw series was thinned to, echoed back. It is present only when dataPoints= was sent with points=true, and its presence is the only thing that distinguishes a sampled series from a market with sparse coverage. soldCount in a thinned series cannot be summed: the days between the samples were dropped, not observed as zero. The value is the ceiling that was applied rather than a per-item count, so an item with fewer observations comes back whole, and it is the clamped figure: a request above 400 is answered at 400 and reports 400.

  • maxPointsPerItemnumberoptional

    The most observations any one item can return on this page. Present on every raw response (points=true) and on no other shape. A raw page reads at most 12,000 rows and shares them equally between the items on it, so this is 12000 / pageSize, never above the endpoint’s own 400-point per-item limit: pageSize=100 allows 120 observations per item and pageSize=500 allows 24. Lower pageSize to buy more points per item. An item holding more than the ceiling is sampled evenly across the window with the oldest and newest always kept — the same rule dataPoints applies, and with the same consequence, that soldCount in a sampled series cannot be summed. An item holding fewer comes back whole. It is a ceiling on the number of points, never on the window: period, windowDays and observedDays always describe a window the series actually spans.

  • nextSince*string | null

    The cursor to send as since= on your next poll. Use it rather than your own clock: rows are stamped with the UTC day they describe and written shortly after that day begins, so a wall-clock cursor can sit after the timestamp of rows that have not been written yet, and those rows would then never appear in any delta. Present on every response, including an empty one. null only when this market has no observations in the window.

  • items*array<any>

    Aggregates by default; one block per requested window when windows= is passed; raw series when points=true.

A market's own published sale statistics

GET/api/marketplace/{market}/sales-summary

A marketplace's own published sale statistics, read through from the vendor and attributed to them: minimum, maximum, mean and median, and the sale volume behind each, across their trailing 24h, 7d, 30d and 90d windows. Prices are USD hundredths (divide by 100), like every other price on this API.

The response defaults to the 30d and 90d windows and to the average alone; pass windows= and stats= for the rest. volume always ships and cannot be excluded. These are trailing windows rather than daily buckets, so they cannot be summed, differenced, or divided into a per-day rate. They are read live from the vendor behind a cache and are not captured into our history: stale: true means the vendor call failed and this is the last good payload, and fetchedAt says when it was read.

Warning: these are sale prices — what items actually sold for — and not listing prices. They are a different quantity from avgListingPrice on the price-history endpoint, which averages the cheapest ask observed each day. Do not compare or merge the two.

Warning: a null here means the vendor sold none of this item in that window. It is an observation, always paired with volume: 0, and it is the opposite of soldCount on the price-history endpoint, whose null means the market publishes no sold count at all.

Warning: read volume beside every average — an average over two sales looks identical to one over two hundred.

Parameters

NameInRequiredDescription
marketpathyesMarket name. A market that is not published, or one that publishes no sale statistics, returns 404 — the same answer an unknown market gets.
windowsquerynoComma-separated subset of 24h,7d,30d,90d. Defaults to 30d,90d. Unrecognised entries are ignored, and an entirely unrecognised list falls back to the default rather than erroring.
statsquerynoComma-separated subset of avg,min,max,median. Defaults to avg alone, which is the shape this endpoint is sized for; asking for all four roughly doubles the payload. volume is always present and cannot be excluded.

Request

curl "https://api.scmm.app/api/marketplace/<market>/sales-summary?windows=<windows>"

Response 200 · application/json · object

  • market*enum

    One of: Skinport

  • source*string

    Whose figure this is. Attribution is required when re-publishing it.

  • sourceUrl*string

    The vendor's own site, for attribution in a UI.

  • appId*string

    Steam app id this summary covers — the vendor is queried per app.

  • currency*string

    Currency the vendor reported in. Prices are USD hundredths regardless.

  • fetchedAt*string

    When we last successfully called the vendor.

  • expiresAt*string

    When this payload goes stale and the next request refreshes it.

  • stale*boolean

    true when the vendor call failed and this is the last good payload rather than a fresh one. Check fetchedAt to see how old it is.

  • windows*array<string>

    Which windows each item carries, in the order requested.

  • stats*array<string>

    Which statistics each window block carries, echoed back so an absent field is never ambiguous. An opt-IN — the default is avg alone. volume is always present and cannot be excluded: 23.5% of 30-day averages rest on two sales or fewer, so an average without its count is a confident wrong number.

  • total*number
  • array<object>

Crafting cost analysis (containers + resources)

GET/api/marketplace/crafting

For each craftable container: the cheapest craft-from-resources total vs. the cheapest direct buy, plus the resource breakdown. For each base resource: the cheapest "break a larger item" cost vs. the cheapest direct buy.

Parameters

NameInRequiredDescription
currencyqueryno3-letter currency code. Prices convert to it. Defaults to USD.

Request

curl "https://api.scmm.app/api/marketplace/crafting?currency=USD"

Response 200 · application/json · object

  • array<object>
  • array<object>

List current marketplace deals

GET/api/marketplace/deals

Items with the largest absolute or percentage discount versus their store/market reference price.

Parameters

NameInRequiredDescription
startquerynoZero-based offset into the ranked results (infinite scroll). Defaults to 0.
countquerynoPage size. Defaults to 50, max 100.
limitquerynoDeprecated alias for count. Defaults to 50.
modequerynoflips ranks profitable buy→Steam flips by profit %. Otherwise cheapest offers (default).
sortquerynoCheapest mode: discount (default), price (market offer price) or steam (Steam Community Market price). Flips mode: profit (default) or price.
dirquerynoSort direction: asc or desc (default).
filterquerynoCase-insensitive substring match on item name.
marketquerynoOnly deals whose cheapest offer is on this MarketType.
minPricequerynoMinimum market price, in dollars.
maxPricequerynoMaximum market price, in dollars.
minDiscountquerynoCheapest mode: minimum discount % vs. the Steam Community Market price. Flips mode: minimum flip-profit %.
sellNowquerynoFlips mode only. true (default) prices the sell leg at Steam's highest buy order (sell right now). false uses the lowest sell order (list it and wait).
currencyqueryno3-letter currency code. Prices convert to it; minPrice/maxPrice are interpreted in it.

Request

curl "https://api.scmm.app/api/marketplace/deals?start=1"

Response 200 · application/json · object

  • array<object>
  • total*number

    Total deals matching the filters across all pages

  • start*number

    Zero-based offset of this page into the ranked set

  • count*number

    Page size requested (items.length ≤ this)

List marketplace items

GET/api/marketplace/items

Paginated list of items tracked across markets.

Parameters

NameInRequiredDescription
pagequeryno1-based page index. Defaults to 1.
pageSizequerynoItems per page. Defaults to 24, max 500. Values above the cap are clamped, never rejected.
currencyqueryno3-letter currency code. Defaults to USD.

Request

curl "https://api.scmm.app/api/marketplace/items?page=1"

Response 200 · application/json · object

  • array<object>
  • total*number
  • page*number
  • pageSize*number

Get a single marketplace item with cross-market prices

GET/api/marketplace/items/{id}

One item with every market’s current offer for it, cheapest first. Accepts the item guid or its nameHash. It is the market-side view of GET /api/item/{id}: fewer catalogue fields, and the per-market offers are the point rather than an add-on.

Each offer carries its own price, fee and supply, and isAvailable says whether it can be bought now — an offer the market holds no stock of stays in the list with supply: 0.

Parameters

NameInRequiredDescription
idpathyesItem guid or name-hash.
currencyqueryno3-letter currency code. Defaults to USD.

Request

curl "https://api.scmm.app/api/marketplace/items/<id>?currency=USD"

Response 200 · application/json · object

  • marketPrices*map<string, number> | null
  • array<object>
  • guid*string
  • nameHash*string | null
  • appId*string
  • itemId*number
  • workshopId*number
  • type*string | null
  • name*string
  • description*string | null
  • collection*string | null
  • imageUrl*string | null
  • isTradable*boolean
  • isMarketable*boolean
  • marketItemGuid*string | null
  • marketItemId*string | null
  • marketBuyOrderHighestPrice*number | null
  • marketSellOrderLowestPrice*number | null
  • marketLast168hrSales*number | null
  • releasePrice*number | null
  • releasedOn*string | null

Per-market presentation metadata

GET/api/marketplace/markets

One row per market carrying the presentation flags a client needs to render it consistently: call-to-action emphasis, whether the market supports bulk buying, and whether it is verified.

Request

curl "https://api.scmm.app/api/marketplace/markets"

Response 200 · application/json · array<object>

  • marketType*enum
    One of: SteamStore, SteamCommunityMarket, Skinport, LootFarm, SwapGGTrade, TradeitGG, CSDeals, SkinBaron, RustSkins, RustTM, RustyTrade, CSTrade … +14 more

    iTradegg, SkinsMonkey, SkinSwap, DMarket, Waxpeer, ShadowPay, ManncoStore, RapidSkins, SkinSerpent, Rustyloot, SnipeSkins, LISSkins, PirateSwap, AvanMarket

  • hidden*boolean

    The SCMM web UI does not render this market anywhere, while the API keeps publishing its data. Published as a field rather than an omission so a consumer can tell "hidden" from "absent".

  • featuredCta*boolean
  • bulkBuy*boolean
  • verified*boolean
  • pricesIngested*boolean

    we run a price job for this market and it is not in DISABLED_MARKETS.

Price history across every market, aggregated

GET/api/marketplace/prices/history

Price history for every item on every market we track, in one request — the cross-market sibling of GET /api/marketplace/{market}/prices/history, which fixes one market and returns every item.

Each item is returned once, carrying a markets map keyed by market name. A block inside that map is exactly the row the per-market endpoint returns, without the repeated item identity, so a query can be moved between the two endpoints unchanged: period, windows, stats, fields and since all keep their vocabulary, their defaults and their meaning. Use markets= to narrow the roster, and read each block’s own avgDays — coverage is a property of the market, not of the request, and it differs widely between them.

This endpoint is not paginated: one call carries the whole catalogue, and the response is the same size at every window because an aggregate is one block per item per market regardless of how many days it rests on. Narrow it with markets= and fields= rather than by paging, and poll it with since=.

Every window here is bounded by when we began archiving each market, which each response reports per market. For Steam’s own sale volume there is a far deeper series: GET /api/item/{id}/sales/market?maxDays=0 returns one point per day for a single item, back to the first sale Steam recorded for it. That is a sale volume rather than a listing price, so read it beside these figures rather than in place of them.

Warning: there is no raw mode here. A raw series is items multiplied by days multiplied by markets, which is a download rather than a response. For the daily observations behind these aggregates use GET /api/item/{id}/prices/history, which returns every market for one item, or GET /api/marketplace/{market}/prices/history?points=true, which returns one market for every item, paginated.

Warning: a market that has no observation for an item is absent from that item’s markets map, rather than present and empty. The roster that answered is on the response as markets, each entry with its own day count, so an absent key means this market published nothing for this item and never means the market was not searched.

Parameters

NameInRequiredDescription
periodquerynoTime window. Defaults to 30d. An unrecognised value falls back rather than erroring. Read it beside each block’s avgDays: the window you ask for is a request, and the day count you get back is what the average actually rests on.

One of: 7d, 30d, 60d, 90d, 1y, all

windowsquerynoComma-separated list of windows to aggregate in one response, e.g. 30d,60d,90d. Each market block then carries a windows map keyed by the window label instead of flat statistics, and every block carries its own avgDays per window. Read those before comparing windows: two windows backed by the same observed days are the same number twice, and the day count is the only thing on the wire that says so. Accepts the same vocabulary as period, and a period= you pass as well is included rather than dropped. Unrecognised entries are ignored.
statsquerynoComma-separated subset of avg,min,max,sold,bid limiting what each window block carries. An opt-out: the default is all of them, and an unrecognised list falls back to all of them rather than to none. avgDays is always present and cannot be excluded. Multi-window mode only — it narrows the per-window blocks, where fields= narrows the flat ones, and each is ignored in the other one’s mode.
sincequerynoISO 8601 timestamp. Returns only the items observed strictly after it — pass the nextSince from your previous response rather than your own clock. An item that moved on any market in the roster comes back with all of its markets, because a delta selects items rather than cells. The cursor is the newest observation across the roster you asked for, so take it from a request with the same markets= you intend to keep polling: a cursor from a wider roster can skip a slower market’s day. An unparseable value is a 400 rather than a silent full response.
fieldsquerynoComma-separated subset of avg,min,max,last,sold,bid,liquidity limiting what each market block carries. This endpoint defaults to avg alone, unlike its per-market sibling, because a default here is multiplied by every market in the roster; the value applied is echoed back on every response, so what you got is never something you have to infer. An unrecognised list falls back to the full set of default groups rather than to none. liquidity scores the item as a whole rather than any single market block, because it reads Steam’s order book and 30-day sales volume — one measurement per item, whichever markets carry it. avgDays can never be excluded, because it is the denominator that makes every other number in the block readable. last and sold are groups rather than single fields: a supply without the observation it was measured at is not interpretable, and a sold total without its window cannot be read at all. An excluded field is absent from the block, while a field that is present and null means no such observation exists.
marketsquerynoComma-separated list of markets to include, e.g. Skinport,DMarket. Defaults to every market we publish. Unrecognised entries are ignored rather than rejected, but a list of nothing but unrecognised entries narrows to nothing and returns an empty roster — this parameter is a filter, so refusing every named market is a real answer rather than a reason to hand back the largest response on the API. Narrowing it is the cheapest way to shrink this response: the payload is roughly proportional to how many markets carry each item.

Request

curl "https://api.scmm.app/api/marketplace/prices/history?period=7d"

Response 200 · application/json · object

  • period*string

    The window as requested. In multi-window mode it reports the widest window you asked for, because that is the window scanned and therefore the one that decides which items appear; every window you asked for is listed under windows.

  • windowDays*number | null

    Days the requested window spans. null for all.

  • observedDays*number

    Distinct UTC days any market in the roster has data for inside the window. This is the ceiling across the whole roster and therefore the wrong number to attribute to any single market: read the per-market figure under markets for that.

  • array<object>

    The markets this response carries, each with its own coverage, in a fixed order. It is the roster that survived markets=, and it is how you tell “this market published nothing for this item” — the market is listed here, the item’s key is not — from “I did not ask for this market”, where it is absent from both.

  • total*number

    Distinct items with history in the window across the roster, before any since= narrowing.

  • array<object>optional

    The windows you requested and their coverage. Present only when you passed windows=, and its presence is how you know each market block carries a windows map rather than flat statistics. Its observedDays is again the ceiling across the roster, not any single market’s.

  • statsarray<string>optional

    Which statistics the per-window blocks carry, echoed back so an absent field is never ambiguous between “you opted out” and “the server does not serve it”. Multi-window mode only.

  • sincestringoptional

    The delta cursor this response was filtered by, echoed back. Present only when you passed since=, so a genuinely empty delta is never confused with a cursor the server ignored.

  • fieldsarray<string>optional

    Which field groups each market block carries, echoed back on every aggregate response — including one narrowed to an empty roster. It is unconditional here, unlike on the per-market route, because this endpoint defaults to avg alone: the default is itself a narrowing, so leaving it to be inferred would be the one thing a consumer cannot afford to get wrong. Absent only in multi-window mode, where stats= is the narrowing parameter and is echoed in its place.

  • nextSince*string | null

    The cursor to send as since= on your next poll. Use it rather than your own clock: rows are stamped with the UTC day they describe and written shortly after that day begins, so a wall-clock cursor can sit after the timestamp of rows that have not been written yet, and those rows would then never appear in any delta. It is the newest observation across the roster you asked for, so take it from a request with the same markets= you intend to keep polling — a cursor taken from a wider roster can skip a slower market’s day. Present on every response, including an empty one. null only when no market in the roster has an observation in the window.

  • array<object>

    One entry per item, each carrying every requested market that has data for it.