# How to check CS2 market data freshness before you trust a price

> Check CS2 market data freshness before you trust a price: per-market ingest status, latency percentiles, stale flags, and marketplace health.

*Catalog & data quality · Beginner · Updated 2026-09-25*

Every price you read is a claim about the past. A marketplace published it at some moment, a collector picked it up a little later, and by the time your code acts on it the listing may be gone. Most of the time the gap is seconds and it does not matter. The expensive cases are the ones where a marketplace quietly stopped updating an hour ago and its last prices still look perfectly normal.

SkinPricer exposes freshness at three levels so you can catch that before it costs money: on every quote, per marketplace right now, and per marketplace over the last day. This guide explains each signal, which endpoint serves it, and how to combine them into a simple trust gate.

## Level 1: freshness on every quote

Each quote the pricing endpoints return carries its own timing. These fields are documented in full on the [Freshness & timestamps](/api-docs/freshness) page; the ones that matter most are:

| Field | Meaning |
| --- | --- |
| `fetchedAt` | When SkinPricer last fetched this market's quote. |
| `ingestedAt` | When the quote reached SkinPricer's price store, shortly after `fetchedAt`. Judge staleness by `fetchedAt` or `isStale`, not this. |
| `ingestLatencyMs` | The delay between the two, in milliseconds: SkinPricer's processing delay, not the age of the price. |
| `isStale` | True when `fetchedAt` is more than 30 minutes old (a uniform window across markets). On snake_case endpoints it is `is_stale`. |
| `freshMarketCount` | On NBBO and pricing responses: how many of the contributing markets are still fresh. |

Stale quotes are still returned, deliberately. On a thinly traded item a 40-minute-old ask may be the only price that exists, and hiding it would be worse than labelling it. Treat `isStale` as a soft signal: fine for display with a label, not fine as one leg of a trade. Also remember that `null` is not zero. A missing bid means no one is bidding, and the [Nulls & partial data](/api-docs/nulls) page lists every case where a field is null rather than measured.

## Level 2: is each marketplace updating right now?

Per-quote flags tell you about one price. [`GET /v1/status/freshness`](/api-docs/status) tells you about a whole marketplace: for each market it returns `delay_seconds` since its most recent ingested data, a status, and an `overall_status` roll-up across all markets. It is key-gated (Developer plan and up) and its payload is snake_case.

| Status | Rule |
| --- | --- |
| `healthy` | Last data 5 minutes ago or less. |
| `degraded` | Between 5 and 15 minutes. |
| `stale` | More than 15 minutes. |
| `overall_status` | `stale` if more than half the markets are stale; `degraded` if any market is stale or more than 30% are degraded; otherwise `healthy`. |

**curl**

```bash
export SKINPRICER_API_KEY="sp_live_•••"   # from your account's API keys page

curl -s "https://pricing.skinpricer.com/v1/status/freshness" \
  -H "Authorization: ApiKey $SKINPRICER_API_KEY"
```

**Node (SDK)**

```ts
// npm i @skinpricer/sdk
import { SkinpricerClient } from "@skinpricer/sdk";

const client = new SkinpricerClient({
  apiKey: process.env.SKINPRICER_API_KEY!,
  authScheme: "ApiKey",
});

const freshness = await client.status.freshness();
console.log(`Overall: ${freshness.overall_status} at ${freshness.checked_at}`);

for (const m of freshness.markets) {
  if (m.status !== "healthy") {
    console.log(`${m.market} is ${m.status}: last data ${m.delay_seconds}s ago`);
  }
}
```

**Response 200** (`GET /v1/status/freshness`)

```json
{
  "markets": [
    {
      "market": "dmarket",
      "delay_seconds": 0,
      "last_update": "2026-06-23T22:00:00.000Z",
      "status": "healthy"
    },
    {
      "market": "steamcommunity",
      "delay_seconds": 0,
      "last_update": "2026-06-23T22:00:00.000Z",
      "status": "healthy"
    }
  ],
  "overall_status": "healthy",
  "checked_at": "2026-06-23T22:04:00.118Z"
}
```

*The documented response: one row per market plus the roll-up.*

## Level 3: how reliable has each marketplace been?

A market can be healthy this minute and still be flaky. [`GET /v1/status/market-latency`](/api-docs/market-latency) summarizes the last 24 hours per market: average, p95, and p99 fetch-to-ingest latency in milliseconds, the number of fetches, the share that errored (`error_rate`, 0 to 1), and the share that breached the latency target (`slo_breach_rate`, 0 to 1). A market with a high p99 or a rising breach rate is one to down-weight even when its latest quote looks fresh.

```ts
const latency = await client.status.marketLatency();

const shaky = latency.markets
  .filter((m) => m.error_rate > 0.05 || m.slo_breach_rate > 0.25)
  .map((m) => `${m.market} (p95 ${Math.round(m.p95_latency_ms / 1000)}s)`);

console.log(`Over the last ${latency.window_hours}h, watch: ${shaky.join(", ") || "none"}`);
```

*The thresholds are examples. Pick yours from how your strategy tolerates delay.*

**Response 200** (`GET /v1/status/market-latency`)

