# Market

> Listings, buy orders, sales history and deliveries on Steam, CSFloat and market.csgo.


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` or `marketcsgo`.

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 three 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` |
| --- | --- | --- | --- |
| Stats | Yes | Yes | Yes |
| Listings: list, cancel, sync | Yes | Yes | Yes |
| Reprice a listing | No | Yes | Yes |
| Buy orders | Yes | No | No |
| Sales history | Yes | Yes | Yes |
| Sale deliveries (trades) | No | Yes | Yes |
| Accept a sale | No | Yes | No |
| Cancel a sale | No | Yes | No |

Steam fixes a listing's price at creation, so there is nothing to reprice. CSFloat and market.csgo are sell-only, so they have no buy orders. Steam settles a sale internally the moment it happens, so it has no delivery stage: on the other two the marketplace only brokers the deal and you still have to send the item.

> [!NOTE]
> Creating a listing is not here. You sell an **item** you hold, chosen with its price and its venue, so selling lives on the inventory surface. A listing on this page is the result of that, which is why every write below either ends one or refreshes one.

## 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, repricing, syncing, reading history 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.

Two writes here can still come back `403 plan_limit_reached`:

- **Placing a buy order.** Buying spends the Steam venue entitlement, so it needs `steam` in your plan's `allowed_marketplaces`, plus `place_buy_order` in `allowed_task_types`.
- **Syncing on Steam.** A Steam refresh is a real task, so it needs `sync_market_listings` (or `sync_market_history`) in `allowed_task_types`. The CSFloat and market.csgo refreshes are poller requests and are never gated.

Read `plan` from `GET /api/v1/me` at startup and you will rarely meet either. See [Errors](/docs/api/en/concepts/errors) for the response shape.

## Venue stats

The headline numbers above a venue's tables.

```endpoint
method: GET
path: /api/v1/market/{venue}/stats
description: Open listings, recent money moved, and where your balance sits on this venue.
auth: bearer
```

Requires `market.read`.

```bash tab=curl
curl https://dashboard.steamlabs.dev/api/v1/market/steam/stats \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
```

```json
{
    "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:

```json
{
    "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 }
    }
}
```

> [!IMPORTANT]
> Every money figure is a map keyed by currency, never one converted total. Wallets and marketplace balances live in per-account currencies and nothing here applies an exchange rate. The dashboard's single USD headline is a display approximation; this is the real breakdown.

`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.

```endpoint
method: GET
path: /api/v1/market/{venue}/listings
description: Your listings on this venue, paginated and filterable.
auth: bearer
```

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 |

```bash tab=curl
curl "https://dashboard.steamlabs.dev/api/v1/market/steam/listings?state=open&game=cs2&per_page=1" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
```

```json
{
    "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",
            "resolved_at": 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.

## Refresh listings

Re-read the venue for the accounts you name.

```endpoint
method: POST
path: /api/v1/market/{venue}/listings/sync
description: Pull fresh listings, wallet and buy orders from the venue.
auth: bearer
```

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.

```bash tab=curl
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:

```json
{
    "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 are polled continuously over HTTP, so a refresh asks the poller to visit those accounts now instead of on its cadence:

```json
{
    "task_ids": [],
    "accounts_affected": 34,
    "sync": "poll_requested"
}
```

> [!NOTE]
> Steam only ever syncs market-eligible accounts, whether you named them or not. Naming a trade-banned account does not make it able to read a market it cannot reach. CSFloat and market.csgo only target accounts connected to that venue: an API key, a proxy, the integration enabled, and an identity secret on the Steam account.

Answers `422 nothing_eligible` when nothing matched.

## Cancel many listings

Pull a selection of listings off the marketplace.

```endpoint
method: POST
path: /api/v1/market/{venue}/listings/cancel
description: Delist many listings, grouped into one task per account.
auth: bearer
```

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](/docs/api/en/concepts/bulk-operations) for the shared contract.

```bash tab=curl
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"}}'
```

```php tab=PHP
$result = Http::withToken($apiKey)
    ->post('https://dashboard.steamlabs.dev/api/v1/market/csfloat/listings/cancel', [
        'filters' => ['state' => 'open', 'game' => 'cs2'],
    ])
    ->json();
