Endpoints
Market
Listings, buy orders, sales history and deliveries on Steam, CSFloat, market.csgo, Skin.Land, Skinport, and DMarket.
Everything the Market pages do, over HTTP. The venue is part of the path: every route lives under /api/v1/market/{venue}/, where venue is steam, csfloat, marketcsgo, skinland, dmarket, skinport or assetpay.
AssetPay behaves differently from the other listing venues and it shows in the responses. Its listings live on a feed AssetPay reads rather than on AssetPay's own servers, so cancelling one and changing its price both take effect immediately and answer 200 with listings_affected, not 202 with a task id. Bulk repricing there accepts exact only. POST /market/assetpay/listings/sync is a 404: there is nothing remote to sync against, our own database is the record. AssetPay also charges nothing on a pool sale, so receive_cents and buyer_pays_cents on those listings are always the same number.
There is no default venue, so no request can quietly read Steam rows while you believed it was reading CSFloat. An unknown venue answers 404 with code unknown_venue.
Two scopes cover the domain. market.read for the listings, orders, history, sales and stats. market.write for everything that changes them.
Venues
The venues do not carry the same surfaces. A surface a venue genuinely lacks answers 404 with code venue_unsupported, not 403: the resource does not exist there, it is not forbidden there.
| Surface | steam |
csfloat |
marketcsgo |
skinland |
dmarket |
skinport |
assetpay |
|---|---|---|---|---|---|---|---|
| Stats | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Listings: list, cancel | Yes | Yes | Yes | No | Yes | Yes | Yes |
| Listings: sync | Yes | Yes | Yes | No | Yes | Yes | No |
| Reprice a listing | No | Yes | Yes | No | Yes | Yes | Yes |
| Automatic repricing rules | No | Yes | Yes | No | Yes | No | No |
| Inventory (custody, not listed) | No | No | No | No | No | Yes | No |
| Buy orders | Yes | No | No | No | No | No | No |
| Sales history | Yes | Yes | Yes | No | Yes | Yes | Yes |
| Sales (trades) | No | Yes | Yes | Yes | No | No | Yes |
| Deliver or retry a sale | No | Yes | Yes | No | No | No | No |
| Accept a sale | No | Yes | No | No | No | No | No |
| Cancel a sale | No | Yes | No | No | No | No | No |
| Withdraw unsold inventory | No | No | No | No | Yes | No | No |
| Relist unsold inventory | No | No | No | No | Yes | No | No |
| Earnings and payouts | No | No | No | No | No | No | Yes |
Steam fixes a listing's price at creation, so there is nothing to reprice. Every venue but Steam is sell-only, so they have no buy orders. Steam settles a sale internally the moment it happens, so it has no delivery stage: on CSFloat and market.csgo the marketplace only brokers the deal and you still have to send the item.
Skin.Land has no listings at all. It is a buyer of record: it quotes a fixed price, buys the items outright, and its own bot sends the trade offer, which SteamLabs accepts for you inside the same operation. So there is nothing to list, no price to choose, nothing to reprice or delist, and no delivery left outstanding for you to act on. It has no sales history either, for the same reason: history records listing events, and there are none. What it does have is sales, which you read exactly like the other venues', and terminal ones stay there rather than moving elsewhere, so /market/skinland/trades is the complete record.
AssetPay is the only venue whose money lands at SteamLabs. Everywhere else a sale pays into a wallet you hold at the venue and you withdraw it there. AssetPay pays into one SteamLabs merchant balance, and SteamLabs keeps your share of it, so it is the only venue with earnings and payouts surfaces. It also has no automatic repricing (we are the sole supplier of our own pool, so there is nothing to undercut) and no listing sync (our database is the record). Its sales come in two kinds, instant and pool, which the source field tells apart.
DMarket has listings and history, and no Sales tab. A sale on DMarket does not leave a delivery for you to send. /market/dmarket/trades answers 404 with venue_unsupported. Withdraw unsold inventory with POST /api/v1/market/dmarket/withdraw. Relist it without depositing again with POST /api/v1/market/dmarket/relist. Other venues answer 404 with venue_unsupported there.
What your plan gates
Your subscription plan controls which venues you may sell on. It never gates reads, and it never gates pulling your own items back.
Listing, delisting, manual repricing, syncing, reading history, withdrawing Skinport custody items and delivering a sale all work on every venue, whatever your plan says. Someone who downgraded still has real items on a real marketplace, and an API that hid them would strand inventory they cannot get back. Scheduled automatic repricing checks pause while the user's plan does not include the listing's venue; the rules remain stored and resume when access returns.
Writes here that can still come back 403 plan_limit_reached:
- Placing a buy order. Buying spends the Steam venue entitlement, so it needs
steamin your plan'sallowed_marketplaces, plusplace_buy_orderinallowed_task_types. - Syncing on Steam. A Steam refresh is a real task, so it needs
sync_market_listings(orsync_market_history) inallowed_task_types. The CSFloat and market.csgo refreshes are poller requests and are never gated. DMarket sync is a task too, and it is also never gated: it only refreshes listings and history you already have. - Relisting from Skinport inventory. Relist puts a new listing up, so it needs
skinportin your plan'sallowed_marketplaces. Withdraw does not: it only asks Skinport to send a Steam offer.
Read plan from GET /api/v1/me at startup and you will rarely meet either. See Errors for the response shape.
Venue stats
The headline numbers above a venue's tables.
/api/v1/market/{venue}/statsOpen listings, recent money moved, and where your balance sits on this venue.
Requires market.read.
curl https://dashboard.steamlabs.dev/api/v1/market/steam/stats \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"venue": "steam",
"window_days": 30,
"listings": {
"open_count": 184,
"open_value_cents": { "EUR": 412300, "USD": 88150 },
"sold_recent_cents": { "EUR": 96420, "USD": 12005 }
},
"buy_orders": {
"open_count": 12,
"committed_value_cents": { "EUR": 31400 },
"bought_recent_cents": { "EUR": 18720 }
},
"wallet_cents": { "EUR": 24150, "PLN": 9800 },
"marketplace_balance": null
}On CSFloat and market.csgo the shape stays the same, with buy_orders and wallet_cents null and the venue-side balance filled in:
{
"venue": "csfloat",
"window_days": 30,
"listings": {
"open_count": 62,
"open_value_cents": { "USD": 148900 },
"sold_recent_cents": { "USD": 51230 }
},
"buy_orders": null,
"wallet_cents": null,
"marketplace_balance": {
"available_cents": { "USD": 21400 },
"pending_cents": { "USD": 6800 }
}
}On Skin.Land the same keys mean the venue's own lifecycle, because it has no listings: open_count and open_value_cents cover sales still moving (queued, awaiting accept, delivering, or locked in settlement), and sold_recent_cents sums the baskets whose trade offer went through in the window. A queued sale has no price yet (Skin.Land quotes the basket at sell time), so the open value is only ever a partial figure.
window_days is 30, and it applies to sold_recent_cents and bought_recent_cents only. Sales are summed from history events rather than from listings, because an item that sells instantly by crossing a standing buy order never lingers as an open listing.
committed_value_cents is what your open buy orders still hold: per-unit price times the quantity not yet filled.
List listings
What you have up for sale on this venue, and what has happened to it.
/api/v1/market/{venue}/listingsYour listings on this venue, paginated and filterable.
Requires market.read.
| Parameter | Type | Description |
|---|---|---|
state |
string | open, sold, or canceled. open covers pending_confirmation, active and on_hold. canceled covers both a listing you pulled and one that vanished from an authoritative sync |
account_id |
uuid | Only listings from this Steam account. Must be an account you own, otherwise 422 |
origin |
string | steamlabs for listings this platform created, external for ones found already on the venue |
game |
string | cs2, tf2, or steam |
appid |
integer | The Steam app id, if you already hold the number. Wins over game when both are sent |
search |
string | Matches the item name or its market hash name |
sort |
string | listed_at (default), resolved_at, buyer_pays_cents, created_at |
direction |
string | asc or desc (default) |
per_page |
integer | 50 by default, 200 at most |
curl "https://dashboard.steamlabs.dev/api/v1/market/steam/listings?state=open&game=cs2&per_page=1" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"data": [
{
"id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
"listing_id": "4258109377312946881",
"marketplace": "steam",
"origin": "steamlabs",
"state": "active",
"is_open": true,
"steam_account": {
"id": "019fb42e-9a7e-728d-b960-8b4c2162898c",
"username": "farm_017"
},
"asset_id": "38294011745",
"appid": 730,
"game": "cs2",
"context_id": "2",
"market_hash_name": "AK-47 | Redline (Field-Tested)",
"name": "AK-47 | Redline",
"icon_url": "https://community.fastly.steamstatic.com/economy/image/…",
"amount": 1,
"buyer_pays_cents": 1842,
"receive_cents": 1602,
"fee_cents": 240,
"sold_receive_cents": null,
"currency": "EUR",
"listed_at": "2026-07-28T11:40:02+00:00",
"instant_sell_after_at": null,
"resolved_at": null,
"auto_repricing": null,
"created_at": "2026-07-28T11:40:05+00:00"
}
],
"meta": { "page": 1, "per_page": 1, "total": 184, "last_page": 184 }
}buyer_pays_cents is what the listing shows a buyer. receive_cents is your net after the venue's fee, and sold_receive_cents fills in once it actually sells. is_open saves you mapping the six raw states yourself.
On AssetPay, instant_sell_after_at is when the listing gives up waiting and sells to AssetPay at the instant price (see Schedule an instant sell); null means it stays up until it sells. It is null on every other venue.
On CSFloat, auto_repricing is either null or the listing's captured rule. It includes enabled, interval_minutes, undercut_cents, floor_percent, the fixed floor_cents, and its last and next check timestamps.
Get listing price history
The current catalog prices and recent price changes behind the listing history panel.
/api/v1/market/{venue}/listings/{listing}/price-historyCurrent provider prices and a seven-day item price series for one listing.
Requires market.read.
curl "https://dashboard.steamlabs.dev/api/v1/market/csfloat/listings/019fb42e-9a61-70d2-818a-f6a56593f3a5/price-history" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"listing_id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
"item_id": "019fb421-5484-7c4c-a31b-e247db42df90",
"active_source": "csfloat",
"window": {
"days": 7,
"starts_on": "2026-08-05",
"ends_on": "2026-08-11"
},
"current_prices": [
{
"source": "csfloat",
"marketplace": "csfloat",
"price_cents": 1825,
"safe_price_cents": null,
"median_price_cents": null,
"min_price_cents": null,
"max_price_cents": null,
"lowest_sell_price_cents": 1799,
"highest_buy_price_cents": null,
"volume": 38,
"currency": "USD",
"fetched_at": "2026-08-11T12:04:18+00:00"
}
],
"series": [
{
"source": "csfloat",
"points": [
{ "recorded_on": "2026-08-05", "price_cents": 1610, "currency": "USD" },
{ "recorded_on": "2026-08-08", "price_cents": 1740, "currency": "USD" },
{ "recorded_on": "2026-08-11", "price_cents": 1825, "currency": "USD" }
]
}
],
"sales": {
"recent": [
{ "price_cents": 1790, "currency": "USD", "marketplace": "assetpay", "sold_at": "2026-08-10T15:22:08+00:00" }
],
"window": { "count": 1, "median_cents": 1790 }
}
}current_prices can contain Steam, CSFloat, market.csgo and AssetPay snapshots for the item, regardless of which venue owns the listing. active_source tells a client which series the dashboard highlights for this route.
History is change-only. When a price was already known before the seven-day window, the first point is carried forward to starts_on; the current snapshot extends the final step to ends_on. Draw the points as a stepped line rather than interpolating between them.
active_source is assetpay for an AssetPay listing. That series is what AssetPay pays for the item (its deposit price, recorded once a day when it moves), not what the item sells for on their store. sales is what pool-listed items actually sold for: platform-wide and anonymised (price, venue and time only), delivered sales only, and a reversed sale's print is removed again. recent holds the last five prints newest first; window counts the prints inside the seven-day window and gives their median.
The item link can be absent on older imported listings. Those listings still answer 200, with item_id: null, current_prices: [] and series: []. A listing belonging to another user or another venue answers the same plain 404 as the other single-listing routes.
Refresh listings
Re-read the venue for the accounts you name.
/api/v1/market/{venue}/listings/syncPull fresh listings, wallet and buy orders from the venue.
Requires market.write.
| Field | Type | Description |
|---|---|---|
account_ids |
uuid[] | Accounts to refresh, up to 1,000. Leave it out for the whole eligible fleet |
A sync is account-oriented, never listing-oriented: the venue is asked for an account's whole state at once. That is why an absent account_ids is safe here and nowhere else in this domain. It creates no listings, spends no money and moves no items.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/steam/listings/sync" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"account_ids":["019fb42e-9a7e-728d-b960-8b4c2162898c"]}'On Steam a refresh is a task, so you get a task to follow:
{
"task_id": "019fb440-1c22-73a4-9f0e-2b7c5d1e8a44",
"accounts_affected": 412,
"async": false,
"accounts_skipped": 18
}async is true above 500 accounts, where the batch is built by a background job. accounts_skipped counts accounts left out because they already had market work queued.
On CSFloat and market.csgo there is no task. Those venues use scheduled HTTP checks. A refresh requests a full check of the selected accounts, while preserving marketplace rate-limit delays. Repeated requests are combined. Refreshing does not keep idle Market.CSGO accounts online or put idle CSFloat accounts on the fastest schedule:
{
"task_ids": [],
"accounts_affected": 34,
"sync": "poll_requested"
}On Skinport (and Skin.Land) a refresh is a task again: { "task_ids": [...], "accounts_affected": N, "sync": "task_queued" }. SteamLabs also queues that same task about every 30 minutes for connected accounts that have gone stale, so a sale that happens on the venue website still lands without this call. A vanished Skinport listing is sold only when /api/transactions has a matching sale; otherwise it is canceled. A newly sold listing sends the same sold notification as a CSFloat poller tick.
Answers 422 nothing_eligible when nothing matched.
Cancel many listings
Pull a selection of listings off the marketplace.
/api/v1/market/{venue}/listings/cancelDelist many listings, grouped into one task per account.
Requires market.write.
| Field | Type | Description |
|---|---|---|
listing_ids |
uuid[] | The listings to pull, up to 1,000. Required unless you send filters |
filters |
object | The same filter keys the listings index accepts, resolved server-side. Required unless you send listing_ids |
Send one or the other, never neither. An empty body delisting your entire market is exactly the accident an API should refuse. See Bulk operations for the shared contract.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/csfloat/listings/cancel" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filters":{"state":"open","game":"cs2"}}'$result = Http::withToken($apiKey)
->post('https://dashboard.steamlabs.dev/api/v1/market/csfloat/listings/cancel', [
'filters' => ['state' => 'open', 'game' => 'cs2'],
])
->json();const response = await fetch('https://dashboard.steamlabs.dev/api/v1/market/csfloat/listings/cancel', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ filters: { state: 'open', game: 'cs2' } }),
});
const result = await response.json();result = requests.post(
"https://dashboard.steamlabs.dev/api/v1/market/csfloat/listings/cancel",
headers={"Authorization": f"Bearer {api_key}"},
json={"filters": {"state": "open", "game": "cs2"}},
).json()Answers 202, with one task per account rather than a single parent:
{
"task_ids": [
"019fb441-2c88-71ea-b0a3-8ce2f5d19b07",
"019fb441-3d19-7285-9c41-72b0e4a9d233"
],
"accounts_affected": 2,
"listings_affected": 47,
"accounts_busy": 1
}listings_affected is counted off the tasks that were really created, so an account the single-flight guard refused is reported in accounts_busy instead of being counted as queued.
Listings that are already closed are dropped rather than refused, so a sale that lands between reading the page and pressing the button does not fail the other forty-nine. If none of the selection is still open you get 422 nothing_to_cancel with matched.
Cancel one listing
/api/v1/market/{venue}/listings/{listing}/cancelPull one listing off the marketplace.
Requires market.write.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/steam/listings/019fb42e-9a61-70d2-818a-f6a56593f3a5/cancel" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"task_ids": ["019fb441-2c88-71ea-b0a3-8ce2f5d19b07"],
"accounts_affected": 1,
"listings_affected": 1
}202, not 204. The venue is reached by a worker holding the account's session, so the listing keeps its current state until the task comes back. Reporting 204 would tell you the item is already delisted, which is the lie that makes an integration relist it twice.
A listing that is no longer open answers 422 listing_not_open with its state. An account that already has market work in flight answers 409 account_busy; wait for that task and try again. A listing id that belongs to another venue answers a plain 404.
Reprice a listing
Change what a live listing asks a buyer to pay.
/api/v1/market/{venue}/listings/{listing}/priceSet a new buyer-pays price. CSFloat, market.csgo, DMarket, and Skinport.
Requires market.write.
| Field | Type | Description |
|---|---|---|
price_cents |
integer | The new buyer-pays price in cents. At least 1, at most 100000000 |
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/csfloat/listings/019fb42e-9a61-70d2-818a-f6a56593f3a5/price" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"price_cents":1795}'{
"task_ids": ["019fb442-8a10-7c33-b4d2-0f1e6c7a9b55"],
"accounts_affected": 1,
"listings_affected": 1,
"price_cents": 1795,
"repricing_paused": true
}Changing a CSFloat or market.csgo price manually pauses that listing's automatic rule. This prevents a scheduled check from replacing the price you just chose. Resume it through the rule endpoint when that is intentional.
/market/steam/listings/{id}/price answers 404 venue_unsupported. A Steam listing's price is fixed at creation, and the equivalent is a delist plus a fresh listing, which costs a fee and a new asset id. Doing that silently under a "change price" call would surprise every caller exactly once.
Reprice many listings
Change the prices of a whole selection at once, the API face of the listings table's "Change price of selected" bulk action.
/api/v1/market/{venue}/listings/priceReprice many listings, grouped into one task per account. CSFloat, market.csgo, DMarket, and Skinport.
Requires market.write.
| Field | Type | Description |
|---|---|---|
listing_ids |
uuid[] | The listings to reprice, up to 1,000. Required unless you send filters |
filters |
object | The same filter keys the listings index accepts, resolved server-side. Required unless you send listing_ids |
pricing.mode |
string | exact or undercut |
pricing.price_cents |
integer | exact only: the buyer-pays price applied to every selected listing. At least 1 |
pricing.undercut_cents |
integer | undercut only: how far under each item's lowest live listing to go. 1 to 10000 |
exact sets one price everywhere, which is what a selection of fifty identical cases wants. undercut prices each distinct item under its venue's lowest live listing, read from the same cached quotes the reprice modal shows; a listing whose item has no quote yet (or whose computed price would fall below one cent) is skipped and counted in skipped_no_quote.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/csfloat/listings/price" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filters":{"state":"open","search":"Revolution Case"},"pricing":{"mode":"exact","price_cents":20}}'$result = Http::withToken($apiKey)
->post('https://dashboard.steamlabs.dev/api/v1/market/csfloat/listings/price', [
'filters' => ['state' => 'open', 'search' => 'Revolution Case'],
'pricing' => ['mode' => 'exact', 'price_cents' => 20],
])
->json();const response = await fetch('https://dashboard.steamlabs.dev/api/v1/market/csfloat/listings/price', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
filters: { state: 'open', search: 'Revolution Case' },
pricing: { mode: 'exact', price_cents: 20 },
}),
});
const result = await response.json();result = requests.post(
"https://dashboard.steamlabs.dev/api/v1/market/csfloat/listings/price",
headers={"Authorization": f"Bearer {api_key}"},
json={
"filters": {"state": "open", "search": "Revolution Case"},
"pricing": {"mode": "undercut", "undercut_cents": 1},
},
).json()Answers 202, with one task per account:
{
"task_ids": [
"019fb441-2c88-71ea-b0a3-8ce2f5d19b07",
"019fb441-3d19-7285-9c41-72b0e4a9d233"
],
"accounts_affected": 2,
"listings_affected": 47,
"skipped_no_quote": 3,
"accounts_busy": 1
}Like the single reprice, every listing that gets a new price has its automatic-repricing rule paused, so a scheduled check does not immediately replace the price you just set. Closed listings are dropped rather than refused; if nothing in the selection is still open you get 422 nothing_to_reprice with matched. The selection is capped like the bulk delist: over 1,000 matched listings answers 422 bulk_limit_exceeded.
DMarket accepts exact only. It has no cached order book for undercut, so that mode answers 422 undercut_unsupported.
Schedule an instant sell
AssetPay only. Set or clear when a pool listing gives up waiting and sells to AssetPay at the instant price. The hourly check then takes the listing down and sells the item, provided the venue's instant sell unsold rule is switched on and AssetPay is paying at least its minimum for the item at that moment. Anything AssetPay does not take goes straight back up at its old price.
/api/v1/market/{venue}/listings/{listing}/instant-sellSet or clear one AssetPay listing's instant sell deadline.
Requires market.write. A local write, like an AssetPay reprice: it answers 200, not 202.
| Field | Type | Description |
|---|---|---|
after_days |
integer or null | Days from now, 1 to 365. null clears the deadline so the listing stays up until it sells |
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/assetpay/listings/019fb42e-9a61-70d2-818a-f6a56593f3a5/instant-sell" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"after_days":7}'{
"listings_affected": 1,
"after_days": 7,
"instant_sell_after_at": "2026-09-22T10:15:00+00:00"
}/api/v1/market/{venue}/listings/instant-sellThe same deadline for many AssetPay listings, by id or by filter.
Takes after_days plus the same listing_ids or filters selection as Cancel many listings. Listings that are no longer open are skipped and not counted.
Withdraw unsold inventory
Send unsold DMarket inventory back to Steam. Delisting leaves the item in DMarket inventory. This is the step that asks DMarket to trade it back. SteamLabs accepts that offer for you.
/api/v1/market/{venue}/withdrawQueue a withdraw of unsold DMarket inventory. DMarket only.
Requires market.write. Other venues answer 404 with venue_unsupported.
| Field | Type | Description |
|---|---|---|
account_ids |
uuid[] | Accounts to withdraw from, up to 1,000. Leave it out for every DMarket-ready account |
assets |
object[] | Specific in-market rows. Each needs asset_id, class_id, and game_id. Omit it and the worker finds unsold in-market items itself |
Named assets need exactly one account_id. Missing that is 422 account_required. No DMarket-ready account matching the selection is 422 nothing_eligible. An account that already has DMarket work in flight is counted busy like the other writes.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/dmarket/withdraw" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"account_ids":["019fb42e-9a7e-728d-b960-8b4c2162898c"]}'{
"task_ids": ["019fb441-2c88-71ea-b0a3-8ce2f5d19b07"],
"accounts_affected": 1,
"withdraw": "queued"
}202, not 204. The worker still has to talk to DMarket, then accept the Steam trade. Reporting 204 would tell you the items are already back.
This write is never plan-gated. A lapsed subscriber still owns those items.
List unsold inventory
List items that already sit in DMarket inventory and are not currently for sale. Delisting leaves the item in DMarket inventory. This is the step that puts it back on sale without depositing again.
/api/v1/market/{venue}/relistQueue a listing of unsold DMarket inventory. DMarket only.
Requires market.write. Other venues answer 404 with venue_unsupported. This write is plan-gated: it puts something new up for sale. A plan that does not include DMarket answers 403 with plan_limit_reached.
| Field | Type | Description |
|---|---|---|
account_ids |
uuid[] | Accounts to list from, up to 1,000. Leave it out for every DMarket-ready account |
assets |
object[] | Specific in-market rows. Each needs asset_id, appid, and market_hash_name. Optional context_id (defaults to 2) and reference_usd_cents. Omit assets and the worker finds unsold in-market items itself |
pricing |
object | Same modes as selling: undercut, reference_percent, manual, top_bid. Defaults to undercut by 1 cent |
Named assets need exactly one account_id. Missing that is 422 account_required. No DMarket-ready account matching the selection is 422 nothing_eligible. An account that already has DMarket work in flight is counted busy like the other writes.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/dmarket/relist" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"account_ids":["019fb42e-9a7e-728d-b960-8b4c2162898c"],"pricing":{"mode":"undercut","undercut_cents":1}}'{
"task_ids": ["019fb441-2c88-71ea-b0a3-8ce2f5d19b07"],
"accounts_affected": 1,
"relist": "queued"
}202, not 204. The worker still has to talk to DMarket. Reporting 204 would tell you the items are already listed.
Manage automatic repricing
Opt one open CSFloat, market.csgo, or DMarket listing into decrease-only competitive repricing.
/api/v1/market/{venue}/listings/{listing}/repricingCreate or replace the automatic repricing rule for one listing.
Requires market.write. PATCH on the same path changes selected fields, and DELETE disables the retained rule.
/api/v1/market/{venue}/listings/{listing}/repricingChange, pause, or resume an existing automatic repricing rule.
/api/v1/market/{venue}/listings/{listing}/repricingDisable an automatic repricing rule while retaining its history.
| Field | Type | Description |
|---|---|---|
enabled |
boolean | Pause or resume an existing rule. Used by PATCH |
interval_minutes |
integer | 15, 30, 60, or 240 |
undercut_cents |
integer | Cents below a true competing lowest price, 1 to 10,000 |
floor_percent |
integer | 1 to 100. Resolves to a fixed USD floor when the rule is enrolled |
curl -X PUT "https://dashboard.steamlabs.dev/api/v1/market/csfloat/listings/019fb42e-9a61-70d2-818a-f6a56593f3a5/repricing" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"interval_minutes":30,"undercut_cents":1,"floor_percent":70}'{
"data": {
"marketplace": "csfloat",
"strategy": "undercut",
"enabled": true,
"interval_minutes": 30,
"undercut_cents": 1,
"floor_percent": 70,
"reference_cents": 1842,
"floor_cents": 1289,
"reference_fetched_at": "2026-08-07T14:10:00+00:00",
"next_check_at": "2026-08-07T14:40:00+00:00",
"last_checked_at": null,
"last_repriced_at": null,
"last_outcome": null,
"last_error": null,
"pause_reason": null
}
}The checker only lowers a price. It compares the public lowest price with every copy of that item in your live stall, so it does not undercut your own managed or imported listings. A tie is left alone. A target below floor_cents is skipped and checked again later.
Scheduled checks are off for every account until the first explicit opt-in: creating or resuming a rule (here, from the dashboard, or through a listing created with auto_reprice) turns the venue's scheduling on. The per-venue master switch in Settings pauses all of a venue's rules again without deleting them.
Skinport inventory
Items sitting in Skinport custody and not currently listed. Cancel leaves them here. Relist puts them back on Skinport with no Steam offer. Withdraw asks Skinport to send a Steam trade offer and stops there: SteamLabs does not accept that offer.
Other venues answer 404 with code venue_unsupported.
/api/v1/market/{venue}/inventoryItems in Skinport custody that are not listed.
Requires market.read. Skinport only.
| Parameter | Type | Description |
|---|---|---|
account_id |
uuid | Only items from this Steam account. Must be one you own |
search |
string | Matches the item name, market hash name, or Skinport product id |
sort |
string | created_at (default), suggested_price_cents, name |
direction |
string | asc or desc (default) |
per_page |
integer | 50 by default, 200 at most |
curl "https://dashboard.steamlabs.dev/api/v1/market/skinport/inventory?per_page=1" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"data": [
{
"id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
"product_id": "59334964",
"skinport_asset_id": "693023720",
"steam_asset_id": "52808880736",
"market_hash_name": "Sealed Dead Hand Terminal",
"name": "Sealed Dead Hand Terminal",
"icon_url": null,
"appid": 730,
"suggested_price_cents": 82,
"currency": "EUR",
"lock_at": null,
"trade_locked": false,
"steam_account": { "id": "019fb42e-9a7e-728d-b960-8b4c2162898c", "username": "farm_014" },
"created_at": "2026-08-19T18:02:11+00:00",
"updated_at": "2026-08-19T18:02:11+00:00"
}
],
"meta": { "page": 1, "per_page": 1, "total": 1, "last_page": 1 }
}product_id is Skinport's own id for the held item. Relist and withdraw key off that, not the Steam asset id and not a listing saleId. trade_locked is true while lock_at is still in the future. A CS2 trade lock blocks withdraw.
/api/v1/market/{venue}/inventory/relistList held Skinport items at one price, with no Steam offer.
Requires market.write. Skinport only. This puts a new listing up, so it is plan-gated: a plan without skinport answers 403 plan_limit_reached.
| Field | Type | Description |
|---|---|---|
item_ids |
uuid[] | Skinport inventory row ids from the list above |
price_cents |
integer | Buyer-pays price in EUR cents, at least 2 |
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/skinport/inventory/relist" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"item_ids":["019fb42e-9a61-70d2-818a-f6a56593f3a5"],"price_cents":4999}'Answers 202 with one task per account:
{
"task_ids": ["019fb445-77c3-7f21-a0b6-9c4e0d1a8b62"],
"accounts_affected": 1,
"items_affected": 1,
"accounts_busy": 0
}IDs that are not yours are not matched. If none of the ids exist, the call answers 404 with code not_found.
/api/v1/market/{venue}/inventory/withdrawAsk Skinport to send a Steam withdraw offer. SteamLabs does not accept it.
Requires market.write. Skinport only. Not plan-gated: a lapsed subscriber must still be able to pull items back.
| Field | Type | Description |
|---|---|---|
item_ids |
uuid[] | Skinport inventory row ids from the list above |
Trade-locked rows are skipped. If every selected row is locked, the call answers 422 with code nothing_withdrawable.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/skinport/inventory/withdraw" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"item_ids":["019fb42e-9a61-70d2-818a-f6a56593f3a5"]}'The queued task asks Skinport to send the Steam offer. Steam offerId is null at that send. SteamLabs does not accept or mobile-confirm it.
List buy orders
Standing bids resting on the Steam market, waiting to be filled.
/api/v1/market/{venue}/buy-ordersYour buy orders. Steam only.
Requires market.read.
| Parameter | Type | Description |
|---|---|---|
state |
string | open, canceled, or closed. closed means it vanished from an authoritative sync: filled or cancelled outside our sight |
account_id |
uuid | Only orders from this Steam account. Must be one you own |
game |
string | cs2, tf2, or steam |
appid |
integer | The Steam app id. Wins over game when both are sent |
search |
string | Matches the item name or its market hash name |
sort |
string | created_at (default), buyer_pays_per_unit_cents, closed_at |
direction |
string | asc or desc (default) |
per_page |
integer | 50 by default, 200 at most |
curl "https://dashboard.steamlabs.dev/api/v1/market/steam/buy-orders?state=open&per_page=1" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"data": [
{
"id": "019fb443-6b71-70aa-8f3c-51d0c9e4a712",
"buy_order_id": "6412887301",
"state": "open",
"steam_account": {
"id": "019fb42e-9a7e-728d-b960-8b4c2162898c",
"username": "farm_017"
},
"appid": 730,
"game": "cs2",
"market_hash_name": "Fracture Case",
"name": "Fracture Case",
"icon_url": "https://community.fastly.steamstatic.com/economy/image/…",
"buyer_pays_per_unit_cents": 21,
"currency": "EUR",
"quantity": 250,
"quantity_remaining": 187,
"quantity_filled": 63,
"closed_at": null,
"created_at": "2026-07-24T08:12:44+00:00"
}
],
"meta": { "page": 1, "per_page": 1, "total": 12, "last_page": 12 }
}On CSFloat and market.csgo this answers 404 venue_unsupported. Those venues are sell-only.
Place a buy order
Rest one bid across a selection of accounts.
/api/v1/market/{venue}/buy-ordersPlace one buy order across a selection of accounts. Steam only.
Requires market.write.
| Field | Type | Description |
|---|---|---|
market_hash_name |
string | The exact market hash name to bid on. Up to 255 characters |
game |
string | cs2, tf2, or steam |
prices_cents |
object | Per-unit buyer-pays price in cents, keyed by ISO wallet currency. At least one entry, each at least 3 cents |
quantity |
integer | Units per order. 1 by default, 1,000 at most |
account_ids |
uuid[] | Accounts to place on, up to 1,000. Leave it out for the whole eligible fleet |
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/steam/buy-orders" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"game":"cs2","market_hash_name":"Fracture Case","quantity":250,"prices_cents":{"EUR":21,"PLN":92}}'$result = Http::withToken($apiKey)
->withHeaders(['Idempotency-Key' => (string) Str::uuid()])
->post('https://dashboard.steamlabs.dev/api/v1/market/steam/buy-orders', [
'game' => 'cs2',
'market_hash_name' => 'Fracture Case',
'quantity' => 250,
'prices_cents' => ['EUR' => 21, 'PLN' => 92],
])
->json();const response = await fetch('https://dashboard.steamlabs.dev/api/v1/market/steam/buy-orders', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify({
game: 'cs2',
market_hash_name: 'Fracture Case',
quantity: 250,
prices_cents: { EUR: 21, PLN: 92 },
}),
});
const result = await response.json();result = requests.post(
"https://dashboard.steamlabs.dev/api/v1/market/steam/buy-orders",
headers={
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"game": "cs2",
"market_hash_name": "Fracture Case",
"quantity": 250,
"prices_cents": {"EUR": 21, "PLN": 92},
},
).json()Answers 202, one task per currency group:
{
"task_ids": [
"019fb444-9e02-72c1-b7a8-3d5e1f0b6c49",
"019fb444-9e33-7410-84be-c1a72d9f5e80"
],
"accounts_affected": 128,
"async": false,
"accounts_underfunded": 14,
"accounts_busy": 2,
"accounts_unknown_currency": 3,
"selection_currencies": ["EUR", "PLN"]
}202 rather than 201 is not a technicality: nothing exists yet. Steam only knows about an order once a worker has logged the account in and placed it. The order rows appear later, through the sync that confirms them, so watch the returned tasks to know when.
The two skip counts need different fixes. accounts_underfunded means Steam's cap of ten times the wallet balance would reject the order, so add funds or lower the price. accounts_busy means the account already had market work in flight, and the same submission works again in a minute. accounts_unknown_currency counts accounts whose wallet currency has never been synced, so they could not be priced at all.
Only accounts that can actually place are targeted, named or not: market access allowed, an identity secret stored, and a billing address on file.
Refusals:
| Status | Code | Meaning |
|---|---|---|
422 |
missing_price_for_currency |
A currency in the selection had no price. Adds missing_currencies and selection_currencies. Nothing was placed |
422 |
nothing_eligible |
No selected account could fund the order. Adds the four counts above |
403 |
plan_limit_reached |
Your plan does not sell on Steam (allowed_marketplaces) or does not include place_buy_order (allowed_task_types) |
Refresh buy orders
/api/v1/market/{venue}/buy-orders/syncPull fresh buy orders from Steam. Steam only.
Requires market.write.
Takes the same optional account_ids as the listings refresh and answers the same 202 envelope with task_id, accounts_affected, async and accounts_skipped.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/steam/buy-orders/sync" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'Cancel many buy orders
/api/v1/market/{venue}/buy-orders/cancelCancel many resting orders, grouped into one task per account. Steam only.
Requires market.write.
| Field | Type | Description |
|---|---|---|
buy_order_ids |
uuid[] | The orders to cancel, up to 1,000. Required unless you send filters |
filters |
object | The same filter keys the buy orders index accepts. Required unless you send buy_order_ids |
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/steam/buy-orders/cancel" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filters":{"state":"open","search":"Fracture Case"}}'{
"task_ids": ["019fb445-4411-7b90-9d0c-6e2a5f38c114"],
"accounts_affected": 1,
"buy_orders_affected": 9,
"accounts_busy": 0
}Cancelling frees the wallet balance an open order commits, so it is never plan-gated and never needs an Idempotency-Key. Its target state is "not resting", and a repeat is a no-op.
Orders that are no longer open are dropped. If none of the selection is still open you get 422 nothing_to_cancel with matched. A filter matching more than 1,000 orders answers 422 bulk_limit_exceeded.
Cancel one buy order
/api/v1/market/{venue}/buy-orders/{buyOrder}/cancelCancel one resting order. Steam only.
Requires market.write.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/steam/buy-orders/019fb443-6b71-70aa-8f3c-51d0c9e4a712/cancel" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"task_ids": ["019fb445-77c3-7f21-a0b6-9c4e0d1a8b62"],
"accounts_affected": 1,
"buy_orders_affected": 1
}An order that is not open answers 422 buy_order_not_open with its state. An account with market work already in flight answers 409 account_busy.
Sales history
What has already happened on this venue: listed, cancelled, sold, bought, plus the marketplace-only lifecycle steps.
Steam, CSFloat, market.csgo, DMarket and Skinport. skinland answers 404 with code venue_unsupported, on both endpoints below: these are listing events, and it has no listings. Read /market/skinland/trades instead, which keeps terminal sales alongside live ones.
/api/v1/market/{venue}/historyMarket history events on this venue, newest first.
Requires market.read.
| Parameter | Type | Description |
|---|---|---|
event_type |
integer | One of the codes below |
account_id |
uuid | Only events from this Steam account. Must be one you own |
listing_id |
string | Only events for this venue listing. Pair with account_id when the same external id can exist on more than one account |
game |
string | cs2, tf2, or steam |
appid |
integer | The Steam app id. Wins over game when both are sent |
search |
string | Matches the item name or its market hash name |
sort |
string | happened_at (default), created_at |
direction |
string | asc or desc (default) |
per_page |
integer | 50 by default, 200 at most |
event_type is the stored integer. Codes 1 to 4 are Steam's own history codes, 5 is a repricing detected by SteamLabs, and 10 to 12 are marketplace lifecycle transitions.
| Code | event_type_key |
Meaning |
|---|---|---|
1 |
listing_created |
An item went up for sale |
2 |
listing_canceled |
A listing was pulled |
3 |
listing_sold |
A listing sold |
4 |
purchased |
You bought an item |
5 |
listing_repriced |
An open listing's price changed |
10 |
delivered |
The item reached the buyer |
11 |
settled |
The venue released the funds |
12 |
failed |
The lifecycle step failed |
curl "https://dashboard.steamlabs.dev/api/v1/market/steam/history?event_type=3&per_page=1" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"Use listing_id with account_id to build the timeline for one listing. CSFloat and Market.CSGO add a listing_repriced event whenever a fresh venue snapshot shows a different price.
{
"data": [
{
"id": "019fb446-1a05-7c88-9f42-88b1e0c3d975",
"event_type": 3,
"event_type_key": "listing_sold",
"marketplace": "steam",
"steam_account": {
"id": "019fb42e-9a7e-728d-b960-8b4c2162898c",
"username": "farm_017"
},
"listing_id": "4258109377312946881",
"purchase_id": null,
"actor_steam64_id": "76561198000000042",
"asset_id": "38294011745",
"new_asset_id": "38301884012",
"appid": 730,
"game": "cs2",
"context_id": "2",
"market_hash_name": "AK-47 | Redline (Field-Tested)",
"name": "AK-47 | Redline",
"icon_url": "https://community.fastly.steamstatic.com/economy/image/…",
"listed_price_cents": 1842,
"paid_amount_cents": 1842,
"paid_fee_cents": 240,
"received_amount_cents": 1602,
"currency": "EUR",
"purchase_failed": false,
"happened_at": "2026-07-30T14:02:11+00:00",
"created_at": "2026-07-30T14:07:36+00:00"
}
],
"meta": { "page": 1, "per_page": 1, "total": 2941, "last_page": 2941 }
}event_type_key rides alongside the integer so you can branch on something legible. Nobody reads 11 and thinks "settled".
All three listing venues write into one history table and every query here is venue-scoped, so CSFloat proceeds never show up under Steam.
Refresh history
/api/v1/market/{venue}/history/syncPull fresh history from the venue.
Requires market.write.
Takes the same optional account_ids as the listings refresh, capped at 1,000.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/steam/history/sync" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'Steam has its own history task, because its history page is a separate and expensive fetch from the listings one. You get the task_id envelope back.
CSFloat and market.csgo carry history on the same poll as everything else, so this is the same poll-now request the listings refresh makes: { "task_ids": [], "accounts_affected": 34, "sync": "poll_requested" }. Asking for either gets both.
Skinport history refresh queues the same sync_skinport task as the listings refresh: { "task_ids": [...], "accounts_affected": N, "sync": "task_queued" }. A vanished listed item is sold only when /api/transactions has a matching sale; otherwise it is canceled.
List sale deliveries
Sales in flight on the P2P venues: the item sold, and you still have to hand it over.
/api/v1/market/{venue}/tradesSales awaiting or completing delivery. Not Steam or DMarket.
Requires market.read.
On CSFloat and market.csgo the marketplace only brokers the deal. Between "sold" and "paid" there is a Steam trade offer you must send, on a clock, or the venue penalises you. These rows track that window. /market/steam/trades answers 404 venue_unsupported, because Valve holds the item and a Steam sale is finished the moment it happens.
| Parameter | Type | Description |
|---|---|---|
state |
string | A venue state (below), or all, or needs_action for the sales waiting on you right now |
account_id |
uuid | Only sales from this Steam account. Must be one you own |
origin |
string | steamlabs or external. Not on AssetPay, where every sale is ours |
source |
string | AssetPay only: instant or pool |
search |
string | Matches the item name or its market hash name |
sort |
string | A venue sort (below) |
direction |
string | asc or desc (default) |
per_page |
integer | 50 by default, 200 at most |
The state and sort vocabularies are both per venue, because the lifecycles genuinely differ. CSFloat has an accept stage and a 2 hour clock; market.csgo has neither and roughly 7 hours; Skin.Land has a 10 minute window that SteamLabs answers for you.
| Venue | States | needs_action covers |
Sorts (first is the default) |
|---|---|---|---|
csfloat |
queued, accepted, delivering, delivered, settled, canceled, failed |
queued, accepted |
created_at, sale_price_cents |
marketcsgo |
queued, delivering, delivered, settled, canceled, failed |
queued |
sold_at, created_at, sale_price_cents |
skinland |
queued, offered, accepted, settling, settled, declined, expired, failed |
offered |
created_at, sum_cents |
assetpay |
queued, sending, offered, clearing, settled, declined, canceled, failed, reversed |
failed, reversed |
created_at, gross_cents |
curl "https://dashboard.steamlabs.dev/api/v1/market/csfloat/trades?state=needs_action&per_page=1" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"A CSFloat sale:
{
"data": [
{
"id": "019fb447-2f60-7d13-b8a4-5c0e9d7f2136",
"trade_id": "3f9a1c2e-77bd-4d0a-9f21-8ac4e5b60d11",
"contract_id": "b1c7d0e4-2a55-4f88-9d33-6e2b7a9c1f04",
"marketplace": "csfloat",
"state": "accepted",
"origin": "steamlabs",
"steam_account": {
"id": "019fb42e-9a7e-728d-b960-8b4c2162898c",
"username": "farm_017"
},
"buyer": { "steam64_id": "76561198000000042", "persona": "kessler" },
"asset_id": "38294011745",
"appid": 730,
"game": "cs2",
"market_hash_name": "AK-47 | Redline (Field-Tested)",
"name": "AK-47 | Redline",
"icon_url": "https://csfloat.com/…",
"sale_price_cents": 1980,
"fee_cents": 40,
"receive_cents": 1940,
"currency": "USD",
"steam_offer_id": null,
"steam_offer_state": null,
"delivery_task_id": null,
"delivery_attempts": 0,
"last_error": null,
"accepted_at": "2026-07-30T13:58:02+00:00",
"accept_deadline_at": "2026-07-30T15:58:02+00:00",
"delivered_at": null,
"buyer_accepted_at": null,
"settlement_ends_at": null,
"settled_at": null,
"created_at": "2026-07-30T13:41:19+00:00"
}
],
"meta": { "page": 1, "per_page": 1, "total": 6, "last_page": 6 }
}A market.csgo sale carries market_item_id instead of trade_id and contract_id, no buyer block, and its own clock fields: sold_at, delivery_deadline_at, registered_at, delivered_at, settlement_ends_at, settled_at.
An AssetPay sale, like a Skin.Land one, is a basket: the money is on the row (gross_cents) and the identity is in items.
{
"data": [
{
"id": "019fd1a2-4c7e-7b10-9b2e-3c1f7a9d2e44",
"marketplace": "assetpay",
"source": "pool",
"state": "clearing",
"order_id": "ap_ord_8H2KQ4",
"steam_account": {
"id": "019fb42e-9a7e-728d-b960-8b4c2162898c",
"username": "farm_017"
},
"item_count": 1,
"items": [
{
"asset_id": "38912044517",
"appid": 730,
"game": "cs2",
"market_hash_name": "AK-47 | Case Hardened (Field-Tested)",
"name": "AK-47 | Case Hardened",
"icon_url": "https://community.cloudflare.steamstatic.com/…",
"price_cents": 18450
}
],
"gross_cents": 18450,
"currency": "USD",
"steam_offer_id": "7812345678",
"bot_name": null,
"task_id": "019fd1a2-9b31-70ac-8f52-2d7c1b4e6a08",
"failure_reason": null,
"last_error": null,
"reversed_by": null,
"queued_at": "2026-09-12T10:15:03+00:00",
"offered_at": "2026-09-12T10:15:41+00:00",
"accepted_at": "2026-09-12T10:16:40+00:00",
"hold_until": "2026-09-19T10:16:40+00:00",
"last_checked_at": "2026-09-13T11:02:15+00:00",
"settled_at": null,
"reversed_at": null,
"created_at": "2026-09-12T10:15:03+00:00"
}
],
"meta": { "page": 1, "per_page": 1, "total": 4, "last_page": 4 }
}source is the fork. An instant sale is a basket AssetPay bought outright at its own quote, so bot_name names the AssetPay bot that sent the offer we accepted. A pool sale is one skin their store sold to a buyer, so order_id is set and steam_offer_id is the offer our bot sent that buyer.
hold_until is Steam's trade protection, read off the accepted offer rather than computed, which is why settled_at lags a delivery by about a week. last_checked_at is the last time we logged in to check that trade had not been reversed; there are no fee fields, because AssetPay charges nothing on either kind and the SteamLabs fee is taken at cash-out instead.
delivery_task_id is the thing to watch. It is set the moment a delivery is queued, and it is why repeating a deliver call finds nothing to do.
Deliver many sales
/api/v1/market/{venue}/trades/deliverSend the items for many due sales, grouped into one task per account.
Requires market.write.
| Field | Type | Description |
|---|---|---|
trade_ids |
uuid[] | The sales to deliver, up to 1,000. Required unless you send filters |
filters |
object | The same filter keys the sales index accepts. Required unless you send trade_ids |
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/marketcsgo/trades/deliver" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filters":{"state":"needs_action"}}'$result = Http::withToken($apiKey)
->post('https://dashboard.steamlabs.dev/api/v1/market/marketcsgo/trades/deliver', [
'filters' => ['state' => 'needs_action'],
])
->json();const response = await fetch('https://dashboard.steamlabs.dev/api/v1/market/marketcsgo/trades/deliver', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ filters: { state: 'needs_action' } }),
});
const result = await response.json();{
"task_ids": [
"019fb448-05aa-7e34-b1f7-2d9c8e0a4b73",
"019fb448-0611-7c02-9a58-4f3b1d7e6055"
],
"accounts_affected": 2,
"sales_affected": 19,
"sales_busy": 0,
"sales_ineligible": 3
}sales_ineligible counts sales that were not deliverable from their current state, and they are skipped rather than failing the batch.
When nothing could be queued you get one of two answers, and they need different responses from you:
| Status | Code | Meaning |
|---|---|---|
409 |
account_busy |
Every selected sale belongs to an account with market work already in flight. Retry shortly |
422 |
nothing_deliverable |
No selected sale was in a deliverable state. Fix the selection |
Both add sales_ineligible and sales_busy.
Cancel many sales
/api/v1/market/{venue}/trades/cancelRefuse many sales before anything moves. CSFloat only.
Requires market.write.
Takes the same trade_ids or filters selection as the deliver endpoint.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/csfloat/trades/cancel" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"trade_ids":["019fb447-2f60-7d13-b8a4-5c0e9d7f2136"]}'Answers 200, not 202. Cancelling is a local state change with no worker involved, so by the time you get this it has already happened:
{
"sales_affected": 1,
"matched": 1
}The window is narrow on purpose: only while the sale is still queued and unassigned. Nothing cancellable in the selection answers 422 nothing_cancelable with matched.
market.csgo offers no cancel at all, here or in the dashboard, and answers 404 venue_unsupported. Its sales are registered with the venue the moment they land, so a local refusal would leave your row saying cancelled while the venue waits for an item and holds you responsible.
Accept and deliver one sale
/api/v1/market/{venue}/trades/{trade}/accept-and-deliverAccept the buyer's offer and send the item. CSFloat only.
Requires market.write.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/csfloat/trades/019fb447-2f60-7d13-b8a4-5c0e9d7f2136/accept-and-deliver" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"task_ids": ["019fb449-3b12-70ef-8c47-1a6d5e2f9073"],
"accounts_affected": 1,
"sales_affected": 1
}One endpoint rather than two, on purpose. Accepting without sending starts a 2 hour penalty clock you could not then satisfy from the API.
The sale must be queued with no delivery already assigned, otherwise 422 trade_not_in_state with state and required_state. On market.csgo this answers 404 venue_unsupported: there is no accept stage.
Deliver one sale
/api/v1/market/{venue}/trades/{trade}/deliverSend the item now for a sale that is already due.
Requires market.write.
"Due" means accepted on CSFloat and queued on market.csgo: the same moment in two vocabularies. Either way the sale must have no delivery assigned yet.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/marketcsgo/trades/019fb447-9c31-7a80-bd12-70e5c3f4a819/deliver" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"task_ids": ["019fb449-6d70-7188-a933-52c0e7b1f486"],
"accounts_affected": 1,
"sales_affected": 1
}Wrong state gives 422 trade_not_in_state, naming the state it wanted. An account with market work in flight gives 409 account_busy.
Retry a failed delivery
/api/v1/market/{venue}/trades/{trade}/retry-deliveryTry a failed delivery again.
Requires market.write.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/csfloat/trades/019fb447-2f60-7d13-b8a4-5c0e9d7f2136/retry-delivery" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"Answers the same 202 envelope as Deliver one sale.
Separate from deliver even though both end in the same task, because the states they start from are disjoint. This one requires failed. Anything else gives 422 trade_not_in_state, which is more useful than one endpoint that silently accepts both.
Check last_error and delivery_attempts on the sale before retrying. A delivery that failed because the item is gone will fail again.
Cancel one sale
/api/v1/market/{venue}/trades/{trade}/cancelRefuse one sale before anything moves. CSFloat only.
Requires market.write.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/csfloat/trades/019fb447-2f60-7d13-b8a4-5c0e9d7f2136/cancel" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"Answers 200 with the whole updated sale, in the same shape the index returns:
{
"id": "019fb447-2f60-7d13-b8a4-5c0e9d7f2136",
"trade_id": "3f9a1c2e-77bd-4d0a-9f21-8ac4e5b60d11",
"marketplace": "csfloat",
"state": "canceled",
"sale_price_cents": 1980,
"receive_cents": 1940,
"currency": "USD",
"delivery_task_id": null,
"created_at": "2026-07-30T13:41:19+00:00"
}Only a sale that is still queued and unassigned can be refused. Anything else gives 422 trade_not_in_state. market.csgo answers 404 venue_unsupported.
Earnings
What your AssetPay sales paid, and where the money is now. AssetPay only: every other venue pays into a wallet you hold at the venue, so they answer 404 venue_unsupported here.
/api/v1/market/{venue}/earningsAvailable, clearing and paid-out totals for the venue. AssetPay only.
Requires market.read.
curl "https://dashboard.steamlabs.dev/api/v1/market/assetpay/earnings" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"currency": "USD",
"available_cents": 4210,
"clearing_cents": 18450,
"next_release_at": "2026-09-19T10:16:40+00:00",
"paid_out_cents": 129000,
"payout_count": 4,
"platform_credit_cents": 98498,
"platform_credit_currency": "EUR",
"payout": {
"fee_percent": 5,
"minimum_cents": 2000,
"requestable_via_api": false
}
}Three pots, and they never mix. available_cents has cleared Steam's trade protection and is yours to withdraw. clearing_cents is sold but still inside that protection, and next_release_at is when the earliest of it comes out. platform_credit_cents is prepaid SteamLabs credit in the billing currency: it buys the platform and never comes back out, which is why it is reported separately and not added to anything.
available_cents can be negative. A trade reversed after the money was already credited claws it back, and the balance is allowed to go under zero rather than silently absorbing the loss.
Earnings ledger
Every movement of the earnings balance, newest first.
/api/v1/market/{venue}/earnings/ledgerThe earnings ledger. AssetPay only.
Requires market.read.
| Parameter | Type | Description |
|---|---|---|
type |
string | sale_settled, sale_reversed, payout_hold, payout_release or adjustment |
per_page |
integer | 50 by default, 200 at most |
{
"data": [
{
"id": "019fd1b7-4a02-73de-9c11-0f6a2e4b8d97",
"type": "sale_settled",
"amount_cents": 930,
"balance_after_cents": 4210,
"currency": "USD",
"reason": "AssetPay pool sale settled",
"source_type": "App\\Models\\AssetpaySale",
"source_id": "019fd1a2-4c7e-7b10-9b2e-3c1f7a9d2e44",
"created_at": "2026-09-19T10:31:52+00:00"
}
],
"meta": { "page": 1, "per_page": 50, "total": 12, "last_page": 1 }
}amount_cents is signed, so balance_after_cents adds up: a settled sale is positive, a payout or a reversal negative. source_id points at whatever caused the row, which for a sale is the id you read from /market/assetpay/trades.
Payouts
Cash-outs and where each one stands.
/api/v1/market/{venue}/payoutsPayout history. AssetPay only.
Requires market.read.
| Parameter | Type | Description |
|---|---|---|
state |
string | requested, sending, completed, failed or rejected |
per_page |
integer | 50 by default, 200 at most |
{
"data": [
{
"id": "019fd1c4-8e55-7a20-b7f3-91c4d0e2a6b8",
"state": "completed",
"amount_cents": 5000,
"fee_cents": 250,
"net_cents": 4750,
"currency": "USD",
"chain": "bsc",
"address": "0x2f1c…",
"tx_hash": "0x9ab3…",
"failure_reason": null,
"requested_at": "2026-09-14T08:02:11+00:00",
"sent_at": "2026-09-14T08:44:03+00:00",
"resolved_at": "2026-09-14T08:44:03+00:00"
}
],
"meta": { "page": 1, "per_page": 50, "total": 4, "last_page": 1 }
}amount_cents is what left your balance, net_cents is what the address received, and the difference is the SteamLabs fee. The whole address is returned rather than a truncated one: checking it against a block explorer is the only reason to read this.
Errors
Beyond the universal codes, this group returns:
| Status | Code | Meaning |
|---|---|---|
404 |
unknown_venue |
The path segment is not a marketplace. Adds venues |
404 |
venue_unsupported |
The venue has no such surface. Adds venue and supported_venues |
409 |
account_busy |
The account already has marketplace work queued or running. Wait for that task |
422 |
listing_not_open |
The listing is no longer open. Adds state |
422 |
buy_order_not_open |
The buy order is no longer open. Adds state |
422 |
trade_not_in_state |
The sale is in the wrong state. Adds state and required_state |
422 |
nothing_to_cancel |
Nothing in the selection is still open. Adds matched |
422 |
nothing_cancelable |
No selected sale can still be cancelled. Adds matched |
422 |
nothing_deliverable |
No selected sale can be delivered from its state |
422 |
nothing_eligible |
No account matched the refresh, or none could fund the order |
422 |
missing_price_for_currency |
A wallet currency in the selection had no price. Adds missing_currencies and selection_currencies |
422 |
bulk_limit_exceeded |
Over 1,000 ids, or a filter matching over 1,000 rows. Adds max, and matched for a filter |
503 |
maintenance_mode |
Platform maintenance pauses new listings and buy orders. Cancels, syncs and reads keep working. Adds reason |
Everything else, including 401, 403 missing_scope, 403 plan_limit_reached, 422 validation_failed and 429, is on Errors.