```json
{
  "markets": [
    {
      "market": "buff163",
      "avg_latency_ms": 51439,
      "p95_latency_ms": 69525,
      "p99_latency_ms": 75350,
      "fetch_count": 30003561,
      "error_rate": 0,
      "slo_breach_rate": 0.28
    },
    {
      "market": "dmarket",
      "avg_latency_ms": 839,
      "p95_latency_ms": 1021,
      "p99_latency_ms": 16686,
      "fetch_count": 3078604,
      "error_rate": 0,
      "slo_breach_rate": 0
    }
  ],
  "window_hours": 24,
  "measured_at": "2026-06-23T09:08:55.013Z"
}
```

## The keyless view: market health

[`GET /v1/markets/health`](/api-docs/markets) is the one endpoint that needs no API key, which makes it ideal for status pages and quick checks. It is served from `api.skinpricer.com` and limited by IP (roughly 60 requests a minute). For every integrated marketplace it returns:

- `status`: `live`, `warn`, or `stale`, judged against that market's own expected refresh cadence (a market that normally updates every few minutes is held to a tighter bar than a slow one).
- `lastEventAt`: the time of the latest ingested event, or `null` if there has not been one.
- `freshnessCoveragePct`: the share (0 to 100) of that market's served listings fetched within its expected refresh window. `null` when the market serves no rows. It complements `status`, which only tracks the ingest heartbeat.
- `healthBars`: 48 half-hour buckets covering the last 24 hours, most recent last, where `2` is live, `1` degraded, and `0` down.

```python
import requests

# No API key needed for market health.
res = requests.get("https://api.skinpricer.com/v1/markets/health", timeout=10)
res.raise_for_status()

for market in res.json():
    coverage = market.get("freshnessCoveragePct")
    down_last_day = market["healthBars"].count(0)
    print(
        f"{market['id']:<16} {market['status']:<6}",
        f"coverage={coverage if coverage is not None else 'n/a'}%",
        f"down buckets={down_last_day}",
    )
```

> **Two vocabularies, on purpose:** The freshness endpoint says `healthy / degraded / stale` (fixed 5 and 15 minute thresholds), while market health says `live / warn / stale` (relative to each market's cadence). They answer slightly different questions, so do not map one onto the other one-to-one.

## Put it together: a trust gate

A practical pattern is to refresh the market-level signals once a minute on your server, keep a set of markets you currently trust, and check each quote against that set before acting on it.

```ts
import { SkinpricerClient } from "@skinpricer/sdk";

const client = new SkinpricerClient({
  apiKey: process.env.SKINPRICER_API_KEY!,
  authScheme: "ApiKey",
});

interface HealthRow {
  id: string;
  status: "live" | "warn" | "stale";
  freshnessCoveragePct: number | null;
}

/** Markets that are healthy now and whose listings are mostly fresh. */
export async function trustedMarkets(): Promise<Set<string>> {
  const [freshness, healthRes] = await Promise.all([
    client.status.freshness(),
    fetch("https://api.skinpricer.com/v1/markets/health"), // keyless
  ]);
  const health = (await healthRes.json()) as HealthRow[];

  const healthyNow = new Set(
    freshness.markets.filter((m) => m.status === "healthy").map((m) => m.market),
  );
  return new Set(
    health
      .filter((h) => h.status === "live" && healthyNow.has(h.id))
      .filter((h) => h.freshnessCoveragePct === null || h.freshnessCoveragePct >= 80)
      .map((h) => h.id),
  );
}

/** Act only on quotes that are fresh and come from a trusted market. */
export function canUse(
  quote: { market: string; isStale: boolean },
  trusted: Set<string>,
): boolean {
  return !quote.isStale && trusted.has(quote.market);
}
```

*Cache the trusted set for a minute. The freshness read counts against your plan's budget; market health is keyless and does not.*

> **Filtering the NBBO changes its bid rule:** It is tempting to pass your trusted set as the `markets` list on [`/v1/nbbo`](/api-docs/nbbo). Doing so also switches off the default cash-only best-bid filter, so every listed venue's bid counts verbatim. Leave wallet-credit venues such as `steamcommunity` out of the list unless you want their bids, or filter quotes after the call instead.

## When to reach for a different endpoint

Freshness checks guard the numbers; they do not produce them. For a single item's best prices with per-side staleness built in, read the [best bid and ask guide](/resources/cs2-best-bid-and-ask). If you alert on prices, the [price alert bot guide](/resources/cs2-price-alert-discord-bot) shows where these checks belong in a polling loop. And if you are showing prices to visitors, the [comparison site guide](/resources/build-cs2-price-comparison-site) turns these same signals into status badges.

## Endpoints used

- [GET /v1/status/freshness](/api-docs/status.md)
- [GET /v1/status/market-latency](/api-docs/market-latency.md)
- [GET /v1/markets/health](/api-docs/markets.md)
- [Freshness & timestamps](/api-docs/freshness.md)
- [Nulls & partial data](/api-docs/nulls.md)

## Related guides

- [How to build a CS2 price alert bot for Discord](/resources/cs2-price-alert-discord-bot.md)
- [How to build a CS2 skin price comparison site](/resources/build-cs2-price-comparison-site.md)
- [How to find the best bid and ask for a CS2 skin across every market](/resources/cs2-best-bid-and-ask.md)