```

```javascript tab=Node
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();
```

```python tab=Python
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:

```json
{
    "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.

> [!WARNING]
> A `filters` selection is resolved to rows here, not left as a definition, because each account's task carries its listing ids. It is therefore capped like an id list: a filter matching more than 1,000 listings answers `422 bulk_limit_exceeded` with `max` and `matched`. Narrow it rather than expecting a truncated run.

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

```endpoint
method: POST
path: /api/v1/market/{venue}/listings/{listing}/cancel
description: Pull one listing off the marketplace.
auth: bearer
```

Requires `market.write`.

```bash tab=curl
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/steam/listings/019fb42e-9a61-70d2-818a-f6a56593f3a5/cancel" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
```

```json
{
    "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.

```endpoint
method: POST
path: /api/v1/market/{venue}/listings/{listing}/price
description: Set a new buyer-pays price. CSFloat and market.csgo only.
auth: bearer
```

Requires `market.write`.

| Field | Type | Description |
| --- | --- | --- |
| **`price_cents`** | integer | The new buyer-pays price in cents. At least 1, at most 100000000 |

```bash tab=curl
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}'
```

```json
{
    "task_ids": ["019fb442-8a10-7c33-b4d2-0f1e6c7a9b55"],
    "accounts_affected": 1,
    "listings_affected": 1,
    "price_cents": 1795
}
```

> [!IMPORTANT]
> `price_cents` is what the buyer pays, not your net. That is the number the venue quotes back and the number the reprice modal takes. Meaning the other one would move every price by the fee.

`/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.

## List buy orders

Standing bids resting on the Steam market, waiting to be filled.

```endpoint
method: GET
path: /api/v1/market/{venue}/buy-orders
description: Your buy orders. Steam only.
auth: bearer
```

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 |

```bash tab=curl
curl "https://dashboard.steamlabs.dev/api/v1/market/steam/buy-orders?state=open&per_page=1" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
```

```json
{
    "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.

```endpoint
method: POST
path: /api/v1/market/{venue}/buy-orders
description: Place one buy order across a selection of accounts. Steam only.
auth: bearer
```

Requires `market.write`.

> [!CAUTION]
> This commits real money, so it is the only route in the Market domain that **requires an `Idempotency-Key` header**. Without it you get `400 idempotency_key_required`. A client that posts an order and then times out cannot tell "never arrived" from "worked, reply lost", and the only move left is a retry. Without the key that retry doubles the bid. See [Bulk operations](/docs/api/en/concepts/bulk-operations).

| 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 |

> [!IMPORTANT]
> `prices_cents` is a map, not a number, and it is the one thing about this endpoint you have to understand. Steam denominates an order in the placing account's own wallet currency and converts nothing, so a selection spanning a EUR account and a PLN account is two orders at two prices.
>
> A currency present in your selection with no usable price refuses the **whole** submission rather than placing the orders it could. A partial fan-out over money is the one outcome a retry cannot fix.

```bash tab=curl
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}}'
```

```php tab=PHP
$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();
```

```javascript tab=Node
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();
```

```python tab=Python
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:

```json
{
    "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

```endpoint
method: POST
path: /api/v1/market/{venue}/buy-orders/sync
description: Pull fresh buy orders from Steam. Steam only.
auth: bearer
```

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`.

```bash tab=curl
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 '{}'
```

> [!TIP]
> This queues the same task the listings refresh does, because it is the same trip: one login pulls listings, buy orders and the wallet together. Calling both back to back spends a second login for data the first already brought back.

## Cancel many buy orders

```endpoint
method: POST
path: /api/v1/market/{venue}/buy-orders/cancel
description: Cancel many resting orders, grouped into one task per account. Steam only.
auth: bearer
```

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` |

```bash tab=curl
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"}}'
```

```json
{
    "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

```endpoint
method: POST
path: /api/v1/market/{venue}/buy-orders/{buyOrder}/cancel
description: Cancel one resting order. Steam only.
auth: bearer
```

Requires `market.write`.

```bash tab=curl
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"
```

```json
{
    "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.

```endpoint
method: GET
path: /api/v1/market/{venue}/history
description: Market history events on this venue, newest first.
auth: bearer
```

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 |
| `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, 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 |
| `10` | `delivered` | The item reached the buyer |
| `11` | `settled` | The venue released the funds |
| `12` | `failed` | The lifecycle step failed |

```bash tab=curl
curl "https://dashboard.steamlabs.dev/api/v1/market/steam/history?event_type=3&per_page=1" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
```

```json
{
    "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 venues write into one history table and every query here is venue-scoped, so CSFloat proceeds never show up under Steam.

## Refresh history

```endpoint
method: POST
path: /api/v1/market/{venue}/history/sync
description: Pull fresh history from the venue.
auth: bearer
```

Requires `market.write`.

Takes the same optional `account_ids` as the listings refresh, capped at 1,000.

```bash tab=curl
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.

## List sale deliveries

Sales in flight on the P2P venues: the item sold, and you still have to hand it over.

```endpoint
method: GET
path: /api/v1/market/{venue}/trades
description: Sales awaiting or completing delivery. CSFloat and market.csgo only.
auth: bearer
```

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` |
| `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.

| 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` |

> [!NOTE]
> `sort=sold_at` exists on market.csgo and not on CSFloat. A CSFloat sale has no separate sold timestamp: the row is created when the sale happens, so `created_at` is that moment and is the default. Sending it on CSFloat answers `422` naming the sorts that venue accepts.

```bash tab=curl
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:

```json
{
    "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`.

`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

```endpoint
method: POST
path: /api/v1/market/{venue}/trades/deliver
description: Send the items for many due sales, grouped into one task per account.
auth: bearer
```

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` |

```bash tab=curl
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"}}'
```

```php tab=PHP
$result = Http::withToken($apiKey)
    ->post('https://dashboard.steamlabs.dev/api/v1/market/marketcsgo/trades/deliver', [
        'filters' => ['state' => 'needs_action'],
    ])
    ->json();
```

```javascript tab=Node
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();
```

```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`.

> [!IMPORTANT]
> Delivering is never plan-gated, and this is the strongest case of that rule. By the time a sale is here the item is already sold. A subscription lapsing between the sale and the delivery must not turn into an undelivered trade, because the venue punishes the seller.

## Cancel many sales

```endpoint
method: POST
path: /api/v1/market/{venue}/trades/cancel
description: Refuse many sales before anything moves. CSFloat only.
auth: bearer
```

Requires `market.write`.

Takes the same `trade_ids` or `filters` selection as the deliver endpoint.

```bash tab=curl
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:

```json
{
    "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

```endpoint
method: POST
path: /api/v1/market/{venue}/trades/{trade}/accept-and-deliver
description: Accept the buyer's offer and send the item. CSFloat only.
auth: bearer
```

Requires `market.write`.

```bash tab=curl
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"
```

```json
{
    "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

```endpoint
method: POST
path: /api/v1/market/{venue}/trades/{trade}/deliver
description: Send the item now for a sale that is already due.
auth: bearer
```

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.

```bash tab=curl
curl -X POST "https://dashboard.steamlabs.dev/api/v1/market/marketcsgo/trades/019fb447-9c31-7a80-bd12-70e5c3f4a819/deliver" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
```

```json
{
    "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

```endpoint
method: POST
path: /api/v1/market/{venue}/trades/{trade}/retry-delivery
description: Try a failed delivery again.
auth: bearer
```

Requires `market.write`.

```bash tab=curl
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

```endpoint
method: POST
path: /api/v1/market/{venue}/trades/{trade}/cancel
description: Refuse one sale before anything moves. CSFloat only.
auth: bearer
```

Requires `market.write`.

```bash tab=curl
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:

```json
{
    "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`.

## 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](/docs/api/en/concepts/errors).
