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,
"pending_balance_cents": 594,
"pending_available_at": null,
"pending_available_until": "2026-07-31T08:00:00+00:00",
"currency": "EUR"
},
"point_shop": {
"balance": 2990,
"points_earned": 3690,
"points_spent": 700,
"balance_refreshed_at": "2026-08-05T14:45:00+00:00"
},
"spend": {
"external_funds_used_cents": 2734,
"package_only_cents": 1601,
"currency": "USD",
"refreshed_at": "2026-08-18T12:00:00+00:00"
},
"profile_items": {
"equipped": {
"profile_background": {
"community_item_id": "39213089438",
"app_id": 2243720,
"name": "Outstanding Story-Rich Game",
"title": "Outstanding Story-Rich Game",
"image_url": "https://shared.fastly.steamstatic.com/community_assets/images/items/2243720/51fdb36bd2a933344c9236cb47a0abddf17e32da.jpg"
},
"mini_profile_background": null,
"avatar_frame": {
"community_item_id": "39213089373",
"app_id": 730,
"name": "Akihabara Accept",
"title": "Akihabara Accept",
"image_url": "https://shared.fastly.steamstatic.com/community_assets/images/items/730/c2a822b6422d7a2c9e72ec74bf54139db8723ef0.png"
},
"animated_avatar": null
},
"refreshed_at": "2026-08-15T11:02:00+00:00"
},
"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 },
"group": { "id": "019fb433-…", "name": "Prime" },
"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.
profile_items.equipped lists the cosmetics on the Steam profile, with one key per slot and null for an empty slot. All four keys are always present, so a missing item is null rather than an absent key. refreshed_at is null until a refresh_details task has read them, which is also what keeps them current. Emoticons, stickers and chat effects never appear here: they are consumed per use rather than equipped.
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 |
not_banned |
true for accounts without a VAC, community, or game ban. Trade-only and never-checked accounts match |
login_state |
disabled, locked, suspended, banned, deleted, not_found. Repeatable |
marketplace_access |
steam, csfloat, marketcsgo, skinland, dmarket, skinport, assetpay: 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 |
exclude_tags |
Tag ids. Repeatable, dropping an account that carries any of them |
groups |
Account group ids. Repeatable, matching any selected group |
include_ungrouped |
true to include accounts with no group. ORed with groups |
wallet_min |
Minimum available Steam wallet value in USD across mixed currencies |
wallet_max |
Maximum available Steam wallet value in USD across mixed currencies |
has_pending_balance |
true for accounts with a pending Steam wallet balance greater than zero |
wallet_pending_min |
Minimum pending Steam wallet value in USD across mixed currencies |
wallet_pending_max |
Maximum pending Steam wallet value in USD across mixed currencies |
sort |
created_at (default), username, details_refreshed_at, inventory_refreshed_at, wallet_balance, wallet_pending_balance, point_shop_balance, external_funds_used, cs_level, item_count, items_count, login_state |
direction |
asc, desc |
wallet.balance_cents is available to spend. wallet.pending_balance_cents is held by Steam and is not included in the available balance. When Steam exposes its estimated release window, wallet.pending_available_at is the earliest expected release and wallet.pending_available_until is when all pending funds are expected to be available. Either timestamp can be null when Steam does not provide that boundary.
spend is Steam's External Funds Used snapshot from a refresh_details task: lifetime real money applied to the account, not the current wallet. package_only_cents is store-package spend only. Every field is null until a refresh has read the AccountSpend page.
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.
Single and bulk account create payloads accept nullable account_group_id. PATCH /accounts/{id} accepts the same field. The group must belong to the API key owner.
DELETE answers 409 account_in_trade_protection while the account is delivering an AssetPay pool sale that is still inside Steam's 7 day trade protection. Only that account can see the trade if it gets reversed, and AssetPay refunds their buyer on our word, so it has to stay until the hold ends. Nothing else blocks a delete, and retrying after the hold works.
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, Points Shop balance, profile, privacy, bans) |
POST /api/v1/accounts/refresh-inventory |
An inventory-only refresh, much lighter than a full details refresh |
POST /api/v1/accounts/refresh-wallet |
A wallet-only refresh (balance, pending funds, currency), much faster than a full details refresh |
POST /api/v1/accounts/refresh-point-shop-balance |
Refreshes the Points Shop balance and lifetime earned and spent totals |
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 ten 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 ten 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, custom_url, 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. custom_url is the Steam vanity slug (steamcommunity.com/id/{slug}): letters, numbers and underscores, 2 to 32 characters. Omit it or send null to leave each account's current custom URL alone. Steam rejects a slug that is already taken. Library profiles send their slug only on the first-ever apply (times_used is 0). Reuse (one profile on many accounts, or a group with allow_reuse) leaves later accounts' custom URLs alone.
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 account-level pins and groups. Proxy groups linked on the account group still apply |
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/set-group |
group_id |
Moves every selected account to one group. Send null for Ungrouped |
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 }A bulk delete keeps back the accounts that cannot go yet (an AssetPay pool sale still in trade protection, as above) and deletes the rest. When it does, the answer says so and accounts_affected counts only what really went:
{ "accounts_affected": 239, "queued": false, "kept_in_trade_protection": 1 }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.
Account groups
Groups are exclusive. Each account belongs to one group or is Ungrouped. Deleting a group keeps its accounts and makes them ungrouped.
You can attach proxy groups with proxy_group_ids on create or update. Members then log in through those groups, plus any proxies or proxy groups pinned on the account itself. Moving an account out of the group drops the inherited groups. Its own pins stay. proxies.group_count on an account is still only the groups pinned on that account.
/api/v1/account-groupsYour account groups, including account counts and linked proxy groups.
/api/v1/account-groupsCreate an account group, optionally with linked proxy groups.
/api/v1/account-groups/{id}One account group.
/api/v1/account-groups/{id}Rename an account group, or change its linked proxy groups.
/api/v1/account-groups/{id}Delete a group and leave its accounts ungrouped.
Names are unique per account holder, ignoring case. Responses include accounts_count and proxy_groups (id, name). Omit proxy_group_ids on PATCH to leave the link unchanged. Send an empty list to clear it.