Loading…

Quickstart

The SCMM API gives you the Rust skin economy: the item catalogue, live prices across 26+ third-party markets, store rotations, workshop submissions and inventory valuation. Reads need no API key.

Base URL

Every endpoint lives under https://api.scmm.app, and every path in the reference already includes the /api prefix. There is no versioned path segment — see Migrating from the v1 API if you are moving off rust.scmm.app.

curl
curl "https://api.scmm.app/api/item?filter=Tempered&pageSize=5"

Authentication

Reads are open. The catalogue, prices, stores, workshop and inventory endpoints take no credential at all — the same promise the v1 API made, kept.

A credential is only needed for the handful of endpoints that read or write your own account, marked Auth required in the reference. Two forms are accepted:

  • x-api-key header — a per-profile key. This is the one to use from a bot or a script.
  • The scmm.sid session cookie, issued by the Steam sign-in flow. This is what the website itself uses; it is not something you can mint from a script.
curl
curl "https://api.scmm.app/api/profile/me" \
  -H "x-api-key: YOUR_KEY_HERE"

To check a key is live, call GET /api/auth/me. It returns the profile the key belongs to, or 401.

API keys are issued by hand today — ask in our Discord and tell us what you are building. Self-serve keys are on the way. A key carries your profile’s authority: treat it like a password, never ship it in client-side code, and tell us if it leaks.

Money is always integer hundredths

Every price, value and total in this API is an integer in hundredths of the currency unit. 4250 means $42.50. There are no floats anywhere.

This holds for every currency, including ones whose real-world minor unit is not a hundredth. Divide by 100 and nothing else.

If you are porting from v1, this is the bug you will ship. The v1 API exposed a per-currency scale and expected you to divide by 10^scale. That field still exists on the currency DTO, so the old code keeps compiling and keeps looking right — it is correct for every currency where scale happens to be 2, and silently wrong for the rest. Delete the exponent.

Prices are returned in USD unless you ask otherwise. Pass ?currency=EUR (any 3-letter code from GET /api/currency) on the endpoints that accept it, and conversion happens server-side at the current rate.

Conventions

  • Field names are camelCase.
  • Timestamps are ISO 8601 strings in UTC (2026-08-22T04:13:00.000Z).
  • Item ids are GUID strings — the guid field. Endpoints taking an item id also accept the item’s nameHash, which is the stabler key to store. (There is no marketHashName field on the v2 DTO; nameHash is what v1 called that.)
  • Steam account identifiers — steamId, profileId, creatorId — are strings, because they exceed 253 and would lose precision as JSON numbers. Catalogue identifiers are not: assetDescriptionId, classId, itemDefinitionId and workshopFileId come back as JSON numbers on most endpoints and as strings on /api/workshop. Treat them as opaque and never do arithmetic on them.
  • id is not one type across the API: it is the legacy numeric assetDescriptionId on /api/item and a GUID string on /api/store. Key on guid or nameHash and you never have to care.
  • Paginated endpoints take page (1-based) and pageSize, and return { items, page, pageSize, total }. Three deviate, on purpose: GET /api/workshop pages by week, so it returns weeks with totalWeeks and totalItems in place of items and total; GET /api/inventory/leaderboard calls its rows entries; and GET /api/marketplace/{market}/prices/history paginates only in raw mode (points=true) — in aggregate mode it returns every item in one response and pageSize reports how many that is, not what you asked for.

Errors

Failures return a JSON body with the status, a short error name, and a human-readable message:

404
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Item not found",
  "timestamp": "2026-08-22T04:13:00.000Z",
  "path": "/api/item/does-not-exist"
}

Branch on statusCode. The message is written for a human reading a log and is not a stable contract.

Every price in one call

Do not page through GET /api/item to build a price list. The catalogue is ~6,000 items and pages cap at 100, so that is ~62 requests against a 100/minute limit — one sweep costs your whole minute. GET /api/item/prices returns every item in a single response instead.

curl
curl "https://api.scmm.app/api/item/prices"
One element of the response
{
  "id": "3e5933b9-0d13-4b65-89f1-9447d299fde6",
  "appId": "252490",
  "classId": "1132469022",
  "name": "Multicam Jacket",
  "nameHash": "Multicam Jacket",
  "price": 17,
  "fee": -6,
  "supply": 1,
  "marketType": "CSTrade",
  "currency": "USD"
}

You get the cheapest offer we know of for each item, and marketType tells you where it is. An item nothing currently sells has price: null — not 0.

The cheapest total is price + fee, not price. price is the market’s listed price and fee is our model of what buying there actually costs you — frequently negative, because a market whose cheapest payment route is bonus balance sells below face value. In the row above, CSTrade lists at 17 and costs 11. Sort or compare on the sum, or you will rank a 17 above a 13 that is really more expensive.

Add markets= to restrict it to the markets you can actually buy from. That filters the winner and adds a per-market prices array, cheapest first:

