Skip to content
SteamLabs API

Endpoints

Trading

Read and answer Steam trade offers, clear mobile confirmations, and manage saved routing templates.

Everything the Trades page and an account's Trade Offers panel do, over HTTP: read the offer history the way the table reads it, accept, decline and cancel, work through Steam mobile confirmations, and manage the saved routing rulesets that distribution runs apply. Two scopes cover the domain: trading.read for the listings, trading.write for anything that answers an offer or a confirmation, and for template edits.

List trade offers

The trades table, as a query. Every tab, filter and search box on the page is a parameter here.

GET/api/v1/trade-offers

Your trade offers, paginated and filterable.

API key required

Requires trading.read.

Parameter Type Description
tab string all (default), open (still awaiting someone's action), protected (accepted CS2 trades still in trade protection)
account_id uuid One of your Steam accounts. Matches either side, so you get what it sent and what it was sent
partner_account_id uuid Only offers whose partner is this account of yours
flow string incoming, outgoing (both external only), or internal for transfers between two of your own accounts
state integer One Steam offer state. See the table below
game array cs2, tf2, steam. Repeat it (game[]=) to match any of several
search string Matches the Steam offer id, the external partner's Steam64 id, or either account's username
include_mirrors boolean true returns both rows of an internal transfer instead of collapsing them
sort string state_updated_at (default), sent_at, expires_at
order string asc, desc (default)
per_page integer Page size. See Pagination and filtering

An unknown value is a 422 rather than an ignored parameter, so tab=oepn fails instead of quietly returning everything.

Bash
curl "https://dashboard.steamlabs.dev/api/v1/trade-offers?tab=open&flow=incoming&per_page=1" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
JSON
{
    "data": [
        {
            "id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
            "offer_id": "9211449039",
            "trade_id": null,
            "task_id": "019fb431-2c88-71ea-b0a3-8ce2f5d19b07",
            "direction": "received",
            "flow": "incoming",
            "state": 2,
            "state_label": "Active",
            "is_open": true,
            "is_awaiting_settlement": false,
            "game": "cs2",
            "message": "swap?",
            "steam_account": {
                "id": "019fb42e-9a7e-728d-b960-8b4c2162898c",
                "username": "farm_017",
                "persona_name": "Ada",
                "steam64_id": "76561198000000017"
            },
            "partner_steam_account": null,
            "partner_steam64_id": "76561199202970980",
            "give_count": 0,
            "receive_count": 2,
            "items_to_give": [],
            "items_to_receive": [
                {
                    "asset_id": "15744253393",
                    "appid": 730,
                    "context_id": 2,
                    "class_id": "3946324",
                    "instance_id": "11040671",
                    "market_hash_name": "AK-47 | Slate (Field-Tested)",
                    "name": "AK-47 | Slate (Field-Tested)",
                    "icon_url": "https://community.cloudflare.steamstatic.com/economy/image/…",
                    "amount": 1,
                    "est_usd": 372
                }
            ],
            "estimated_give_cents": null,
            "estimated_receive_cents": 4310,
            "is_delayed_settlement": false,
            "settlement_at": null,
            "escrow_end_at": null,
            "sent_at": "2026-07-30T09:14:22+00:00",
            "state_updated_at": "2026-07-30T14:02:11+00:00",
            "expires_at": "2026-08-13T09:14:22+00:00"
        }
    ],
    "meta": { "page": 1, "per_page": 1, "total": 1, "last_page": 1 }
}

States

state is Steam's own ETradeOfferState integer. state_label is the same value in the dashboard's words.

Value Label Open
1 Invalid no
2 Active yes
3 Accepted no
4 Countered no
5 Expired no
6 Canceled no
7 Declined no
8 Invalid Items no
9 Needs Confirmation yes
10 Canceled via Mobile no
11 In Escrow yes

is_open is that last column. An open offer can still change through someone's action; a closed one only ever changes when a fresh fetch finds Steam disagrees.

tab=protected is narrower than state=3. It is the accepted CS2 trades whose items have left the sender, have not reached the receiver, and are still reversible: is_delayed_settlement true, state Accepted, and settlement_at in the future. is_awaiting_settlement says the same thing per row.

Get one trade offer

GET/api/v1/trade-offers/{id}

One trade offer, with both item lists in full.

API key required

Requires trading.read.

Same object as a listing row. {id} is the SteamLabs uuid, not the Steam offer id, and it names one perspective: this endpoint never collapses an internal transfer, so a mirror uuid returns the mirror.

Bash
curl https://dashboard.steamlabs.dev/api/v1/trade-offers/019fb42e-9a61-70d2-818a-f6a56593f3a5 \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"

Accept an offer

Queues an accept on the account that received the offer.

POST/api/v1/trade-offers/{id}/accept

Accept one incoming offer.

API key required

Requires trading.write.

This route carries the idempotent middleware, so an Idempotency-Key header is required. Accepting moves real items, and a retry after a timeout must replay rather than accept twice. See Bulk operations.

No body.

Bash
curl -X POST https://dashboard.steamlabs.dev/api/v1/trade-offers/019fb42e-9a61-70d2-818a-f6a56593f3a5/accept \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

202 Accepted, with the worker task to follow and the Steam offer id it will answer:

JSON
{
    "task_id": "019fb440-3f01-72c6-a1de-1c5ad0f7b9e2",
    "accounts_affected": 1,
    "offer_id": "9211449039"
}

Accepting resolves the receiving side first, so you can call this on a collapsed internal-transfer row and the task lands on the account that can actually answer.

Decline an offer

The same call, the other answer.

POST/api/v1/trade-offers/{id}/decline

Decline one incoming offer.

API key required

Requires trading.write. An Idempotency-Key header is required.

No body. Same 202 shape, same receiving-side resolution, and the same 422 offer_not_actionable when the offer is not an active offer awaiting one of your accounts.

Bash
curl -X POST https://dashboard.steamlabs.dev/api/v1/trade-offers/019fb42e-9a61-70d2-818a-f6a56593f3a5/decline \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

Cancel an offer

Withdraws an offer one of your accounts sent.

POST/api/v1/trade-offers/{id}/cancel

Cancel an open offer this account sent.

API key required

Requires trading.write. An Idempotency-Key header is required.

No body. Only the sending side can withdraw an offer, and only while it is still open, so the row you name must have direction: "sent" and is_open: true. Anything else is 422 offer_not_cancellable, carrying the state it actually has:

JSON
{
    "message": "Only an offer this account sent, and that is still open, can be cancelled.",
    "code": "offer_not_cancellable",
    "state": 7
}

A success is the usual 202 with task_id, accounts_affected and offer_id.

Refresh an account's offers

Asks Steam what this account's offers look like right now.

POST/api/v1/accounts/{account}/trade-offers/refresh

Queue a fresh fetch of one account's trade offers.

API key required

Requires trading.write.

No Idempotency-Key needed. This only reads from Steam, and the single-flight guard already collapses a burst of calls into one task.

Bash
curl -X POST https://dashboard.steamlabs.dev/api/v1/accounts/019fb42e-9a7e-728d-b960-8b4c2162898c/trade-offers/refresh \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
JSON
{
    "task_id": "019fb440-3f01-72c6-a1de-1c5ad0f7b9e2",
    "accounts_affected": 1
}

The offers land asynchronously. Poll the task, then re-read GET /api/v1/trade-offers.

Answer an account's offers in bulk

The panel's Accept all and Decline all, plus everything in between.

POST/api/v1/accounts/{account}/trade-offers/respond

Accept or decline several of one account's incoming offers, or all of them.

API key required

Requires trading.write. An Idempotency-Key header is required.

Field Type Description
action string accept or decline
offer_ids array The Steam offer ids to answer, as strings of up to 32 characters. Omit the field to answer every incoming offer on the account

Steam answers offers per account session, so N offers on one account become one worker task carrying N ids, never N tasks. That is why this is an account sub-resource rather than a flat bulk endpoint. Duplicate ids are collapsed. The list is capped at 1,000 ids (422 bulk_limit_exceeded, carrying max); if you need more than that, omit the field.

Bash
curl -X POST https://dashboard.steamlabs.dev/api/v1/accounts/019fb42e-9a7e-728d-b960-8b4c2162898c/trade-offers/respond \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"action": "accept", "offer_ids": ["9211449039", "9211449102"]}'
JSON
{
    "task_id": "019fb440-3f01-72c6-a1de-1c5ad0f7b9e2",
    "accounts_affected": 1,
    "offers_requested": 2
}

offers_requested is null when you omitted offer_ids, which is how you can tell a fan-out from a list in your own logs.

List an account's confirmations

Steam mobile confirmations waiting on one account.

GET/api/v1/accounts/{account}/confirmations

The latest confirmation snapshot for one account.

API key required

Requires trading.read.

Takes page and per_page only.

Bash
curl https://dashboard.steamlabs.dev/api/v1/accounts/019fb42e-9a7e-728d-b960-8b4c2162898c/confirmations \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
JSON
{
    "data": [
        {
            "id": "14958203481",
            "nonce": "6821994733170518043",
            "type": 2,
            "type_name": "Trade Offer",
            "creator_id": "9211449039",
            "headline": "Sent a trade offer",
            "summary": ["To: TradeBot_04"],
            "icon": "https://community.cloudflare.steamstatic.com/economy/image/…",
            "creation_time": 1785412342,
            "accept_label": "Confirm",
            "cancel_label": "Cancel"
        }
    ],
    "meta": { "page": 1, "per_page": 50, "total": 1, "last_page": 1 },
    "has_identity_secret": true,
    "fetched_at": "2026-07-30T14:02:11+00:00",
    "pending_task_id": null,
    "last_error": null
}
Field Type Description
has_identity_secret boolean Whether this account can answer confirmations at all
fetched_at string When the snapshot was taken, or null if none ever was
pending_task_id uuid The confirmation task currently holding the account, if any
last_error string The error from the last confirmation task, when that task failed

creation_time is Steam's raw Unix timestamp in seconds, not an ISO 8601 string like the rest of the API. Reading is allowed on an account with no identity secret: you get an empty list and has_identity_secret: false, which is the useful answer.

Refresh an account's confirmations

POST/api/v1/accounts/{account}/confirmations/refresh

Queue a fresh confirmation snapshot for one account.

API key required

Requires trading.write.

No Idempotency-Key needed, and no body. Answers 202 with task_id and accounts_affected. Poll the task, then re-read the listing.

Bash
curl -X POST https://dashboard.steamlabs.dev/api/v1/accounts/019fb42e-9a7e-728d-b960-8b4c2162898c/confirmations/refresh \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"

An account with no identity secret cannot fetch either, and is refused with 422 identity_secret_missing. Add the secret through Steam accounts first.

Answer an account's confirmations

POST/api/v1/accounts/{account}/confirmations/respond

Confirm or cancel some or all of an account's pending confirmations.

API key required

Requires trading.write. An Idempotency-Key header is required.

Field Type Description
accept boolean true confirms, false cancels
confirmation_ids array The confirmation ids to answer, as strings of up to 64 characters. Omit the field to answer every confirmation in the latest snapshot
Bash
curl -X POST https://dashboard.steamlabs.dev/api/v1/accounts/019fb42e-9a7e-728d-b960-8b4c2162898c/confirmations/respond \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"accept": true, "confirmation_ids": ["14958203481"]}'
JSON
{
    "task_id": "019fb440-3f01-72c6-a1de-1c5ad0f7b9e2",
    "accounts_affected": 1,
    "confirmations_requested": 1
}

Ids are matched against the latest snapshot before anything is queued, so an id that has since vanished is dropped rather than failing the batch. If none of your ids survive that match you get 422 no_matching_confirmations with known, the number of confirmations the snapshot does hold. Refresh and try again.

confirmations_requested is null when you omitted confirmation_ids.

List routing templates

A routing template is a named, ordered ruleset you reuse when distributing items or sending an inventory selection (routing_template_id on POST /api/v1/inventory/send).

GET/api/v1/trade-routing-templates

Your saved routing templates, with their full rulesets.

API key required

Requires trading.read.

Parameter Type Description
search string Matches the template name
sort string name (default), created_at, updated_at
order string asc (default), desc
per_page integer Page size
Bash
curl "https://dashboard.steamlabs.dev/api/v1/trade-routing-templates?search=vault" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
JSON
{
    "data": [
        {
            "id": "019fb432-77c1-71bd-9a04-4f0e2c9a8d31",
            "name": "Knives to the vault",
            "rules_count": 3,
            "rules": [
                {
                    "action": "skip",
                    "games": [],
                    "price_min": null,
                    "price_max": 0.03,
                    "item_ids": [],
                    "categories": [],
                    "origins": [],
                    "target_account_ids": [],
                    "target_tag_ids": [],
                    "target_trade_urls": [],
                    "strategy": "round_robin",
                    "target_value": null,
                    "items_limit_per_target": null
                }
            ],
            "created_at": "2026-07-12T10:20:00+00:00",
            "updated_at": "2026-07-30T14:02:11+00:00"
        }
    ],
    "meta": { "page": 1, "per_page": 50, "total": 1, "last_page": 1 }
}

Rules always come back in canonical shape: every key present, conditions you left out materialized as empty lists or null. Read them with that in mind rather than checking whether a key exists.

Create a routing template

The richest body in the API. It is the panel's rules builder, persisted.

POST/api/v1/trade-routing-templates

Save a named routing ruleset.

API key required

Requires trading.write.

Field Type Description
name string Up to 80 characters, unique among your own templates
rules array 1 to 50 rule objects, in evaluation order

How rules are evaluated

Rules run top to bottom, first match wins. Each item is tested against every rule's conditions in order, and the first rule that matches decides what happens to it: routed onto that rule's targets, or skipped. An item matching no rule stays where it is. A rule with no conditions matches everything, so put your narrow rules first and your catch-all last.

A rule

Conditions are ANDed, and each one left out or empty means "any".

Field Type Description
action string route (send matched items to the targets) or skip (leave them alone, and stop later rules seeing them)
games array cs2, tf2, steam. The item's game must be one of these
price_min number Minimum Steam market price, in US dollars. Unpriced items count as $0
price_max number Maximum price, in US dollars. Must be at least price_min
item_ids array Catalog item uuids. The item must be one of them. Up to 1,000
categories array Coarse item types: weapon, knife, gloves, container, sticker, graffiti, charm, patch, agent, music_kit, collectible, tool, key, pass, gift, trading_card, booster_pack, emoticon
origins array CS2 item origins as integers, for example 0 (timed drop), 2 (purchased), 3 (traded), 8 (found in crate)
strategy string How matched items spread over the targets. Required when action is route
target_value number US dollars per target. Required for fill_value and top_up_value
items_limit_per_target integer At most this many items per target. Omit for no limit
target_account_ids array Your own Steam account uuids to route onto. Up to 1,000
target_tag_ids array Account tag uuids. Every account carrying any of them becomes a target. Up to 1,000
target_trade_urls array External Steam trade URLs. Up to 1,000, each up to 255 characters

A route rule needs a strategy and at least one destination, in target_account_ids, target_tag_ids or target_trade_urls, in any combination. A skip rule needs none of them, and its target and strategy fields are ignored.

Strategies

Value What it does
round_robin Spread matched items evenly across the targets
random Each item goes to a random target that still has room
fill_value Pack priciest first, up to target_value per target
top_up_value Like fill_value, but counts what an account target already holds toward target_value

Worked example

Skip the near-worthless items, park knives and gloves on the vault account, spread everything else over two accounts and one external trade URL.

Bash
curl -X POST https://dashboard.steamlabs.dev/api/v1/trade-routing-templates \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d @template.json
JSON
{
    "name": "Knives to the vault",
    "rules": [
        {
            "action": "skip",
            "price_max": 0.03
        },
        {
            "action": "route",
            "games": ["cs2"],
            "categories": ["knife", "gloves"],
            "strategy": "top_up_value",
            "target_value": 2500,
            "items_limit_per_target": 40,
            "target_account_ids": ["019fb42e-9a7e-728d-b960-8b4c2162898c"]
        },
        {
            "action": "route",
            "games": ["cs2"],
            "price_min": 1.5,
            "origins": [0, 8],
            "strategy": "round_robin",
            "target_account_ids": [
                "019fb42e-9c10-7233-8f7a-5b2d90c14a77",
                "019fb42e-9c22-70ab-b3c4-118e6f2d0e59"
            ],
            "target_trade_urls": [
                "https://steamcommunity.com/tradeoffer/new/?partner=39734289&token=AbCd1234"
            ]
        }
    ]
}

201 Created with the saved template, rules normalized:

JSON
{
    "id": "019fb432-77c1-71bd-9a04-4f0e2c9a8d31",
    "name": "Knives to the vault",
    "rules_count": 3,
    "rules": [
        {
            "action": "skip",
            "games": [],
            "price_min": null,
            "price_max": 0.03,
            "item_ids": [],
            "categories": [],
            "origins": [],
            "target_account_ids": [],
            "target_tag_ids": [],
            "target_trade_urls": [],
            "strategy": "round_robin",
            "target_value": null,
            "items_limit_per_target": null
        },
        {
            "action": "route",
            "games": ["cs2"],
            "price_min": null,
            "price_max": null,
            "item_ids": [],
            "categories": ["knife", "gloves"],
            "origins": [],
            "target_account_ids": ["019fb42e-9a7e-728d-b960-8b4c2162898c"],
            "target_tag_ids": [],
            "target_trade_urls": [],
            "strategy": "top_up_value",
            "target_value": 2500,
            "items_limit_per_target": 40
        },
        {
            "action": "route",
            "games": ["cs2"],
            "price_min": 1.5,
            "price_max": null,
            "item_ids": [],
            "categories": [],
            "origins": [0, 8],
            "target_account_ids": [
                "019fb42e-9c10-7233-8f7a-5b2d90c14a77",
                "019fb42e-9c22-70ab-b3c4-118e6f2d0e59"
            ],
            "target_tag_ids": [],
            "target_trade_urls": [
                "https://steamcommunity.com/tradeoffer/new/?partner=39734289&token=AbCd1234"
            ],
            "strategy": "round_robin",
            "target_value": null,
            "items_limit_per_target": null
        }
    ],
    "created_at": "2026-07-30T14:02:11+00:00",
    "updated_at": "2026-07-30T14:02:11+00:00"
}

Validation is strict, because a bad ruleset would otherwise move items to the wrong place:

Error field Cause
name Missing, over 80 characters, or already used by one of your templates
rules Empty, over 50 rules, or naming a destination account, account tag or catalog item that does not exist
rules.N.action Missing, or not route / skip
rules.N.strategy A route rule with no strategy, or an unknown one
rules.N.target_value fill_value or top_up_value with no target value
rules.N.target_account_ids A route rule with no destination at all, in any of the three target fields
rules.N.price_max Lower than price_min
rules.N.target_trade_urls.N Not a https://steamcommunity.com/tradeoffer/new/?partner=…&token=… URL

An account belonging to someone else fails exactly like an id that never existed, so a template cannot be used to probe for other people's accounts.

Get one routing template

GET/api/v1/trade-routing-templates/{id}

One routing template, with its full ruleset.

API key required

Requires trading.read.

Bash
curl https://dashboard.steamlabs.dev/api/v1/trade-routing-templates/019fb432-77c1-71bd-9a04-4f0e2c9a8d31 \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"

Replace a routing template

PUT/api/v1/trade-routing-templates/{id}

Replace a template's name and its whole ruleset. PATCH does the same.

API key required

Requires trading.write.

Same body as create, same validation, and name and rules are both required. This is a full replacement, not a patch: the rules are an ordered first-match-wins list, so "change rule 3" has no meaning without the rules above it. Read the template, edit the array, send the whole thing back.

Bash
curl -X PATCH https://dashboard.steamlabs.dev/api/v1/trade-routing-templates/019fb432-77c1-71bd-9a04-4f0e2c9a8d31 \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Knives to the vault", "rules": [{"action": "skip", "price_max": 0.05}]}'

Answers 200 with the saved template. Sending a template back under its own name is fine; the uniqueness check ignores the record being edited.

Delete a routing template

DELETE/api/v1/trade-routing-templates/{id}

Delete a routing template. Returns 204.

API key required

Requires trading.write.

Work already queued is untouched. Applying a template copies its rules into the task, so a running distribution keeps the ruleset it started with.

Errors

On top of the universal codes, this group returns:

Code Status Meaning
offer_not_actionable 422 The offer is not an active offer awaiting one of your accounts. Adds state
offer_not_cancellable 422 Not an offer you sent, or no longer open. Adds state
trade_task_pending 409 A trade task is already queued or running for that account. Adds pending_task_id
identity_secret_missing 422 That account has no identity secret, so it cannot answer confirmations
confirmation_task_pending 409 A confirmation task is already queued or running for that account. Adds pending_task_id
no_matching_confirmations 422 None of your ids are in the latest snapshot. Adds known
maintenance_mode 503 Platform maintenance pauses sending offers. Answering incoming offers, confirmations and cancels keep working. Adds reason

The two _pending codes are the one-task-per-account guard, and they carry the task holding the account:

JSON
{
    "message": "A trade task is already queued or running for this account.",
    "code": "trade_task_pending",
    "pending_task_id": "019fb440-3f01-72c6-a1de-1c5ad0f7b9e2"
}

Poll that task until it finishes, then retry. Do not spin on the endpoint: it will keep saying the same thing until the worker is done with the account.

Two plan entitlements gate this group, and GET /api/v1/me reports both up front. allows_incoming_trades covers fetching and answering incoming offers (the refresh, respond, accept and decline endpoints); allows_confirmations covers fetching and answering mobile confirmations. When your plan lacks one, those endpoints answer 403 plan_limit_reached with the upgrade path attached. Cancelling an offer you already sent is never plan-gated: withdrawing your own trade always works.