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.
{
"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.
{
"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.
/api/v1/tasks/previewResolve a selection and return the count, without queueing any work.
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"}}'{
"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.
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:
Idempotent-Replay: trueRules
- 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
500you 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
previewin development so you find out a filter matches 80,000 accounts before it queues 80,000 tasks.