Endpoints
Steam accounts
List, filter, create and act on your Steam accounts from code, one at a time or a hundred thousand at once.
Everything the Accounts page does, over HTTP: add accounts, filter the fleet the way the table filters it, and run any of its bulk actions against a selection.
Two scopes cover the domain. accounts.read for the listings and the read-only sub-resources, accounts.write for everything that changes an account or queues work for one. Tags share them, because a tag is just a label on an account.
Credentials are write-only
No response ever contains a password, shared secret, identity secret or session token. Not masked, not truncated, not the last four characters.
You already have the credential (you sent it), so returning it buys you nothing, while a leaked read-only key would otherwise hand over a working set of Steam logins. What you get instead is whether one exists:
{
"steam_guard": { "enabled": true, "two_factor": true, "has_shared_secret": true, "has_identity_secret": false },
"session": { "online": true, "has_stored_session": true, "login_state": null }
}Send a new value to change a secret, or null to clear it. Omitting a field leaves it alone.
List accounts
/api/v1/accountsYour Steam accounts, paginated and filterable.
curl "https://dashboard.steamlabs.dev/api/v1/accounts?ban_status[]=vac&per_page=1" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"data": [
{
"id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
"username": "farm_017",
"persona_name": "Ada",
"steam64_id": "76561198000000017",
"steam_partner_id": "39734289",
"avatar_url": "https://avatars.steamstatic.com/…_full.jpg",
"summary": null,
"country": "NL",
"store_country": "NL",
"email": "[email protected]",
"email_verified": true,
"profile_configured": true,
"profile_needs_setup": false,
"trade_url": "https://steamcommunity.com/tradeoffer/new/?partner=39734289&token=AbCd1234",
"steam_guard": { "enabled": true, "two_factor": true, "has_shared_secret": true, "has_identity_secret": true },
"session": {
"online": true,
"has_stored_session": true,
"stored_session_expires_at": "2026-08-27T09:15:00+00:00",
"login_state": null,
"login_state_at": null
},
"wallet": { "balance_cents": 1234, "currency": "EUR" },
"market": {
"eligibility": "eligible",
"allowed": true,
"unlocks_at": null,
"restriction_reason": null,
"checked_at": "2026-07-30T14:02:11+00:00"
},
"bans": {
"banned": false,
"vac": false,
"community": false,
"game": false,
"economy": false,
"vac_count": 0,
"game_count": 0,
"days_since_last_ban": null,
"checked_at": "2026-07-30T14:02:11+00:00"
},
"inventory": { "item_count": 412, "synced_item_count": 412, "value_cents": 18355, "refreshed_at": "2026-07-30T13:00:00+00:00" },
"cs2": {
"level": 21,
"xp": 4820,
"armory_balance": 300,
"armory_pass_count": 1,
"armory_unactivated_pass_count": 0,
"armory_progress_percentage": 42.5,
"weekly_drops": null,
"service_medal_claimable": false,
"service_medal_claimed": true,
"refreshed_at": "2026-07-30T12:00:00+00:00"
},
"tf2": { "premium": false, "additional_backpack_slots": null, "trade_ban_expires_at": null, "refreshed_at": null },
"boost": { "minutes_total": 12400, "state": "boosting", "plan_id": "019fb431-…" },
"proxies": { "pinned_count": 1, "group_count": 0 },
"tags": [ { "id": "019fb432-…", "name": "prime", "color": "#3b82f6" } ],
"billing_address": { "first_name": "Ada", "last_name": "Lovelace", "address": "…", "city": "Amsterdam", "country": "NL", "postal_code": "1011AB" },
"billing_address_is_custom": false,
"privacy_settings": { "profile": 3, "inventory": 3 },
"details_refreshed_at": "2026-07-30T14:02:11+00:00",
"ownership_refreshed_at": "2026-07-29T08:00:00+00:00",
"created_at": "2026-07-01T09:15:00+00:00",
"updated_at": "2026-07-30T14:02:11+00:00"
}
],
"meta": { "page": 1, "per_page": 1, "total": 1, "last_page": 1 }
}session.online is null rather than false when no worker is reporting. That means "we cannot see", not "offline", and it is worth branching on: absence from the session registry only means offline while the registry is live.
Filters
These are the accounts table's own filters, by the same names.
| Parameter | Values |
|---|---|
search |
Username, persona name, or Steam64 id |
ban_status |
community, vac, game, trade. Repeat it (ban_status[]=) to match any of several |
login_state |
disabled, locked, suspended, banned, deleted, not_found. Repeatable |
marketplace_access |
steam, csfloat, marketcsgo: accounts that can sell there right now. Repeatable |
market_unlocking_soon |
true for restrictions that lift within a week |
steam_market_ineligible |
true for accounts that cannot sell on the Community Market |
profile_not_set_up |
true for accounts confirmed never to have saved a Community profile |
details_never_refreshed |
true for accounts no refresh has ever run against |
boost |
boosting, on_plan, not_on_plan |
session |
online, offline |
tags |
Tag ids. Repeatable, matching any of them |
sort |
created_at (default), username, details_refreshed_at, inventory_refreshed_at, wallet_balance, cs_level, item_count, items_count, login_state |
direction |
asc, desc |
details_never_refreshed=true is the one to run after an import. Until a refresh lands there is no Steam64 id, no wallet, no profile state and no privacy settings, so most of the rest of the API has nothing to work with.
Read, create, update, delete
/api/v1/accounts/{id}One account.
/api/v1/accountsAdd one account.
/api/v1/accounts/{id}Edit credentials, trade URL, or billing address.
/api/v1/accounts/{id}Delete one account.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/accounts" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"username":"farm_018","password":"…","shared_secret":"…","identity_secret":"…"}'Usernames are unique per account holder, not globally: two customers can farm the same public alias.
PATCH accepts trade_url as a whole Steam trade URL and stores only its token. A URL carrying a different partner= is refused, because it belongs to another account and would send this one's items to a stranger.
billing_address follows the panel: send the fields to set it (and mark it custom, so refreshes stop overwriting it), or null to clear it back to auto-generated.
Creating goes through your plan's account cap. Over it, you get 403 with plan_limit_reached and the upgrade path:
{
"message": "Your plan covers 25 Steam accounts.",
"code": "plan_limit_reached",
"plan": { "heading": "Account limit reached", "upgrade_url": "https://dashboard.steamlabs.dev/subscription", "max_steam_accounts": 25, "remaining": 0 }
}Add many at once
/api/v1/accounts/bulkAdd up to 1,000 accounts in one call.
{
"accounts": [
{ "username": "farm_019", "password": "…" },
{ "username": "farm_020", "password": "…", "shared_secret": "…" }
]
}All or nothing. A duplicate anywhere in the payload, or against an account you already have, fails the whole batch before anything is written, so you never have to work out which half landed. The answer is 201 with created and the full records.
Acting on many accounts
Every action below takes a selection, in one of two shapes.
{ "account_ids": ["019fb42e-…", "019fb42f-…"] }An explicit list, capped at 1,000 ids. It is applied immediately and the response tells you what happened. Ids you do not own are simply not in the selection; they are never refused individually, since refusing them differently from unknown ids would confirm they exist.
{ "filters": { "details_never_refreshed": true, "tags": ["019fb432-…"] } }The same filter object the listing accepts. It is resolved on the server and the ids are never materialized, so "every account with no proxy" stays a filter definition no matter how many accounts match. Filter selections always run asynchronously and answer 202.
Send one or the other, never both, and never neither: a request with no selection would otherwise mean "everything I own" by accident, which is the most expensive mistake this API can make. Over the id cap you get 422:
{ "message": "A bulk request may carry at most 1000 account ids…", "code": "bulk_limit_exceeded", "max": 1000 }There is no "everything" filter selection here: {"filters": {}} counts as a request with no selection and is refused alongside sending nothing at all. To act on every account you own, name a filter that matches them all. Run any selection through POST /api/v1/tasks/preview first if you want the count before you commit.
Queue work on the fleet
| Endpoint | What it queues |
|---|---|
POST /api/v1/accounts/refresh-details |
A full details refresh (Steam64 id, wallet, profile, privacy, bans) |
POST /api/v1/accounts/refresh-trade-url |
Re-scrapes and overwrites the stored trade URL token |
POST /api/v1/accounts/login |
A bare login, so a batch starts against warm sessions |
POST /api/v1/accounts/sign-out-everywhere |
Deauthorizes every Steam session, ours included |
POST /api/v1/accounts/check-bans |
A ban check (skips accounts with no Steam64 id yet) |
POST /api/v1/accounts/set-profile |
Sets the Community profile across the selection |
POST /api/v1/accounts/change-profile-privacy |
Flips privacy settings across the selection |
All seven require an Idempotency-Key header: they cost fleet capacity and several of them change something on Steam, so a retry after a timeout must replay rather than repeat.
All seven refuse with 503 maintenance_mode during platform maintenance, like every other endpoint that queues new work. See Errors.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/accounts/refresh-details" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"filters":{"details_never_refreshed":true}}'{ "task_id": "019fb440-…", "accounts_affected": 8412, "accounts_skipped": 0, "queued": true }task_id is the batch parent. Poll it through the tasks API to follow the run. accounts_skipped counts accounts left out as not eligible, most often because they already had a task of that type pending (which is what stops a repeated call from double-queueing anyone). When anything was skipped, accounts_skipped_reasons splits the count by reason, using the same slugs as task creation: task_in_flight here, plus never_refreshed on check-bans for accounts with no Steam64 id yet.
set-profile takes mode (manual, profile, group) plus the matching fields: persona_name, summary, country for manual, profile_id for a library profile, profile_group_id and allow_reuse for a group. Avatars come from library profiles; there is no upload here.
change-profile-privacy takes any of profile, owned_games, inventory, inventory_gifts, playtime, friends_list (1 private, 2 friends only, 3 public) and comment_permission (0 friends, 1 public, 2 private). Omitted settings are left untouched, so one call can flip a single setting across the whole fleet. Sending none of them is refused.
Edit many accounts
These change our own records rather than anything on Steam, so no task is created. With account_ids they apply immediately and answer 200; with filters they are queued and answer 202.
| Endpoint | Body | Effect |
|---|---|---|
POST /api/v1/accounts/assign-proxies |
proxy_ids |
Replaces the pinned proxies. An empty list clears them |
POST /api/v1/accounts/distribute-proxies |
proxy_ids |
One proxy per account, round-robin |
POST /api/v1/accounts/assign-proxy-groups |
proxy_group_ids |
Replaces the pinned groups |
POST /api/v1/accounts/clear-proxies |
None | Drops both, returning the accounts to your pool |
POST /api/v1/accounts/add-tags |
tag_ids |
Adds tags without disturbing existing ones |
POST /api/v1/accounts/remove-tags |
tag_ids |
Removes only those tags |
POST /api/v1/accounts/clear-stored-session |
None | Forgets our saved login locally (nothing is revoked on Steam) |
POST /api/v1/accounts/generate-billing-address |
country_source |
Generates a billing address for market buy orders |
DELETE /api/v1/accounts/bulk |
None | Deletes the selection |
{ "accounts_affected": 240, "queued": false }country_source picks which signal the country comes from: auto (default), store_country, wallet_currency or login_country. Accounts whose chosen source is empty are skipped rather than given a default-country address, because a wrong country fails at Steam's checkout instead of here.
clear-stored-session is the local half of the pair. sign-out-everywhere is the one that talks to Steam and revokes every device.
Games, packages and ban history
/api/v1/accounts/{id}/gamesThe account's Steam library, most played first.
/api/v1/accounts/{id}/packagesThe account's Steam licenses and the apps they grant.
/api/v1/accounts/{id}/bansEvery ban we have detected on the account, newest first.
These are separate endpoints rather than fields on the account because each is unbounded: a well-stocked account owns hundreds of games. games takes search, sort (playtime_forever_minutes, playtime_two_weeks_minutes, last_played_at, name, app_id) and direction.
The account's current ban state is on the record itself, under bans. The sub-resource is the timeline of detections.
Tags
/api/v1/account-tagsYour tags, with how many accounts carry each.
/api/v1/account-tagsCreate a tag.
/api/v1/account-tags/{id}Rename or recolor a tag.
/api/v1/account-tags/{id}Delete a tag. Accounts keep everything except the label.
Names are unique per account holder, ignoring case, so Prime and prime cannot both exist and become two badges nobody can tell apart. color is an optional six-digit hex; leave it out and the tag takes a colour derived from its own name, so the same word always lands on the same badge.
Attaching tags to accounts is not done here. It is a bulk operation on a selection of accounts, so it lives with the other account edits: add-tags and remove-tags above.