Endpoints
Tasks
Queue any of the 44 task types across your fleet, then follow, retry, or cancel the work.
Tasks are what the platform actually does. Everything else (accounts, proxies, profiles, integrations) exists so that a task can run. One endpoint creates them, the rest let you watch, retry, and stop them.
Two scopes cover the domain. tasks.read for the catalogue, the listings and a single task, tasks.write for creating, retrying, cancelling and deleting. POST /api/v1/tasks/preview also needs tasks.write even though it writes nothing, so a read-only key cannot size up a selection.
Start at the task catalogue. It tells you the 44 types you may create, the exact config each one accepts, and which of them your plan covers. A client that guesses instead will cache the wrong shapes and find out through 422.
Selecting accounts
Creating tasks and previewing a selection both take a selection: which Steam accounts the work targets. There are two shapes, and you send exactly one.
{ "account_ids": ["019fb42e-9a61-70d2-818a-f6a56593f3a5", "019fb42e-9a7e-728d-b960-8b4c2162898c"] }An explicit list, capped at 1,000 ids. Over the cap you get 422 bulk_limit_exceeded with the cap in max.
{ "filters": { "guard": "mobile", "wallet": "has" }, "exclude_account_ids": ["019fb42e-9a7e-728d-b960-8b4c2162898c"] }A definition resolved on the server. The ids are never materialized, so "every account with a mobile authenticator" is the same size on the wire whether it names four accounts or forty thousand. A filter selection always runs asynchronously and always answers 202, even when it happened to resolve small enough to run inline.
Sending both is refused, and so is sending neither: a request with no selection would otherwise mean "every account I own" by accident.
Filter keys
| Key | Values |
|---|---|
search |
Username, persona name, or Steam64 id |
guard |
mobile, email, none |
wallet |
has, none |
details |
refreshed, never |
market |
eligible, restricted, unknown |
csfloat |
ready |
inventory |
synced, never |
session |
online, offline |
tags |
Array of tag ids. Matches an account carrying any of them |
exclude_tags |
Array of tag ids. Drops an account carrying any of them |
groups |
Array of account group ids. Matches an account in any selected group |
include_ungrouped |
Boolean. Includes accounts with no group and is ORed with groups |
capability |
Array of sell_steam, sell_csfloat, sell_marketcsgo, place_buy_orders. Matches any of them |
wallet_currency |
A wallet currency code, for example EUR. Send none to match accounts Steam has not opened a wallet for yet |
wallet_min, wallet_max |
Numeric balance bounds |
points_min, points_max |
Steam Points balance bounds. Accounts whose Points balance has never been refreshed match neither bound |
external_funds_min, external_funds_max |
Steam External Funds Used bounds, in USD. Accounts whose spend has never been refreshed match neither bound |
exclude_account_ids |
Ids to drop from the match, capped at 1,000. Only meaningful with filters |
wallet_min and wallet_max compare natively when wallet_currency is also set. Without it, balances are normalized to USD, because a bare number would otherwise compare 15 PLN against 15 EUR. An account whose balance has never been reported matches neither bound.
session is read from the live worker registry rather than a column. While no worker is reporting, the filter is ignored rather than matching nothing.
search, session, tags, exclude_tags, groups, and include_ungrouped are spelled the same in both vocabularies. For the other account-side filters, use GET /api/v1/accounts to get the ids and pass them as account_ids.
Task lifecycle
A task moves through six statuses.
| Status | Meaning |
|---|---|
| Waiting | Created but held back, waiting for fleet capacity. Large batches start here |
| Queued | Handed to the worker fleet, not picked up yet |
| Running | A worker has it |
| Completed | Finished, and it did what it was asked |
| Failed | Finished with an error. error says what went wrong |
| Cancelled | Stopped before it finished |
The last three are final. Nothing leaves them.
Parents and children
How many accounts are eligible decides the shape of what you get back.
- One eligible account: a single standalone task, bound to that account.
201with the task record. - Many eligible accounts: a batch parent with one child task per account. The parent has no
steam_account_idof its own and carries the rolled-up counters inchildren. - More than 500 eligible accounts, or any filter selection: the same batch parent, but built by a queued job.
202with just the parent's id.
"Eligible" excludes accounts that already have a pending, queued or running task of the same type, which is what stops a repeated call from double-queueing anyone. It also excludes accounts a type cannot work on: check_bans skips accounts with no steam64_id, assign_currency skips accounts that already have a wallet, place_buy_order skips accounts that cannot fund the order, change_password skips accounts missing a stored Steam password or either authenticator secret. Those show up as accounts_skipped.
When anything was skipped, the response also carries accounts_skipped_reasons: an object of reason slug to account count. An account failing several checks is counted once, against the first check it failed, so the counts sum to accounts_skipped (any remainder a reason could not be pinned on is simply not listed). The slugs are stable:
| Slug | The skipped accounts |
|---|---|
task_in_flight |
Already have a pending, queued or running task of this type |
nothing_to_list |
No inventory items matching the sell config (sell_items, any marketplace) |
no_authenticator |
No mobile authenticator, so listings cannot be confirmed (sell_items on Steam) |
market_restricted |
Known Community Market restriction (sell_items on Steam) |
not_ready_csfloat |
Not connected and enabled for CSFloat, or no mobile authenticator |
not_ready_marketcsgo |
Not connected and enabled for Market.CSGO, no dedicated proxy, or no mobile authenticator |
no_matching_units |
No storage units (or none matching the name) with anything to move (storage types) |
insufficient_funds |
Cannot fund price x quantity on top of open orders (place_buy_order) |
cannot_place_buy_orders |
Missing market access, mobile authenticator, or billing address (place_buy_order) |
no_wallet_currency |
Steam has not opened a wallet for the account, so there is no currency to pay in. An assign_currency task opens one for free (add_funds_paysafecard, add_funds_blik) |
unsupported_wallet_currency |
Wallet currency the payment method does not support (add_funds_paysafecard in tier mode; add_funds_blik outside PLN) |
topup_already_pending |
A wallet top-up is still awaiting payment; Steam cancels it if a new one is created, so pay or discard it first (add_funds_paysafecard, add_funds_blik) |
never_refreshed |
No steam64_id on record yet (check_bans) |
already_claimed |
Nothing left to claim: the account already has these games, or Steam does not offer them in its region (claim_free_games) |
wallet_already_assigned |
Already have a wallet currency (assign_currency) |
no_stored_password |
No Steam password on file (change_password) |
no_shared_secret |
No shared secret, so Steam Guard cannot log in or confirm (change_password) |
no_identity_secret |
No identity secret, so the recovery confirmation cannot be accepted (change_password) |
The field is absent when nothing was skipped, and on the few pipelines that do not attribute their skips (distribution, boosting, trade-ups).
Following a fan-out
curl "https://dashboard.steamlabs.dev/api/v1/tasks/019fb431-2c88-71ea-b0a3-8ce2f5d19b07" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"Poll the parent. It sits at Queued while its children are still being written, flips to Running once they all exist, and finishes when the last child does. children.total grows as the batch is built, so treat completed + failed + cancelled == total as done rather than watching total alone.
For per-account detail, use GET /api/v1/tasks/{id}/children. Do not page the whole child set just to count outcomes: the parent's counters already have them.
Discover what you can create
Every creatable type, its config schema, and whether your plan covers it.
/api/v1/task-typesThe task catalogue: 43 types, their config shapes, and your plan's access to each.
Requires tasks.read.
| Parameter | Type | Description |
|---|---|---|
page |
integer | Page number, default 1 |
per_page |
integer | Rows per page, default 50, clamped to 200. All 44 types fit on the first page |
curl "https://dashboard.steamlabs.dev/api/v1/task-types" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"import requests
types = requests.get(
"https://dashboard.steamlabs.dev/api/v1/task-types",
headers={"Authorization": f"Bearer {api_key}"},
).json()["data"]
creatable = {t["type"]: t["config_schema"] for t in types if t["allowed"]}const response = await fetch('https://dashboard.steamlabs.dev/api/v1/task-types', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const { data } = await response.json();
const creatable = data.filter((type) => type.allowed);{
"data": [
{
"type": "claim_weekly_drops",
"label": "Claim Weekly Drops",
"description": "Claim each account's two weekly CS2 drops, picked automatically by highest price or from your selection.",
"category": "cs2",
"requires_steam_account": true,
"allowed": true,
"blocked_by": null,
"plan": null,
"config_schema": {
"strategy": {
"type": "string",
"required": true,
"enum": ["highest_price", "manual"],
"default": "highest_price"
},
"ignore_graffitis": { "type": "boolean", "required": false, "default": false },
"claim_item_ids": {
"type": "array",
"required": false,
"max": 2,
"default": [],
"items": { "type": "string", "required": false, "max": 64 }
}
}
},
{
"type": "trade_up_contract",
"label": "Trade-Up Contract",
"description": "Craft 10 same-rarity skins into 1 skin of the next rarity, picked automatically by expected value or by hand.",
"category": "cs2",
"requires_steam_account": true,
"allowed": false,
"blocked_by": "plan",
"plan": {
"heading": "Not included in your plan",
"badge": "Available on Pro or higher",
"upgrade_url": "https://dashboard.steamlabs.dev/subscription"
},
"config_schema": { "…": {} }
}
],
"meta": { "page": 1, "per_page": 50, "total": 33, "last_page": 1 }
}Locked types are listed, not hidden, exactly as the panel greys them instead of dropping them. allowed is false and plan carries the same block a 403 plan_limit_reached would return, so an integration can tell "you have not bought this" apart from "this does not exist".
blocked_by says why a locked entry is locked: plan (buy more plan) or maintenance (wait it out). During platform maintenance every entry reports allowed: false with blocked_by: "maintenance", and the plan block's upgrade_url is null because no purchase lifts it.
category is one of account, profile, trading, inventory, market, cs2, tf2. requires_steam_account is false for the four orchestration types (distribute_items, store_items, withdraw_items, sell_items), whose parents group per-account children rather than reaching a worker themselves.
Reading a config schema
Each field in config_schema is described with the same descriptor the server validates against, so the two cannot drift.
| Key | Meaning |
|---|---|
type |
string, integer, number, boolean, array, or object |
required |
Always required |
required_if |
{ "field": "mode", "values": ["manual"] }: required when that sibling matches |
enum |
The allowed values |
min, max |
A numeric bound for numbers, a length for strings, an item count for arrays |
format |
An extra format rule, uuid or url |
default |
The panel's default, published so you can mirror it |
items |
The element shape of an array. For arrays of objects, items.properties holds the per-object fields |
Fields that are not required accept null, which means the same as leaving the key out.
List tasks
/api/v1/tasksYour tasks, newest first, paginated and filterable.
Requires tasks.read.
| Parameter | Type | Description |
|---|---|---|
status |
string | One status: pending, queued, running, completed, failed, cancelled |
type |
string | One task type value, for example sell_items |
parent |
uuid | Only children of this batch parent |
top_level |
boolean | true for parents and standalone tasks only, hiding batch children |
account_id |
uuid | Only tasks bound to this Steam account |
search |
string | Username, persona name or Steam64 id. Matches the task's own account, or for a batch parent any of its children's |
created_after |
date | Tasks created at or after this timestamp |
created_before |
date | Tasks created at or before this timestamp |
sort |
string | created_at (default), started_at, finished_at |
direction |
string | asc, desc (default) |
page, per_page |
integer | Default 50 per page, clamped to 200 |
curl "https://dashboard.steamlabs.dev/api/v1/tasks?top_level=true&status=running&per_page=1" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"$tasks = Http::withToken($apiKey)
->get('https://dashboard.steamlabs.dev/api/v1/tasks', [
'top_level' => true,
'status' => 'running',
])
->json('data');{
"data": [
{
"id": "019fb431-2c88-71ea-b0a3-8ce2f5d19b07",
"type": "refresh_details",
"status": "running",
"is_batch_parent": true,
"is_cancelling": false,
"parent_id": null,
"steam_account_id": null,
"steam_account": null,
"children": { "total": 4182, "completed": 3907, "failed": 12, "cancelled": 0 },
"error": null,
"worker_id": null,
"bytes_sent": null,
"bytes_received": null,
"cancel_requested_at": null,
"dispatch_after": null,
"started_at": "2026-07-30T14:02:11+00:00",
"finished_at": null,
"created_at": "2026-07-30T14:01:58+00:00",
"updated_at": "2026-07-30T14:19:40+00:00"
}
],
"meta": { "page": 1, "per_page": 1, "total": 1, "last_page": 1 }
}bytes_sent and bytes_received are what the worker measured on the wire while running the task. They are null when it reported nothing, which is not the same as zero, so skip those rows rather than coerce them when you add figures up. A task that was re-queued reports only what its final attempt moved. The same bytes roll up per proxy, account and source under bandwidth usage.
task_data and logs are deliberately absent from list rows. A storage batch's payload carries thousands of asset ids and a finished task's log is unbounded, so a 200-row page would be megabytes nobody asked for. Both are on the single-task response.
top_level=true is the filter you want for a dashboard. Without it, a 100,000-account batch puts 100,000 children in your list.
Preview a selection
Resolve a selection and count it, without creating anything.
/api/v1/tasks/previewHow many accounts a selection resolves to. Writes nothing.
Requires tasks.write. No Idempotency-Key: it writes nothing, so a retry costs one COUNT(*).
Takes a selection and nothing else. No type, no config.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/tasks/preview" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filters": {"guard": "mobile", "market": "eligible"}}'affected = requests.post(
"https://dashboard.steamlabs.dev/api/v1/tasks/preview",
headers={"Authorization": f"Bearer {api_key}"},
json={"filters": {"guard": "mobile", "market": "eligible"}},
).json()["accounts_affected"]
if affected > 500:
raise SystemExit(f"refusing to queue {affected} accounts"){ "accounts_affected": 4182 }The count is the selection, not the eligible set. POST /api/v1/tasks will usually create fewer tasks than this, because accounts that already have a task of the same type queued are skipped.
Cancel many tasks
/api/v1/tasks/cancelAsk a list of tasks to stop.
Requires tasks.write.
| Field | Type | Description |
|---|---|---|
task_ids |
array | Task ids, at least one, capped at 1,000 |
Explicit ids only. There is no filter mode here on purpose: these act on task rows you have just listed and can see, and "cancel everything queued" is a blast radius you should have to spell out.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/tasks/cancel" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"task_ids": ["019fb431-2c88-71ea-b0a3-8ce2f5d19b07", "019fb431-3d0f-7002-9a41-51c8b6f7e2aa"]}'{ "cancelled": 1, "skipped": 1 }skipped covers everything that did not move: tasks that finished between your listing and this call, tasks already cancelling, children of a live batch (cancel the parent instead), and ids that are not yours.
Create tasks
The endpoint everything else on this page exists to support.
/api/v1/tasksCreate tasks of one type across a selection of accounts.
Requires tasks.write. Requires an Idempotency-Key header. See Bulk operations.
| Field | Type | Description |
|---|---|---|
type |
string | One of the 33 values from GET /api/v1/task-types. Anything else is refused |
config |
object | The type's own options. Shape depends on type, see Config by task type |
account_ids |
array | Explicit account ids, capped at 1,000 |
filters |
object | A filter selection instead of ids |
exclude_account_ids |
array | Ids to drop from a filter selection, capped at 1,000 |
curl -X POST "https://dashboard.steamlabs.dev/api/v1/tasks" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"type": "sell_items",
"config": { "marketplace": "steam", "mode": "undercut", "undercut_cents": 1, "categories": ["container"], "max_items": 50 },
"filters": { "market": "eligible", "inventory": "synced" }
}'$response = Http::withToken($apiKey)
->withHeaders(['Idempotency-Key' => (string) Str::uuid()])
->post('https://dashboard.steamlabs.dev/api/v1/tasks', [
'type' => 'sell_items',
'config' => [
'marketplace' => 'steam',
'mode' => 'undercut',
'undercut_cents' => 1,
'categories' => ['container'],
'max_items' => 50,
],
'filters' => ['market' => 'eligible', 'inventory' => 'synced'],
]);
$taskId = $response->json('task_id') ?? $response->json('id');const response = await fetch('https://dashboard.steamlabs.dev/api/v1/tasks', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
type: 'refresh_details',
account_ids: ['019fb42e-9a61-70d2-818a-f6a56593f3a5'],
}),
});import uuid, requests
response = requests.post(
"https://dashboard.steamlabs.dev/api/v1/tasks",
headers={
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"type": "claim_weekly_drops",
"config": {"strategy": "highest_price", "ignore_graffitis": True},
"filters": {"guard": "mobile"},
},
)What you get back
201 Created, when explicit ids resolved to 500 or fewer eligible accounts. The task record (a standalone task, or the batch parent) plus two counters:
{
"id": "019fb440-7a10-7c33-bd51-2f0c9e4471d2",
"type": "refresh_details",
"status": "queued",
"is_batch_parent": false,
"is_cancelling": false,
"parent_id": null,
"steam_account_id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
"steam_account": {
"id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
"username": "farm_017",
"persona_name": "Ada",
"steam64_id": "76561198000000017"
},
"children": { "total": 0, "completed": 0, "failed": 0, "cancelled": 0 },
"error": null,
"worker_id": null,
"bytes_sent": null,
"bytes_received": null,
"cancel_requested_at": null,
"dispatch_after": null,
"started_at": null,
"finished_at": null,
"created_at": "2026-07-30T14:02:11+00:00",
"updated_at": "2026-07-30T14:02:11+00:00",
"tasks_created": 1,
"accounts_skipped": 0
}202 Accepted, for every filter selection and for any batch over 500 eligible accounts. The parent exists, its children are being written by a queued job:
{
"task_id": "019fb431-2c88-71ea-b0a3-8ce2f5d19b07",
"accounts_affected": 4182,
"accounts_skipped": 311,
"accounts_skipped_reasons": { "task_in_flight": 292, "never_refreshed": 19 }
}200 OK, only for a trade-up dry run (config.trade_up_dry_run: true). Guardrails were evaluated, nothing was created and nothing was consumed:
{
"dry_run": { "contracts": 42, "slots": 378, "cost": 118.44, "ev": 131.02, "scanned": 100, "total": 4182 }
}The dry run scans at most the first 100 accounts of the selection, so scanned and total differ on anything large.
When nothing is created
| Status | Code | Why |
|---|---|---|
422 |
no_eligible_accounts |
Every account in the selection already has a task of this type queued, or none met the type's requirements. Adds accounts_skipped, plus accounts_skipped_reasons saying which requirement each account failed |
422 |
nothing_to_plan |
The selection was fine, but the planner matched no items. Only for distribute_items, sell_items and the storage types |
422 |
proxies_required |
You own no proxies and hold more accounts than the shared pool covers, so these logins would be refused anyway |
403 |
plan_limit_reached |
Your plan's allowed_task_types does not cover this type, or its allowed_marketplaces does not cover the venue |
503 |
maintenance_mode |
Platform maintenance pauses new tasks. Adds reason. Transient, see Errors |
The two 422s are different answers and worth telling apart. no_eligible_accounts means "pick different accounts"; nothing_to_plan means "the accounts were fine, their inventories were not".
plan_limit_reached is not transient, and retrying never fixes it. GET /api/v1/task-types tells you which types are locked before you ask, and GET /api/v1/me reports allowed_task_types and allowed_marketplaces outright. The marketplace half bites sell_items (per its config.marketplace) and place_buy_order (always Steam).
What Steam is giving away
The giveaway catalogue behind claim_free_games: the paid games Steam is currently handing out permanently. Refreshed hourly from Steam's own store feed.
/api/v1/free-gamesSteam games that are currently free to keep.
Requires account.read, not tasks.read: this describes what Steam is offering, not anything you own or have queued.
curl https://dashboard.steamlabs.dev/api/v1/free-games \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"data": [
{
"app_id": 2000040,
"name": "Space Menace",
"package_id": 1827145,
"header_image": "https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/2000040/header.jpg",
"normal_price_cents": 699,
"ends_at": "2026-09-23T17:00:00+00:00",
"store_url": "https://store.steampowered.com/app/2000040/"
}
],
"meta": { "page": 1, "per_page": 50, "total": 1, "last_page": 1 }
}| Field | Type | Description |
|---|---|---|
app_id |
integer | The Steam app id, which is what a claim_free_games config takes |
name |
string | The game's store name |
package_id |
integer | The promotional package Steam offered our catalogue read. Informational: each account's own store page decides what it is actually offered |
normal_price_cents |
integer | What the game normally costs, always in USD so the figure means the same thing for every caller |
ends_at |
string | When the giveaway closes, or null when Steam does not publish a deadline we can read |
store_url |
string | The store page |
Most of the time this list is short, and often it is empty: giveaways come and go a few times a month. The list is read from one region, so an entry is an offer worth trying rather than a promise that a given account can take it.
You do not have to wait for this catalogue to notice a promotion. A claim_free_games task accepts any app id, and each account checks its own store page, so nothing is claimed unless Steam actually offers it there.
Config by task type
config is validated against the type you sent. Every type below is creatable; the shapes here are the same descriptors GET /api/v1/task-types publishes.
Throughout: min and max are a numeric bound on numbers, a character length on strings, and an item count on arrays. Fields not marked required may be omitted or sent as null, which mean the same thing.
Item conditions
Four types narrow which items they act on with the same shared block: sell_items, distribute_items (also inside each routing rule), store_items and withdraw_items.
| Field | Type | Description |
|---|---|---|
games |
array | cs2, tf2, steam. Absent on the storage types, whose units only ever hold CS2 items |
price_min |
number | Lowest per-item price to include |
price_max |
number | Highest per-item price to include |
item_ids |
array | Specific item ids, strings up to 191 characters |
categories |
array | weapon, knife, gloves, container, sticker, graffiti, charm, patch, agent, music_kit, collectible, tool, key, pass, gift, trading_card, booster_pack, emoticon |
origins |
array | Steam item origin ids, integers |
float_min |
number | sell_items only. Lowest CS2 paint wear to include, 0 to 1. Items without a float are skipped |
float_max |
number | sell_items only. Highest CS2 paint wear to include, 0 to 1 |
Leave the whole block out and the type acts on everything it can. That is fine for a storage deposit and expensive for a sale, which is why sell_items demands an explicit acknowledgement (see below). float_min and float_max are CS2-only and are refused when games is set and does not include cs2.
No configuration
These take no config at all. Send {} or leave the key out.
| Type | What it does |
|---|---|
refresh_details |
Logs in and refreshes Steam details, wallet, External Funds Used, Points Shop balance and inventory |
refresh_inventory |
Logs in and refreshes only the account's inventories, much lighter than a full details refresh |
refresh_wallet |
Logs in and refreshes only the wallet balance, pending funds and currency, much faster than a full details refresh |
refresh_point_shop_balance |
Logs in and refreshes the Points Shop balance and lifetime totals |
login |
Signs in and leaves the session open, so later tasks skip the login |
sign_out_everywhere |
Deauthorizes every Steam session on every device, ours included |
check_bans |
Reads VAC, community, game and trade ban state. No login needed. Accounts with no Steam64 id are skipped |
fetch_trade_url |
Re-reads the trade URL token and overwrites the stored one |
cancel_market_listings |
Cancels every active Steam market listing on the account |
sync_market_listings |
Pulls current listings, buy orders and wallet balance |
sync_market_history |
Pulls sales, purchases and cancelled listings |
cancel_buy_orders |
Cancels every open market buy order |
fetch_armory_progress |
Launches CS2 and reads pass stars, XP, credits, medal state and rank |
fetch_weekly_drops |
Launches CS2 and reads claimable weekly drops |
clear_armory_passes |
Removes completed armory passes to free pass slots |
claim_service_medal |
Claims the year's service medal where the rank allows it |
claim_community_badge |
Completes the free Steam community-badge quests Steam allows without spending wallet or points (Community Pillar and later levels of the same badge) |
Account and wallet
change_password sets a new Steam password on every eligible account through the logged-in help wizard and a mobile Steam Guard confirmation. Accounts without a stored password, shared secret, or identity secret are skipped. After Steam accepts the change, the stored password is updated and the session is cleared so the next task logs in fresh.
Two modes. Omit mode (or send password) to type one password for every account. Send mode: generate to mint a unique password per account at create time. Read a generated password from an account export or from the account Credentials tab after the task succeeds. The task itself never returns it.
| Field | Type | Description |
|---|---|---|
mode |
string | password (default) or generate. Omitted mode is password, so existing clients keep working |
password |
string | Required when mode is password or omitted. The new Steam password, 8 to 64 printable ASCII characters. Steam still rejects common or leaked passwords when the task runs. Ignored when mode is generate. Never returned on task reads |
length |
integer | Generate only. 8 to 64, default 16 |
lowercase |
boolean | Generate only. Default true |
uppercase |
boolean | Generate only. Default true |
digits |
boolean | Generate only. Default true |
symbols |
boolean | Generate only. Default false. Printable ASCII punctuation, no smart quotes |
At least one of lowercase, uppercase, digits, or symbols must be true when generating. The password is stored only so the worker can apply it. GET /api/v1/tasks/{id} omits it from task_data, including on a batch parent.
request_free_licenses registers the free licence for the games you name.
| Field | Type | Description |
|---|---|---|
mode |
string | all (default) or random. Omitted mode is all, so existing clients keep working |
app_ids |
array | Steam app ids, 1 to 200 of them, each a positive integer |
random_count |
integer | Random mode only. How many games to register per account, 1 to the number of app_ids you sent. Default 5 |
In all mode every account registers every app id you sent. In random mode each account
draws its own random_count games from that pool, so a fleet ends up with varied libraries
rather than one list repeated across every account. The draw happens when the tasks are
created, so each child task already names the games it will register and a retry repeats
that account's own draw. Games an account does not already own are drawn first, so the count
delivers new licences wherever there are new licences left to give.
This is the free-to-play door: it registers a free-on-demand licence, which works for permanently free titles such as CS2, Dota 2 or Team Fortress 2. It does not claim a limited giveaway. Steam accepts the request for one and grants nothing, without an error. Use claim_free_games for those.
claim_free_games claims the paid games Steam is currently giving away permanently, the store's one-click "Add to Account" button.
| Field | Type | Description |
|---|---|---|
mode |
string | picked (default) or all_active. Omitted mode is picked |
app_ids |
array | Required when mode is picked or omitted. Steam app ids, 1 to 25 of them, each a positive integer |
all_active claims every giveaway running at the moment the task is created, read from the giveaway catalogue. The list is resolved to concrete app ids before anything is queued, so the created task names the games it actually tried, and retrying it later claims that same set rather than whatever is free by then.
Per account, each game ends up in exactly one of four buckets, reported in the finished task's task_data:
| Bucket | Meaning |
|---|---|
claimed |
Added to the account. Each entry carries app_id and the granted package_id |
already_owned |
The account already had it, so nothing was posted. A list of app ids |
unavailable |
Steam offered the account no giveaway for that app: wrong region, or the promotion ended. Each entry carries app_id and a reason |
failed |
Steam refused the claim. Each entry carries app_id and Steam's own message |
unavailable is not an error. Giveaways are regional, so on a fleet whose proxies exit in different countries a real share of accounts land there, and those accounts are never asked about that game again. failed is treated as transient and can be retried.
Accounts with nothing left to do are skipped rather than queued, and reported under the already_claimed skip reason.
redeem_wallet_code redeems Steam wallet codes, one code per account.
| Field | Type | Description |
|---|---|---|
wallet_codes |
array | 1 to 10,000 codes, each 3 to 64 characters |
The batch is capped at the number of codes you sent: 40 codes across a 600-account selection creates 40 tasks and reports the rest as skipped. Codes are single use, which is the reason this endpoint carries an Idempotency-Key.
add_funds_paysafecard starts a wallet top-up and returns a payment link to finish off-site.
| Field | Type | Description |
|---|---|---|
mode |
string | tier (default) or exact |
tier |
string | tier_5, tier_10, tier_15, tier_25 (default), tier_50, tier_100. Required when mode is tier |
amount |
integer | 1 to 1,000,000, in the account's own wallet currency. Required when mode is exact. Default 5 |
A tier maps to the nearest denomination in each account's currency, so accounts in a currency with no denomination table are skipped rather than dispatched amountless.
Both modes need the account to already hold a wallet currency. Steam assigns one when the account's wallet is first opened, not from its store country, so an account that has never been funded has no currency to pay in even though the store shows prices in one. Those accounts are skipped as no_wallet_currency in either mode; run an assign_currency task on them first, which opens the wallet for free, then top up. You can list them with the account filter wallet_currency: "none".
add_funds_blik starts a Steam wallet top-up via Blik and returns a payment link to finish off-site. Blik is PLN only, so there are no tiers: one exact amount, applied to every selected account.
| Field | Type | Description |
|---|---|---|
amount |
integer | 1 to 1,000,000, in Polish zloty. Default 20 |
Accounts whose wallet currency is not PLN are skipped rather than dispatched. The wallet payment this produces has provider blik; open its pay_url (see the wallet payments endpoint) to complete the payment.
buy_package buys a Steam store package with the account's wallet balance.
| Field | Type | Description |
|---|---|---|
package |
string | cs2_prime (default) or custom |
custom_package |
string | Required when package is custom. A numeric package id, or a https://store.steampowered.com/app/… or /sub/… URL. Max 255 characters |
insufficient_funds_method |
string | paysafecard or blik. When the wallet cannot cover the price, pay the remainder via this provider instead of failing: Steam applies the wallet first and a payment link for the exact remainder appears under wallet payments (see the billing endpoint), with purpose purchase. The purchase completes once that link is paid. Omit for wallet-only behaviour, where an unaffordable price fails the task |
With an insufficient_funds_method set, accounts that already have a pending wallet payment are skipped (topup_already_pending): Steam allows one open checkout transaction per account, and a new one would silently cancel the pending payment. With blik, accounts whose wallet currency is not PLN are skipped (unsupported_wallet_currency), and accounts with no Steam wallet at all are skipped as no_wallet_currency.
assign_currency gives a walletless account a wallet currency, so market and checkout work can run.
| Field | Type | Description |
|---|---|---|
currency |
string | A wallet currency code, USD by default |
Only accounts with no wallet currency are eligible: Steam refuses to open a second wallet. A billing address for the currency's country is generated and stored on each account as part of the task.
buy_point_shop_items spends Steam Points on exact rewards or a filtered catalogue shortlist.
| Field | Type | Description |
|---|---|---|
mode |
string | exact (default) or automatic |
definition_ids |
array | 1 to 100 Points Shop definition ids. Required in exact mode |
classes |
array | Reward category ids, including 1 (seasonal badges). Leave empty to include all supported categories in automatic mode |
game_mode |
string | any (default) or specific |
app_ids |
array | Steam app ids, 1 to 100. Required when game_mode is specific |
minimum_item_cost |
integer | Lowest item cost to include, at least 0 |
maximum_item_cost |
integer | Highest item cost to include, at least the minimum |
sort |
string | cost_low (default), cost_high, name, or newest |
category_strategy |
string | catalogue_order (default) or one_each_first. Only affects automatic mode |
max_purchases_per_category |
integer | Optional successful-purchase limit for each matching reward category, 1 to 100. Only affects automatic mode |
owned_games_only |
boolean | Build the matching-item shortlist from games the selected accounts own, and skip rewards for games an account does not own. Default true. Only affects automatic mode |
budget_mode |
string | fixed_points (default), balance_percentage, or available_balance |
budget_value |
integer | Percentage of the live balance to spend, 1 to 100. Required for balance_percentage, default 50 |
max_points |
integer | Fixed budget or absolute cap, 1 to 10,000,000. Default 1000 |
minimum_remaining_points |
integer | Points that must remain after buying, 0 to 10,000,000. Default 0 |
max_purchases |
integer | Successful purchases allowed per account, 1 to 100. Default 10 |
max_attempts |
integer | Catalogue candidates tried per account, 1 to 100 and at least max_purchases. Default 50 |
vary_by_account |
boolean | Give each account a stable rotated candidate order. Default true |
equip_after_purchase |
boolean | Equip each purchased cosmetic on the profile. Default false |
The server freezes the matching definition ids and approved prices when you create the task. Every account uses that same immutable shortlist. The worker fetches each live definition again before buying and skips it when the price changed, the item disappeared, the account already owns it, or Steam says the account is not eligible.
Seasonal badges (community_item_class 1) use Steam's badge-level redeem, not the ordinary item redeem. One purchase can buy several levels of the same badge, one level per 1000 points on the current Summer and Winter collections, up to level 40. max_purchases counts each level. An account already at the cap is recorded as already owned. Only the collection Steam's Seasonal Badge page is selling right now (Summer from June through November, Winter from December through May) can be selected or redeemed. Older collections stay in Steam's catalogue as active, and the worker records them as unavailable instead of spending points.
When owned_games_only is on (the default) and at least one selected account has a synced library, the frozen shortlist is taken from those owned games, plus rewards that are not tied to a game, including seasonal badges (they use an event app id that no library owns). That stops cheapest-first from filling the list with rewards a CS2 farm cannot buy. Each account still drops anything it does not own at run time. Rewards that cost nothing and rewards not tied to a game stay eligible for that per-account filter, and an account whose library has never been refreshed is not filtered. Expired free promotions (no end date) are left out of matching-item plans. Currently running free giveaways stay in.
A run that buys nothing because every remaining candidate was unavailable, not eligible, or for a game the account does not own fails the task instead of completing. An account that only skipped items it could not afford, or already owned, still completes.
Two kinds of candidate are dropped before the account touches Steam, so they never consume an attempt and stay available to a later run. owned_games_only drops rewards belonging to a game the account has no licence for, because Steam always refuses those. Separately, a candidate whose frozen price is above what the account can spend is skipped without a lookup, so an account short on points finishes immediately instead of working through the whole shortlist. Both counts are reported on the task as point_shop_purchase.skipped.not_owned and point_shop_purchase.skipped.unaffordable.
Set category_strategy to one_each_first to prioritize one successful purchase from every matching reward category before buying another item from a category that is already covered. Failed ownership or eligibility checks move to another candidate in the missing category. A category is not guaranteed to complete when Steam rejects every matching candidate, the task reaches max_attempts, or the account runs out of spendable points.
When vary_by_account is also enabled, each account gets a stable variation of the category and item order while keeping the one-per-category priority.
Set max_purchases_per_category to stop one category from taking more than its share of the budget. For example, combine one_each_first with a per-category limit of 1 to buy at most one profile background, one avatar frame, and one item from every other matching category.
minimum_remaining_points is applied after the budget mode. For example, an account with 4,000 points, a 50% budget, and a 2,500 point reserve can spend at most 1,500 points. max_points is always a hard cap, including percentage and available-balance modes.
Set equip_after_purchase to put what was bought straight onto the profile. Four reward categories are profile slots: profile backgrounds, mini-profile backgrounds, avatar frames and animated avatars. A purchased seasonal badge is featured on the profile instead. Emoticons, stickers and chat effects are consumed per use, and startup movies are a Steam Deck setting, so none of those are ever equipped. A profile holds one item per slot, so when a run buys several for the same slot the most expensive one is equipped. Equipping happens after every purchase is recorded, and a failure to equip is reported in the task result without failing the task, because the points are already spent. The result gains an equipped array of {slot, community_item_id, equipped, error_message} (slot is a profile slot or favorite_badge), and the account's profile_items.equipped is refreshed by the next details refresh.
Profile
set_profile writes the profile, typed in once or drawn per account from your profile library.
| Field | Type | Description |
|---|---|---|
mode |
string | manual (default), profile, or group |
persona_name |
string | 2 to 32 characters. Required when mode is manual |
custom_url |
string | Steam vanity slug (steamcommunity.com/id/{slug}). Letters, numbers and underscores, 2 to 32 characters. Omit or null to leave the current custom URL alone. Library profile / group modes send this only on a profile's first-ever apply |
avatar |
string | Path of an already-stored avatar on the public disk. There is no binary upload on this endpoint |
summary |
string | Profile description, up to 8,000 characters |
country |
string | Two-letter country code |
profile_id |
uuid | A library profile. Required when mode is profile |
profile_group_id |
uuid | A profile group. Required when mode is group |
allow_reuse |
boolean | Default false. With reuse off, the batch is capped at the number of unused profiles in the pool |
change_profile_privacy flips Steam visibility settings. Every field is optional and an omitted field is left unchanged, so one call can change a single setting across the fleet.
| Field | Type | Description |
|---|---|---|
profile |
integer | 1 private, 2 friends only, 3 public |
owned_games |
integer | 1, 2, 3 |
inventory |
integer | 1, 2, 3 |
friends_list |
integer | 1, 2, 3 |
inventory_gifts |
integer | 1 private or 3 public. No friends-only step |
playtime |
integer | 1 private or 3 public |
comment_permission |
integer | 0 friends, 1 public, 2 private |
Send at least one. A config with none would read every account's settings and write them back unchanged, so it is refused with an error on config.profile.
equip_profile_items puts cosmetics an account already owns onto its profile.
| Field | Type | Description |
|---|---|---|
slots |
array | Any of profile_background, mini_profile_background, avatar_frame, animated_avatar. Defaults to all four |
strategy |
string | newest (default), random, or name |
name_contains |
string | Up to 120 characters. Required when strategy is name |
skip_if_equipped |
boolean | Leave slots that already hold something. Default true |
There is no field for a specific item, and that is deliberate: every account owns a different set, so the item is resolved per account when the task runs. newest takes the most recently acquired, random picks one, and name keeps only items whose name contains your text, with the newest of those winning a tie.
A profile holds one item per slot. A slot the account owns nothing for is reported and skipped, never treated as a failure, and with skip_if_equipped on a repeat run only fills the gaps. One slot Steam refuses does not stop the others; the task only fails when every slot it tried was refused.
The result carries a slots array of {slot, outcome, community_item_id, name}, where outcome is equipped, already_equipped, no_match, owns_nothing, or failed. After anything is equipped the account's profile_items.equipped is re-read from Steam and updated, so it reflects the change without waiting for a details refresh.
set_profile_theme sets the colour theme on each Steam profile. This is the chip row on Steam's Profile settings (Default, Summer, Midnight, Steel, Cosmic, Dark mode), not a Points Shop item and not part of set_profile. Steam writes it through IPlayerService/SetProfileTheme, a different surface from the community profileSave that name, summary, country and custom URL use.
| Field | Type | Description |
|---|---|---|
mode |
string | selected (default) or random |
theme |
string | Required when mode is selected. One of default, Summer, Midnight, Steel, Cosmic, DarkMode |
themes |
array | Required when mode is random. One or more of those same ids |
selected applies the same colour to every account. random picks one colour per account from themes when the task is created, so a retry reapplies the same choice. default is Steam's unstyled profile (an empty theme_id on the wire).
The result is {mode, theme, themes}, where theme is the colour that was written.
set_favorite_badge features one of the badges an account has earned on its profile.
| Field | Type | Description |
|---|---|---|
strategy |
string | recommended (default), highest_level, or random |
skip_if_set |
boolean | Leave accounts that already feature a badge. Default true |
As with equipping, there is no field for a specific badge: each account has earned a different set, so it is ranked per account at run time. recommended takes the years-of-service badge first, then the highest seasonal sale badge, then the owned-games badge, and features nothing if the account has none of those. highest_level ignores the type and takes the highest level, then the highest XP. random picks uniformly among the account's Steam-wide badges, so a fleet run does not clone one badge onto every profile.
Only Steam-wide badges are eligible. Per-game badges are addressed by a different identifier that SteamLabs does not write, so they are never featured.
The result is {strategy, outcome, badge_id, badge_name, level, reason}, where outcome is set, already_set, no_badges, or no_match, and reason says which rule picked the badge: years_of_service, seasonal_sale, owned_games, highest_level, or random. badge_name may be null when Steam's badge page could not be read; the ranking still works, though the seasonal-sale step is skipped, since that is the one step that needs names.
claim_community_badge is a different job from featuring a badge or redeeming a paid Points Shop sale badge. Steam awards Community Pillar (and later levels of the same community badge) when enough quests on /badges/2 are done. There is no one-click claim RPC. The worker reads Steam's official quest-progress list (and the badge page for the title and the "N of 28" counts) and runs the remaining quests that have a stable web endpoint and spend nothing: Discovery Queue, discussions search, wishlist, workshop rate or subscribe (then unsubscribe), view a broadcast, join the Steam Trading Cards group, post then delete a status, comment then delete on the account's own profile, activity-feed upvote, set a real name when it is empty, and feature a badge if none is featured. Viewing a guide only counts from the in-game overlay, so that quest is skipped.
It skips anything that costs wallet or points, needs another account, writes a public review, or is account-security / market work (trade, market, phone, Steam Guard, 2FA, craft cards, screenshots, videos, add a friend, play a game, chat emoticon, avatar upload, profile background). Limited accounts can finish fewer quests. Steam still awards the next badge level when the remaining count is high enough.
The result is {outcome, badge_id, badge_name, level_before, level_after, completed_before, completed_after, total, quests}, where outcome is completed, already_complete, or partial, and each quest is {id, outcome, detail?}. A repeat run is cheap: finished quests are reported as already_done and not retried.
Inventory and storage
store_items deposits matching CS2 items into the account's storage units.
| Field | Type | Description |
|---|---|---|
store_mode |
string | all (default) stores everything matching, keep leaves some in the inventory |
keep_count |
integer | How many matching items stay out. Required when store_mode is keep |
unit_mode |
string | auto (default) picks units with space, named targets units by name |
unit_name |
string | Up to 20 characters. Required when unit_mode is named |
max_items |
integer | Cap on items moved per account |
| item conditions | The shared block above, without games |
withdraw_items moves matching items back out of storage.
| Field | Type | Description |
|---|---|---|
unit_mode |
string | all (default) or named |
unit_name |
string | Up to 20 characters. Required when unit_mode is named |
max_items |
integer | Cap on items moved per account |
| item conditions | The shared block above, without games |
rename_storage_unit renames storage units across accounts.
| Field | Type | Description |
|---|---|---|
unit_mode |
string | all (default) or named |
unit_name |
string | Which units to rename. Required when unit_mode is named |
new_name |
string | The new name, up to 20 characters |
distribute_items sends items to one destination, or spreads them with routing rules.
| Field | Type | Description |
|---|---|---|
mode |
string | simple (default) or advanced |
destination_type |
string | own (default) or external. Required when mode is simple |
destination_account_id |
uuid | The receiving account. Required when destination_type is own |
trade_url |
string | A full https://steamcommunity.com/tradeoffer/new/?partner=…&token=… URL, up to 500 characters. Required when destination_type is external |
rules |
array | Routing rules, at least one. Required when mode is advanced |
auto_accept |
boolean | Accept the offer on the receiving side. Default true |
message |
string | Trade offer message, up to 128 characters |
| item conditions | The shared block above, with games |
Each entry of rules is an object:
| Field | Type | Description |
|---|---|---|
action |
string | route (default) sends the matching items, skip leaves them alone |
strategy |
string | round_robin (default), random, fill_value, top_up_value |
target_account_ids |
array | Receiving accounts you own |
target_trade_urls |
array | Receiving trade URLs, up to 500 characters each |
target_value |
number | Dollars fill_value sends per target this run (a ceiling), or the inventory total top_up_value brings each target up to (a floor, so the last item can land a target slightly over) |
items_limit_per_target |
integer | Cap on items per target |
| item conditions | The shared block above, with games, scoped to this rule |
Market
sell_items lists matching marketable items, priced live per item.
| Field | Type | Description |
|---|---|---|
marketplace |
string | steam (default), csfloat, marketcsgo, skinland, dmarket, skinport, assetpay. Decides which pipeline runs and which plan entitlement is checked |
mode |
string | undercut (default), reference_percent, buy_order, top_bid, manual. Required on every listing venue. Omit it on skinland. On assetpay the modes are instant (default, sold outright at AssetPay's quote), instant_markup (listed on AssetPay's store at percent of the instant price), market_percent (listed at percent of another market's price, see reference_source) and manual |
undercut_cents |
number | How far under the lowest listing to price. Default 1. At least 1 on every venue except market.csgo, which prices in tenths of a cent and accepts one decimal place down to 0.1 |
percent |
number | Percentage of the reference price, 1 to 500. Required when mode is reference_percent. Default 100. On assetpay with instant_markup it is the percentage of the instant price to list at, 100 or more, and required |
reference_source |
string | assetpay with market_percent only. csfloat (default), steam_market or marketcsgo. The listing never goes below the instant price; items with no price from that market are skipped |
instant_sell_after_days |
integer | assetpay listing modes only. Days after which an unsold listing sells to AssetPay at the instant price, 1 to 365. Omit to keep listings up until they sell |
floor_percent |
number | Refuse to list below this percentage of the reference price, 1 to 100. Default 70. Ignored when mode is manual |
manual_price_cents |
integer | The exact buyer-pays price in cents, at least 3. Enough for a single named item when mode is manual |
manual_prices_cents |
object | Exact prices keyed by catalog item id. Required when mode is manual and item_ids names more than one item. Every named item needs its own cents value |
manual_currency |
string | The wallet currency the exact prices are quoted in, for example PLN. Default USD |
manual_convert |
boolean | Convert the prices into each account's own wallet currency instead of requiring a match. Default false |
max_items |
integer | Cap on items listed per account |
float_min |
number | Lowest CS2 paint wear to include, 0 to 1. Items without a float are skipped |
float_max |
number | Highest CS2 paint wear to include, 0 to 1 |
confirm_sell_all |
boolean | Acknowledges an unbounded sale. Default false |
auto_reprice |
boolean | Enroll successfully created CSFloat, market.csgo, or DMarket listings into automatic repricing. undercut mode only. Default false |
auto_reprice_interval_minutes |
integer | Automatic check cadence: 15, 30, 60, or 240. Default 30 |
| item conditions | The shared block above, with games |
Rules the sell form enforces visually apply here too:
buy_orderpricing is only available onsteam, andtop_bidonly onmarketcsgoanddmarket. The wrong pairing fails onconfig.mode.skinlandbuys at its own quote, so it takes nomode. Sending one fails onconfig.mode. Leaving it off is how you sell there.- A config with no item condition, no
max_itemsand noconfirm_sell_allwould list every marketable item on every selected account. It is refused onconfig.confirm_sell_alluntil you set that flag totrue. manualneeds named catalog items, soconfig.item_idsmust name at least one. An empty list is refused onconfig.item_ids.- A mixed
item_idslist needsmanual_prices_centswith a cents value for every named item. One sharedmanual_price_centsis refused onconfig.manual_prices_cents. manual_convertis Steam-only.csfloatandmarketcsgoquote in USD and have no per-account wallet to convert into, so pairing them fails onconfig.manual_convert.auto_repricesupports CSFloat, market.csgo and DMarket and requiresmode: "undercut"plus afloor_percent. Each successful listing captures its own absolute USD floor. Listings whose venue reference price is missing are still created but are not enrolled.
Exact pricing across accounts
mode: "manual" lists at prices you type rather than prices derived from the order book. Name the items under item_ids. One item can use manual_price_cents. Several items need manual_prices_cents keyed by those same ids. Copies of one item across accounts share that item's price. Two things then decide how Steam spends the numbers:
manual_currency says what the number means. Steam wallets each have their own currency, so 707 is 7.07 zł on a PLN wallet and $7.07 on a USD one. Accounts whose live Steam wallet is not in manual_currency are skipped, and their items come back under the task's skipped list with reason wallet_currency_mismatch.
manual_convert: true trades exactness for coverage: nothing is skipped, and each account lists at that value converted into its own wallet currency using the item's own order book as the rate. Items whose rate cannot be derived are skipped with no_fx_reference.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/tasks" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"type": "sell_items",
"config": {
"marketplace": "steam",
"mode": "manual",
"manual_price_cents": 707,
"manual_currency": "PLN",
"manual_convert": false,
"item_ids": ["<item-id>"]
},
"filters": { "market": "eligible", "inventory": "synced" }
}'$response = Http::withToken($apiKey)
->withHeaders(['Idempotency-Key' => (string) Str::uuid()])
->post('https://dashboard.steamlabs.dev/api/v1/tasks', [
'type' => 'sell_items',
'config' => [
'marketplace' => 'steam',
'mode' => 'manual',
'manual_price_cents' => 707,
'manual_currency' => 'PLN',
'manual_convert' => false,
'item_ids' => ['<item-id>'],
],
'filters' => ['market' => 'eligible', 'inventory' => 'synced'],
]);place_buy_order places the same buy order across accounts.
| Field | Type | Description |
|---|---|---|
game |
string | cs2 (default), tf2, steam |
market_hash_name |
string | The exact item name, up to 255 characters |
pricing_mode |
string | fixed (default) or outbid |
price |
number | Unit price, at least 0.03. Required when pricing_mode is fixed |
max_price |
number | Ceiling when outbidding, at least 0.03. Required when pricing_mode is outbid |
quantity |
integer | 1 to 1,000. Default 1 |
Only accounts that can actually place buy orders are eligible: market eligible, mobile authenticator, billing address on file, and enough wallet balance to fund price x quantity on top of their already-open orders (Steam caps open orders at ten times the balance). Everything else is reported as accounts_skipped, split by reason in accounts_skipped_reasons (insufficient_funds, cannot_place_buy_orders, task_in_flight). In outbid mode the price is resolved per wallet currency at creation time.
CS2 armory and drops
buy_store_items buys from the in-game CS2 store with the account's wallet balance.
| Field | Type | Description |
|---|---|---|
item |
string | armory_pass (default), storage_unit, name_tag |
quantity |
integer | At least 1, default 1. Capped per item: 5 armory passes, 10 storage units, 10 name tags |
activate_after_purchase |
boolean | Activate armory passes as soon as they are bought. Default true |
buy_armory_items spends armory credits, split by weight.
| Field | Type | Description |
|---|---|---|
items_to_buy |
array | At least one allocation line |
total_balance_percentage |
integer | How much of the credit balance to spend, 1 to 100. Default 100 |
Each line is { "item": "…", "percentage": 25 }, where item is one of fever_case, arabesque_collection, spy_tech_collection, overpass_2024_collection, auto_racing_stickers, fruits_and_veggies_stickers, community_2025_stickers, missing_link_charms, missing_link_community_charms, dr_boom_charms, small_arms_charms, and percentage is 1 to 100.
activate_armory_passes activates bought-but-inactive passes.
| Field | Type | Description |
|---|---|---|
max_to_activate |
integer | 1 to 1,000. Leave it out to activate every inactive pass |
claim_weekly_drops claims the account's two weekly CS2 drops.
| Field | Type | Description |
|---|---|---|
strategy |
string | highest_price (default) picks for you, manual claims what you name |
ignore_graffitis |
boolean | Skip graffiti when picking. Default false |
claim_item_ids |
array | At most 2 ids, strings up to 64 characters. Steam allows two claims a week |
Run fetch_weekly_drops first if you want to review the options before claiming.
trade_up_contract crafts 10 same-rarity skins into 1 of the next rarity.
| Field | Type | Description |
|---|---|---|
trade_up_mode |
string | clear_space (default) burns cheap inventory, best_value chases expected value |
trade_up_max_item_price |
number | Most expensive input to consume, at least 0.01. Required when trade_up_mode is clear_space. Default 0.10 |
trade_up_max_contracts |
integer | Contracts per account, 1 to 100. Default 25 |
trade_up_min_ev |
number | Minimum expected value, -100 to 1000. Default 8 |
trade_up_max_input_cost |
number | Ceiling on the combined input cost of one contract |
trade_up_rarities |
array | Input rarities, at least one of 1 consumer, 2 industrial, 3 mil-spec, 4 restricted, 5 classified. Default [1, 2] |
trade_up_stattrak |
string | any (default), normal, stattrak |
trade_up_dry_run |
boolean | Evaluate the guardrails and report a digest without crafting. Default false |
Planning has side effects: a real run reserves the input items and writes ledger rows per account. Accounts with no viable contract under your guardrails are skipped. Set trade_up_dry_run to true first, which answers 200 with the digest and consumes nothing.
TF2
buy_tf2_store_items buys from the in-game TF2 store with the account's wallet balance.
| Field | Type | Description |
|---|---|---|
item |
string | backpack_expander (default) |
quantity |
integer | At least 1, default 1, capped at 10 |
use_after_purchase |
boolean | Apply the item as soon as it is bought. Default true |
use_tf2_items applies unused items already sitting in the backpack.
| Field | Type | Description |
|---|---|---|
item |
string | backpack_expander (default) |
max |
integer | At least 1. Leave it out to use every matching item |
Delete many tasks
/api/v1/tasksDelete a list of tasks. Unfinished ones are cancelled first.
Requires tasks.write.
| Field | Type | Description |
|---|---|---|
task_ids |
array | Task ids, at least one, capped at 1,000 |
curl -X DELETE "https://dashboard.steamlabs.dev/api/v1/tasks" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"task_ids": ["019fb431-2c88-71ea-b0a3-8ce2f5d19b07"]}'{ "deleted": 1, "skipped": 0 }Unfinished tasks are flagged cancelled before the rows go, so the fleet drops the work instead of erroring on a missing row. Deleting a batch parent takes its children with it.
skipped counts ids that could not be deleted: children of a batch that has not finished (their results still feed the parent's counters), and ids that are not yours.
Clear finished tasks
/api/v1/tasks/finishedDelete every completed, failed, and cancelled top-level task.
Requires tasks.write. No request body.
curl -X DELETE "https://dashboard.steamlabs.dev/api/v1/tasks/finished" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{ "deleted": 42 }The one-click history cleanup, matching the Tasks page's "Clear finished" button. Every finished top-level task goes, and a deleted batch parent takes its children with it (deleted counts top-level rows only). Queued and running tasks are never touched: to remove live work, cancel it or name its ids in Delete many tasks.
One task
/api/v1/tasks/{id}One task in full, including the worker payload and its logs.
Requires tasks.read.
curl "https://dashboard.steamlabs.dev/api/v1/tasks/019fb440-7a10-7c33-bd51-2f0c9e4471d2" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"id": "019fb440-7a10-7c33-bd51-2f0c9e4471d2",
"type": "claim_weekly_drops",
"status": "failed",
"is_batch_parent": false,
"is_cancelling": false,
"parent_id": "019fb431-2c88-71ea-b0a3-8ce2f5d19b07",
"steam_account_id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
"steam_account": {
"id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
"username": "farm_017",
"persona_name": "Ada",
"steam64_id": "76561198000000017"
},
"children": { "total": 0, "completed": 0, "failed": 0, "cancelled": 0 },
"error": "The Game Coordinator session timed out.",
"worker_id": "worker-eu-14",
"bytes_sent": 1204482,
"bytes_received": 12044820,
"cancel_requested_at": null,
"dispatch_after": null,
"started_at": "2026-07-30T14:02:44+00:00",
"finished_at": "2026-07-30T14:04:02+00:00",
"created_at": "2026-07-30T14:02:11+00:00",
"updated_at": "2026-07-30T14:04:02+00:00",
"task_data": { "strategy": "highest_price", "ignore_graffitis": true },
"point_shop_purchase": null,
"logs": [
{ "ts": 1785420164, "level": "info", "msg": "Signing in to Steam" },
{ "ts": 1785420191, "level": "info", "msg": "Launching CS2" },
{ "ts": 1785420242, "level": "error", "msg": "GC session timed out after 45s" }
],
"worker_public_ip": "185.22.10.4",
"proxy": { "id": "019fb42f-1188-70aa-8f2c-3b7de51a9c60", "host": "res-eu-01.example.net", "port": 8080 }
}Four fields appear only here: task_data (the payload the worker was handed, already resolved per account), point_shop_purchase (the purchase summary and 20 most recent attempts, or null), logs (the last 200 lines the worker sent back), and proxy (which proxy it went through).
For buy_point_shop_items, point_shop_purchase.summary contains purchased, attempted, points_spent, and accounts, and point_shop_purchase.skipped contains unaffordable and not_owned for the candidates that never became attempts. Each recent attempt includes its definition id, title, approved and live costs, balances, outcome, Steam eresult, grant ids, and timestamps. A batch parent rolls up every child that shares its purchase plan.
A task id that belongs to someone else returns 404, the same as an id that does not exist. We do not distinguish, because confirming an id exists elsewhere leaks that it exists.
A batch's children
/api/v1/tasks/{id}/childrenA batch parent's per-account child tasks.
Requires tasks.read.
| Parameter | Type | Description |
|---|---|---|
status |
string | One status value |
search |
string | Username, persona name or Steam64 id |
sort |
string | created_at (default), started_at, finished_at |
direction |
string | asc, desc (default) |
page, per_page |
integer | Default 50 per page, clamped to 200 |
curl "https://dashboard.steamlabs.dev/api/v1/tasks/019fb431-2c88-71ea-b0a3-8ce2f5d19b07/children?status=failed" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"Rows are the same shape the list endpoint returns. GET /api/v1/tasks?parent={id} gives you the identical set with the full filter vocabulary, if you need type or a date range as well.
Call this with status=failed to see which accounts failed and why. To act on them, use retry-failed on the parent rather than looping the children.
Retry a failed task
/api/v1/tasks/{id}/retryRe-run one failed account-bound task.
Requires tasks.write. Requires an Idempotency-Key header.
No body. The task's own resolved task_data becomes the config, so a random persona name re-rolls and an already-planned trade-up re-dispatches verbatim.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/tasks/019fb440-7a10-7c33-bd51-2f0c9e4471d2/retry" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"Answers 201 with the new task, exactly as POST /api/v1/tasks does for a single account.
The task must be Failed, bound to a Steam account, and not a batch parent. Anything else gets 422 task_not_retryable. A failed child of a finished batch qualifies; the parent itself does not.
Points Shop purchases have a stricter rule. A task with a pending, purchased, or unknown attempt returns 409 point_shop_purchase_retry_unsafe. Create a new task after checking the account instead. This prevents a retry from buying the same reward twice after Steam completed an earlier request.
Retry a batch's failures
/api/v1/tasks/{id}/retry-failedRe-run every failed child of a finished batch.
Requires tasks.write. Requires an Idempotency-Key header.
No body. The failed set travels as a filter, never as ids, so a 100,000-child batch does not have to materialize its failures before it can retry them.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/tasks/019fb431-2c88-71ea-b0a3-8ce2f5d19b07/retry-failed" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"{
"task_id": "019fb44a-51e7-7ab8-9f30-6ca7ce31f008",
"accounts_affected": 12,
"accounts_skipped": 0
}Always 202 with a new batch parent, because you cannot know how many children failed. The original parent is left alone.
The parent must be finished, have at least one failed child, and still carry the selection it was created with. Otherwise you get 422 task_not_retryable.
Points Shop purchase batches always return 409 point_shop_purchase_retry_unsafe here. Create a new task for the accounts you want to run again, so the catalogue and prices are frozen into a new plan.
Cancel one task
/api/v1/tasks/{id}/cancelAsk a task to stop.
Requires tasks.write. No body, and no Idempotency-Key: cancelling twice changes nothing.
Cancelling is a request, not a transition, which is why this answers 200 with the record rather than 204:
- a Waiting or Queued task flips to Cancelled straight away;
- a Running task only gets
cancel_requested_atstamped, and the worker decides the final status. It may still finish and report Completed.
Read status and cancel_requested_at off the response to tell which of the two happened. While the request is outstanding, is_cancelling is true.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/tasks/019fb431-2c88-71ea-b0a3-8ce2f5d19b07/cancel" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"Cancelling a batch parent cancels its children. Only top-level unfinished tasks with no cancel already pending can be cancelled; anything else gets 422 task_not_cancellable.
Delete one task
/api/v1/tasks/{id}Delete a task. Returns 204.
Requires tasks.write.
An unfinished task is flagged cancelled before the row goes, so the fleet drops the work. A batch parent takes its children with it.
A child of an unfinished batch cannot be deleted on its own: its result feeds the parent's counters, and removing the row would strand the parent running forever. That is 422 task_not_deletable. Cancel or delete the parent instead.
Errors
On top of the universal codes, this group returns:
| Status | Code | Meaning |
|---|---|---|
422 |
no_eligible_accounts |
Nobody in the selection could take this task type. Adds accounts_skipped and, where the pipeline attributes its skips, accounts_skipped_reasons |
422 |
nothing_to_plan |
The planner matched no items for distribute_items, sell_items or a storage type |
422 |
proxies_required |
You own no proxies and hold more accounts than the shared pool covers |
422 |
task_not_retryable |
Not a failed account-bound task, or not a finished batch with failed children and a stored selection |
422 |
task_not_cancellable |
Already finished, already cancelling, or a child covered by cancelling its parent |
422 |
task_not_deletable |
A child of a batch that has not finished yet |
422 |
bulk_limit_exceeded |
More than 1,000 ids in account_ids, exclude_account_ids or task_ids. Adds max |
409 |
point_shop_purchase_retry_unsafe |
A Points Shop task may already have bought something, or a Points Shop batch was sent to retry-failed. Create a new task after checking the accounts |
403 |
plan_limit_reached |
Your plan's allowed_task_types or allowed_marketplaces does not cover this. Adds plan with the upgrade path |
503 |
maintenance_mode |
Platform maintenance pauses creation (POST /api/v1/tasks, retry, retry-failed). Reads, cancels and deletes keep working. Adds reason |
400 |
idempotency_key_required |
POST /api/v1/tasks, retry and retry-failed need the header |
Everything else (validation_failed, not_found, missing_scope, rate_limit_exceeded, the idempotency conflicts) behaves as described on Errors.
Validation failures on a config report per field under config.*, so a bad sell mode reads as config.mode rather than a bespoke envelope:
{
"message": "The given data was invalid.",
"code": "validation_failed",
"errors": {
"config.mode": ["The buy_order pricing mode is only available on the steam marketplace."],
"config.confirm_sell_all": ["This config has no item condition and no cap, so it would list every marketable item on every selected account. Set confirm_sell_all to true to accept that."]
}
}