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 "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-keyheader — a per-profile key. This is the one to use from a bot or a script.- The
scmm.sidsession 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 "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
guidfield. Endpoints taking an item id also accept the item’snameHash, which is the stabler key to store. (There is nomarketHashNamefield on the v2 DTO;nameHashis 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,itemDefinitionIdandworkshopFileIdcome back as JSON numbers on most endpoints and as strings on/api/workshop. Treat them as opaque and never do arithmetic on them. idis not one type across the API: it is the legacy numericassetDescriptionIdon/api/itemand a GUID string on/api/store. Key onguidornameHashand you never have to care.- Paginated endpoints take
page(1-based) andpageSize, and return{ items, page, pageSize, total }. Three deviate, on purpose:GET /api/workshoppages by week, so it returnsweekswithtotalWeeksandtotalItemsin place ofitemsandtotal;GET /api/inventory/leaderboardcalls its rowsentries; andGET /api/marketplace/{market}/prices/historypaginates only in raw mode (points=true) — in aggregate mode it returns every item in one response andpageSizereports 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:
{
"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 "https://api.scmm.app/api/item/prices"{
"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 "https://api.scmm.app/api/item/prices?markets=Skinport,DMarket"{
"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
marketTypevalues you see in any response. Unknown names are ignored rather than rejected, so a typo returns200with fewer offers, not a400. currencyworks here like everywhere else —?currency=EURconverts every monetary field server-side.- Key on
nameHash.classIdis also returned, which is what the v1 version of this endpoint used as itsid. - 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 "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-summaryis its companion: the market’s own completed-sale statistics, where the history endpoint reports listing prices. They are different quantities and are named differently —avgSalePriceagainstavgListingPrice.- 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.
| Limit | Applies to | Why |
|---|---|---|
| 100 / minute | Every endpoint not listed below. | The default. |
| 30 / minute | GET /api/search | Five uncached full-text scans per call. |
| 10 / minute | GET /api/item/prices | 1.3 MB uncached — one call is the whole catalogue. |
| 10 / minute | GET /api/marketplace/{market}/prices/history, GET /api/marketplace/{market}/sales-summary | A market’s whole catalogue in one response; each route counts separately. |
| 10 / minute | POST /api/inventory/calculate | Resolves a Steam profile on our quota. |
| 60 / minute | POST /api/workshop/file/{workshopFileId} | Sized for the 3D viewer’s own polling. |
| 20 / minute | GET /api/workshop/file/{workshopFileId}/download | Streams an archive averaging ~9 MB. |
| 30 / minute | GET /api/social/og/store.png, GET /api/social/og/item/{id}.png | Renders an image; each route counts separately. |
| exempt | GET /api/healthcheck | Uptime 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.
| Header | On | What it is |
|---|---|---|
| X-RateLimit-Limit | Every response | The budget for the endpoint you just called — the number in the table above. |
| X-RateLimit-Remaining | Every response | What is left of it in the current window. |
| X-RateLimit-Reset | Every response | Seconds remaining until the window rolls and the budget refills. |
| Retry-After | 429 only | Seconds 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.