curl
curl "https://api.scmm.app/api/item/prices?markets=Skinport,DMarket"
Same item, filtered
{
  "nameHash": "Multicam Jacket",
  "price": 13,
  "fee": 0,
  "marketType": "DMarket",
  "currency": "USD",
  "prices": [
    { "marketType": "DMarket",  "price": 13, "fee": 0, "supply": 49, "isAvailable": true, "url": "https://dmarket.com/..." },
    { "marketType": "Skinport", "price": 13, "fee": 0, "supply": 24, "isAvailable": true, "url": "https://skinport.com/..." }
  ]
}
  • Market names are the marketType values you see in any response. Unknown names are ignored rather than rejected, so a typo returns 200 with fewer offers, not a 400.
  • currency works here like everywhere else — ?currency=EUR converts every monetary field server-side.
  • Key on nameHash. classId is also returned, which is what the v1 version of this endpoint used as its id.
  • It is not cached, so you always see current prices. It is limited to 10 requests / minute — one call is already the whole catalogue, so polling it once a minute is plenty. See Rate limits.

The same trick for history. GET /api/marketplace/{market}/prices/history is the per-market transpose: one request returns a 30-, 60- or 90-day price aggregate for that market’s whole catalogue, where the per-item series (GET /api/item/{id}/prices/history) is one request per item. Ask for several windows at once with ?windows=30d,60d,90d, narrow the row with ?fields=avg, and stay current with ?since= — every response hands you the next cursor as nextSince, and a poll that finds nothing is an empty list rather than the catalogue again.

curl
curl "https://api.scmm.app/api/marketplace/Skinport/prices/history?windows=30d,60d,90d&stats=avg"
  • It sits on the same 10 requests / minute budget as GET /api/item/prices, counted separately — each endpoint has its own. See Rate limits.
  • GET /api/marketplace/{market}/sales-summary is its companion: the market’s own completed-sale statistics, where the history endpoint reports listing prices. They are different quantities and are named differently — avgSalePrice against avgListingPrice.
  • Every parameter of both is in the market reference, which is generated from the live spec.

An average is only as deep as what we have observed. Ask for a 90-day mean before ninety days have been captured and you get the mean of what exists, under a 90-day label. Every window block carries avgDays and the envelope carries observedDays— read one of them before you trade on the difference between two windows. Capture began at a different date for each market, and until a market’s archive is longer than the widest window you asked for, a 30d, 60d and 90d request return the same number wearing three labels. Pricing data covers this, and the rest of what a price model needs.

Rate limits

Limits are counted per IP and per endpoint, in a rolling 60-second window. Each endpoint carries its own budget, so spending one does not spend another.

LimitApplies toWhy
100 / minuteEvery endpoint not listed below.The default.
30 / minuteGET /api/searchFive uncached full-text scans per call.
10 / minuteGET /api/item/prices1.3 MB uncached — one call is the whole catalogue.
10 / minuteGET /api/marketplace/{market}/prices/history, GET /api/marketplace/{market}/sales-summaryA market’s whole catalogue in one response; each route counts separately.
10 / minutePOST /api/inventory/calculateResolves a Steam profile on our quota.
60 / minutePOST /api/workshop/file/{workshopFileId}Sized for the 3D viewer’s own polling.
20 / minuteGET /api/workshop/file/{workshopFileId}/downloadStreams an archive averaging ~9 MB.
30 / minuteGET /api/social/og/store.png, GET /api/social/og/item/{id}.pngRenders an image; each route counts separately.
exemptGET /api/healthcheckUptime probes must never be rejected.

Five of the endpoints above are not in the reference: GET /api/healthcheck, the two /api/workshop/file routes and the two /api/social/og images. They support the site rather than forming part of the documented API — an uptime probe, the 3D viewer’s cache, and the preview images link unfurlers fetch — so they carry a budget without carrying a contract. They are listed here because a limit you can hit is a limit you should be able to read.

Every response tells you where you stand, so you never have to model the budget yourself.

HeaderOnWhat it is
X-RateLimit-LimitEvery responseThe budget for the endpoint you just called — the number in the table above.
X-RateLimit-RemainingEvery responseWhat is left of it in the current window.
X-RateLimit-ResetEvery responseSeconds remaining until the window rolls and the budget refills.
Retry-After429 onlySeconds to wait before the request would be accepted.

X-RateLimit-Reset is a duration, and it is not a Unix timestamp — on some other APIs the same header name is one. A value of 29 means twenty-nine seconds from now, not 1 January 1970.

Exceeding a limit returns 429. Back off and retry — there is no penalty beyond the rejection, and the budget refills as the window rolls. The rejection carries Retry-After and none of the three X-RateLimit headers, so wait out Retry-After rather than reading a remaining count that is not on that response.

If you want every item’s price, do not sweep GET /api/item — that is ~62 requests, since the catalogue is ~6,000 items and pages cap at 100. Use GET /api/item/prices, which returns the whole catalogue in a single response; at 10 / minute you can refresh it every six seconds, and once a minute is plenty. If you are running into a ceiling for something legitimate, talk to us rather than sharding across IPs.

Next

  • Pricing data — the call sequence for a price model, the day counts that bound every average, and the three different prices this API publishes.
  • API reference — every public endpoint, grouped by resource.
  • Migrating from the v1 API — endpoint-by-endpoint mapping off rust.scmm.app, and the four changes that affect every request.
  • Items — the catalogue, and where most integrations start.

SCMM is a fan project, run at our own cost. The API is free and unmetered in the spirit of the original: use it, build things with it, and do not make us regret it.