API reference
HTTPS and JSON access to the catalog, live listings and prices. Buy listings for your customers and ship them as Steam trade offers, trade your own items with our bots, and follow it all with signed webhooks or the MCP server.
Overview
Everything is under this base URL and answers with JSON. Money is in USD, as decimal strings. The OpenAPI document describes every field, and the MCP endpoint offers the same API to AI agents.
Authentication
Send your key as a bearer token. Keys start with sk_ and are shown once, when you create them. Each key carries scopes, and every endpoint except /v1/public/me needs one of them. Create a key under Account → Developers.
deposits:write, orders:write, trades:write move money or items. Creating a key with one takes a two-factor code from your account; a key without them can only read.
curl https://api.cs2sell.com/v1/public/me \ -H "Authorization: Bearer sk_…"
{
"api_key_id": "6f1c2d3e-…",
"user_id": "0b7e9a41-…",
"scopes": ["catalog:read", "listings:read", "orders:read", "orders:write"],
"rate_limit_per_minute": 120
}Endpoints
The scope each endpoint needs is in the middle column.
| Endpoint | Scope | Returns |
|---|---|---|
| GET /v1/public/me | any key | The key you're calling with: scopes and rate limit. |
| GET /v1/public/catalog | catalog:read | Search the item catalog.paged |
| GET /v1/public/items/{id} | catalog:read | One item with its variants. |
| GET /v1/public/prices/{variant_id} | catalog:read | A variant's prices across marketplaces. |
| GET /v1/public/marketplaces | catalog:read | The marketplaces we track prices on. |
| GET /v1/public/listings | listings:read | Active listings on CS2Sell, with filters.paged |
| GET /v1/public/listings/lowest | listings:read | The cheapest listing and the number listed of every item on sale, each Doppler phase apart. Not paged; refreshed once a minute. |
| GET /v1/public/listings/stock | listings:read | Partner stock you can buy for a trade URL, with floats, stickers, inspect links and the Buy page's filters.paged |
| POST /v1/public/orders | orders:write | Buy instant listings, shipped to a trade URL. |
| GET /v1/public/orders | orders:read | Your orders, with each item's delivery.paged |
| GET /v1/public/orders/{id} | orders:read | One order and its deliveries. |
| GET /v1/public/wallet/balance | wallet:read | Your available and pending balance. |
| GET /v1/public/inventory | trades:read | Your Steam inventory, with what the bots pay per item. |
| POST /v1/public/inventory/refresh | trades:read | Re-read your inventory from Steam. |
| POST /v1/public/trades | trades:write | Trade with the bots: sell items, take listings. |
| GET /v1/public/trades | trades:read | Your trades with our bots. |
| GET /v1/public/trades/{id} | trades:read | One bot trade. |
| POST /v1/public/trades/{id}/refresh | trades:write | Ask the bot for the offer's state now. |
| POST /v1/public/trades/{id}/cancel | trades:write | Withdraw an open trade offer. |
| POST /v1/public/deposits/inventory | deposits:read | Your customer's Steam inventory, with what depositing each item pays you. |
| POST /v1/public/deposits | deposits:write | Deposit your customer's items: a Steam trade offer per item, paid to your balance. |
| GET /v1/public/deposits | deposits:read | Your deposits, with each item's trade.paged |
| GET /v1/public/deposits/{id} | deposits:read | One deposit and its items. |
| POST /v1/public/deposits/{id}/refresh | deposits:write | Ask for the state of a deposit's open items now. |
Buying for customers
Instant listings ship from our stock as a Steam trade offer to any trade URL you name, so you can sell them on your own site. The price comes out of your purchase balance (deposit_usd) first, then your store balance, when you place the order (trade balance, from sales still inside Steam's trade protection, can't pay for API orders); an item that can't be delivered is refunded to the balance it was paid from.
1. Find instant listings
curl "https://api.cs2sell.com/v1/public/listings?q=AK-47%20%7C%20Redline&instant=true&sort=price_asc" \ -H "Authorization: Bearer sk_…"
2. Place the order
trade_url is your customer's. external_id is your own order id: send the same one again after a timeout and you get the same order back (200) instead of a second purchase. max_total_usd caps what the order may cost, and max_price_usd can cap each item.
curl https://api.cs2sell.com/v1/public/orders \
-H "Authorization: Bearer sk_…" \
-H "Content-Type: application/json" \
-d '{
"listing_ids": ["5d1b2a90-7c3e-4b8e-9f0a-6a2c4e8d1f37"],
"trade_url": "https://steamcommunity.com/tradeoffer/new/?partner=99887766&token=AbCdEf12",
"external_id": "order-1042",
"max_total_usd": 25.00
}'HTTP/1.1 201 Created
{
"id": "c2e4f6a8-1b3d-4f5a-8c7e-9d0b2a4c6e81",
"external_id": "order-1042",
"status": "awaiting_delivery",
"source": "api",
"total_usd": "24.15",
"steam_id": "76561198060153494",
"items": [{
"listing_id": "5d1b2a90-7c3e-4b8e-9f0a-6a2c4e8d1f37",
"market_hash_name": "AK-47 | Redline (Field-Tested)",
"price_usd": "24.15",
"delivery": { "state": "queued", "offer_id": null, "offer_url": null, "hold_end_date": null, "reason": null, "refunded_usd": null }
}]
}3. Follow the delivery
Each item has its own trade offer. Poll GET /v1/public/orders/{id} or subscribe to the delivery.* webhooks; an item moves through these states:
- queued
- Paid; the offer is being prepared.
- initiated
- The item is confirmed for this order.
- pending
- The trade offer is being created.
- active
- The trade offer was sent; your customer accepts it in Steam.
- hold
- Accepted. Steam's trade protection holds the item until hold_end_date.
- completed
- Delivered.
- failed
- The offer was declined, expired or failed. Refunded.
- canceled
- Cancelled before delivery. Refunded.
- reverted
- Reversed on Steam after delivery. Refunded.
Only keys with orders:write can buy. An order holds up to 20 listings, all of them instant; anything else is refused before any money moves.
Trading with the bots
Sell items from your own Steam inventory to our bots for balance (give), take bot listings (get), or both in one trade. GET /v1/public/inventory shows what the bots pay per item (trade_in_usd). A bot sends a trade offer to your account; accept it in Steam. What you take is paid from your purchase and store balance and held until the trade ends (trade balance can't pay for it over the API); what the bots pay for your items arrives as trade balance. Set min_receive_usd or max_pay_usd so a price change can't surprise you, and external_id to make the call safe to retry.
curl https://api.cs2sell.com/v1/public/trades \
-H "Authorization: Bearer sk_…" \
-H "Content-Type: application/json" \
-d '{
"give": ["9a3c1e70-…"],
"external_id": "sell-77",
"min_receive_usd": 40.00
}'A trade is pending_offer, then awaiting_partner once the offer is out, and ends completed, declined, expired, cancelled or failed. The trade.offer_sent, trade.completed and trade.failed webhooks report each step.
Deposits for your customers
For accounts we've enabled for partner deposits: sell your customers' items for your balance. POST /v1/public/deposits/inventory with the customer's trade URL lists their inventory with what each item pays you (payout_usd). POST /v1/public/deposits with the items and the payouts you showed: a lower quote now refuses it (partner_deposits.price_changed), so you can show the customer the new value. We send the customer a Steam trade offer per item. external_id makes the call safe to retry.
curl https://api.cs2sell.com/v1/public/deposits \
-H "Authorization: Bearer sk_…" \
-H "Content-Type: application/json" \
-d '{
"trade_url": "https://steamcommunity.com/tradeoffer/new/?partner=99887766&token=AbCdEf12",
"external_id": "deposit-311",
"items": [{ "asset_id": "27144320525", "price_usd": "38.00" }]
}'An item is created, then offer_sent; once the customer accepts it is accepted and its payout is your trade balance while Steam's trade protection runs, then completed and store balance. failed and cancelled pay nothing; reversed (or a failure after the accept) takes the payout back. The deposit.* webhooks report each step; credit your customer at deposit.completed.
Paging
Lists take ?page= (from 1) and ?page_size= (default 50, at most 100) and answer with this envelope:
{
"items": [ … ],
"page": 1,
"page_size": 50,
"total": 1284
}Rate limits
Every key gets 120 requests per minute for each scope separately, so polling the catalog doesn't eat into your order checks. Every response says where you stand.
- X-RateLimit-Limit
- Requests allowed per minute for this scope.
- X-RateLimit-Remaining
- Requests left in the current minute.
- X-RateLimit-Reset
- Seconds until the minute resets.
- Retry-After
- Sent with a 429: seconds to wait before retrying.
Errors
Errors use the HTTP status plus a stable code you can match on. params fills the placeholders of the code's message, and validation errors add a fields list.
HTTP/1.1 409 Conflict
{
"error": {
"code": "public_orders.over_max_total",
"params": { "total": "31.50", "max": "30.00" }
}
}Webhooks
Add an endpoint and pick the events you want. Each event is POSTed as JSON and signed with your endpoint's secret (whsec_…). Add one under Account → Developers.
Events
- listing.created
- One of your listings went live
- listing.sold
- One of your listings sold
- listing.cancelled
- One of your listings was cancelled, by you or by us
- order.completed
- An order of yours was delivered
- order.refunded
- An order of yours was refunded
- delivery.offer_sent
- A bought item's Steam trade offer was sent
- delivery.hold
- A bought item's offer was accepted and is in Steam's trade protection
- delivery.completed
- A bought item was delivered
- delivery.failed
- A bought item's offer was declined, expired or failed; refunded
- delivery.reversed
- A delivered item was reversed on Steam; refunded
- trade.offer_sent
- A bot sent you a trade offer
- trade.completed
- A bot or P2P trade finished
- trade.failed
- A bot trade was declined, expired, cancelled or failed
- trade.reversed
- A trade was reversed on Steam
- deposit.offer_sent
- A deposited item's Steam trade offer was sent to your customer
- deposit.accepted
- Your customer accepted a deposit offer; paid as trade balance during Steam's trade protection
- deposit.completed
- A deposited item's trade protection ended; paid as store balance
- deposit.failed
- A deposit offer was declined, expired, cancelled or failed; nothing paid
- deposit.reversed
- A deposited item was reversed after the accept; its payout was taken back
- wallet.deposit
- A deposit was credited
- wallet.withdrawal
- A withdrawal was sent or failed
Headers
- X-CS2Sell-Signature
- sha256= followed by the hex HMAC-SHA256 of the raw body, keyed with your secret.
- X-CS2Sell-Event
- The event name, also in the body.
- X-CS2Sell-Delivery-Id
- Unique per delivery and the same on every retry; use it to drop duplicates.
- X-CS2Sell-Timestamp
- Unix time this attempt was sent.
Verifying a delivery
Compute the HMAC over the raw body before parsing it, and compare in constant time. With the secret whsec_example, this body signs to:
X-CS2Sell-Signature: sha256=4698a9241a98bcd285a91351115433196163384afd8eb02dc2ad26f7b80c1d16 {"id":"0b9c6f2e-4f7a-4c55-9a43-2d6f1f0e8a11","event":"delivery.completed","created_at":"2026-10-08T12:00:00Z","data":{"order_id":"c2e4f6a8-1b3d-4f5a-8c7e-9d0b2a4c6e81","external_id":"order-1042","listing_id":"5d1b2a90-7c3e-4b8e-9f0a-6a2c4e8d1f37","state":"completed","offer_id":"7100001","offer_url":"https://steamcommunity.com/tradeoffer/7100001/","hold_end_date":null,"reason":null,"refunded_usd":null}}
Answer with any 2xx within 10 seconds. Anything else is retried after 30 seconds, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours; after the sixth failure the endpoint is switched off and you're notified. Endpoints must be public http or https URLs, and redirects aren't followed.
Send test on an endpoint under Account → Developers queues a signed ping event, so you can check your receiver and your signature check.
MCP server
The same API is a Model Context Protocol server at /v1/mcp, so AI agents can search listings, buy, and track orders and trades. Add it to your client with one of your keys:
{
"mcpServers": {
"cs2sell": {
"url": "https://api.cs2sell.com/v1/mcp",
"headers": {
"Authorization": "Bearer sk_…"
}
}
}
}It speaks the 2026-07-28 revision and the initialize handshake of 2025-03-26 to 2025-11-25 over Streamable HTTP, answers in JSON and keeps no sessions. tools/list only offers the tools your key's scopes allow.
| Tool | Scope | Returns |
|---|---|---|
| search_listings | listings:read | Search active listings; instant ones by default. |
| search_catalog | catalog:read | Search the item catalog. |
| get_item | catalog:read | One catalog item. |
| get_prices | catalog:read | A variant's prices across marketplaces. |
| get_balance | wallet:read | Your balance. |
| list_orders | orders:read | Your orders with their deliveries. |
| get_order | orders:read | One order and each item's trade offer. |
| buy_listings | orders:write | Buy instant listings with your purchase and store balance and ship them to a trade URL. |
| get_inventory | trades:read | Your inventory with what the bots pay. |
| refresh_inventory | trades:read | Re-read your inventory from Steam. |
| list_trades | trades:read | Your bot trades. |
| get_trade | trades:read | One bot trade. |
| create_trade | trades:write | Sell items to the bots or take bot listings. |
| refresh_trade | trades:write | Check a trade offer's state now. |
| cancel_trade | trades:write | Withdraw an open trade offer. |
Node.js quickstart
There's no SDK to install: the API is plain HTTPS and JSON. Each quickstart reads /v1/public/me and verifies a webhook signature.
// Node.js 18+: fetch and crypto are built in.
import { createHmac, timingSafeEqual } from 'node:crypto';
const res = await fetch('https://api.cs2sell.com/v1/public/me', {
headers: { Authorization: `Bearer ${process.env.CS2SELL_API_KEY}` },
});
const body = await res.json();
if (!res.ok) throw new Error(body.error.code);
console.log(body.scopes, res.headers.get('x-ratelimit-remaining'));
// Verify a webhook against the raw body, before parsing it.
export function verify(rawBody, signature, secret) {
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature ?? '');
return a.length === b.length && timingSafeEqual(a, b);
}Python quickstart
# pip install requests
import hashlib, hmac, os
import requests
r = requests.get(
"https://api.cs2sell.com/v1/public/me",
headers={"Authorization": f"Bearer {os.environ['CS2SELL_API_KEY']}"},
timeout=10,
)
r.raise_for_status()
print(r.json()["scopes"], r.headers["X-RateLimit-Remaining"])
def verify(raw_body: bytes, signature: str, secret: str) -> bool:
"""Check X-CS2Sell-Signature against the raw request body."""
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")Go quickstart
package cs2sell
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"os"
)
type Me struct {
Scopes []string `json:"scopes"`
RateLimitPerMinute int `json:"rate_limit_per_minute"`
}
func FetchMe() (*Me, error) {
req, err := http.NewRequest("GET", "https://api.cs2sell.com/v1/public/me", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("CS2SELL_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("cs2sell: %s", res.Status)
}
var me Me
return &me, json.NewDecoder(res.Body).Decode(&me)
}
// Verify checks X-CS2Sell-Signature against the raw request body.
func Verify(body []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}