Skip to content
SteamLabs API

Concepts

Bulk operations

Act on thousands of accounts in one call, and make retries safe while you do it.

Most write endpoints act on many accounts at once. There are two ways to say which, and one header that makes retrying safe.

Two ways to select

Explicit IDs

Send the IDs you want. Capped at 1,000 per request, handled inline, and you get the result straight back.

JSON
{
    "type": "refresh_details",
    "account_ids": [
        "019fb42e-9a61-70d2-818a-f6a56593f3a5",
        "019fb42e-9a7e-728d-b960-8b4c2162898c"
    ]
}

Over the cap you get 422 bulk_limit_exceeded, with the cap in max. Split the call, or use filters instead.

Filters

Describe the accounts instead of listing them. The server resolves the selection, so a call that touches 80,000 accounts is the same size as one that touches two.

JSON
{
    "type": "refresh_details",
    "filters": {
        "tags": ["019fb432-6d10-70bb-a1c9-4e2f8b7d5a01"],
        "guard": "mobile",
        "details": "never"
    }
}

Each endpoint documents the exact filter keys it accepts, and they are not all the same vocabulary. The account bulk writes under /api/v1/accounts take the accounts table's keys, the ones GET /api/v1/accounts accepts. POST /api/v1/tasks and POST /api/v1/tasks/preview take the account selector's shorter set, listed on Tasks. Read the page for the endpoint you are calling.

Two families of bulk write never queue anything and so never answer 202. The proxy and profile bulk endpoints change our own rows, which one statement covers however large the selection is: both shapes run inline and answer 200 with a count. The same is true of the account edits that touch no worker (tags, proxy pinning, stored sessions), except that those do queue a filter selection, so read each page rather than assuming.

Check before you commit

POST /api/v1/tasks/preview runs a selection without creating anything and tells you how many accounts it matched. It writes nothing, but it still needs tasks.write, because the same key that previews is the one that commits.

POST/api/v1/tasks/preview

Resolve a selection and return the count, without queueing any work.

API key required
Bash
curl -X POST https://dashboard.steamlabs.dev/api/v1/tasks/preview \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filters": {"tags": ["019fb432-6d10-70bb-a1c9-4e2f8b7d5a01"], "session": "offline"}}'
JSON
{
    "accounts_affected": 4182
}

Worth doing on anything destructive or expensive. A filter that quietly matches every account you own is easier to spot before the tasks exist than after.

Idempotency

If a write times out, you cannot tell whether it arrived. Retrying is the only sensible move, and on anything that costs money or moves items, a duplicate is expensive.

Send an Idempotency-Key: a unique string you invent for each logical attempt. We remember the response for 24 hours, and a repeat of the same key replays it instead of doing the work again.

Bash
curl -X POST https://dashboard.steamlabs.dev/api/v1/tasks \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7f3a9c2e-1b44-4d8a-9f01-cc2e5a9b1234" \
  -d '{"type": "sell_items", "account_ids": ["019fb42e-9a61-70d2-818a-f6a56593f3a5"]}'

Retry with the same key and you get the original response back, plus a header:

Text
Idempotent-Replay: true

Rules

  • Generate a new key for each logical operation, and reuse it only when retrying that operation. A UUID per attempt is the easy version.
  • Keys are scoped to the API key that used them and to the endpoint they were sent to. Two integrations cannot collide, and if you do reuse one value on a different endpoint that call runs normally instead of replaying the first one's response.
  • The body is not part of the scope. Sending different data under the same key on the same endpoint replays the original response and changes nothing, so a key really does mean one attempt at one operation.
  • Only successful responses are remembered. A 500 you retry will genuinely re-run, which is what you want.
  • Keys expire after 24 hours.
  • Maximum 255 characters. Surrounding whitespace is trimmed.

Where it is required

Thirty-seven endpoints require the header. They are the ones that spend money, move items, hold a Steam login, or store a credential.

Group Endpoints that require it
Steam accounts refresh-details, refresh-trade-url, login, sign-out-everywhere, check-bans, set-profile, change-profile-privacy
Tasks POST /v1/tasks, POST /v1/tasks/{id}/retry, POST /v1/tasks/{id}/retry-failed
Inventory All seven writes: sell, send, store, use, trade-up, and a storage unit's withdraw and rename
Market POST /v1/market/{venue}/buy-orders
Trading An offer's accept, decline and cancel, plus an account's trade-offers/respond and confirmations/respond
Trade-ups clear-space, craft, queue-best-value
Hour boosting quick-boost, POST /v1/boost/plans/start, a plan's start and toggle, an assignment's start and retry
Profiles POST /v1/profiles/generate/ai
Proxies POST /v1/proxies/test, POST /v1/proxies/{id}/test
Connected services POST /v1/integrations/marketplaces/{venue}/connect
Settings POST /v1/settings/notification-integrations/{id}/test

Calling one of those without the header returns 400 idempotency_key_required.

Six more accept the header and honour it, but do not demand it: POST /v1/proxies/import, POST /v1/profiles, POST /v1/profile-groups, POST /v1/profiles/bulk/delete, POST /v1/profiles/bulk/reset-use-counts and POST /v1/profiles/generate/random. Everywhere else the header is ignored.

If the same key arrives while the first request is still running, the second gets 409 idempotency_key_in_flight. Wait a moment and retry: that is the mechanism stopping two simultaneous retries from both executing.

Fan-out and fair use

A filter selection can queue work for every account you own, and nothing stops you doing that repeatedly inside your rate limit. Please do not. The worker fleet is shared, and a script that re-queues a full sweep every minute degrades the platform for everyone including you.

Sensible shapes:

  • Queue a sweep, then poll the parent task until it completes before queueing the next.
  • Filter to the accounts that actually need the work (details_never_refreshed, login_state) rather than sweeping everything on a timer.
  • Use preview in development so you find out a filter matches 80,000 accounts before it queues 80,000 tasks.