Concepts
Errors
One error shape, one stable code per failure, and what to do about each.
Every error returns the same JSON shape:
{
"message": "This API key is missing the required scope: accounts.write.",
"code": "missing_scope"
}Branch on code, never on message. The code is a contract and will not change. The message is written for humans and gets reworded whenever we can word it better.
Some errors add fields alongside those two. They are always additive, so a client reading only code keeps working.
Status codes
| Status | When |
|---|---|
200 |
Success |
201 |
Created |
202 |
Accepted. Queued work, see Bulk operations |
204 |
Success, nothing to return |
400 |
The request itself is malformed |
401 |
Missing or bad key |
402 |
Your balance cannot cover an explicitly quoted charge |
403 |
Your key or your plan does not allow this |
404 |
Not found, or not yours |
409 |
Conflicts with the current state |
422 |
Validation failed |
429 |
Rate limited |
500 |
Our fault |
503 |
A shared resource was momentarily unavailable. Retry shortly |
Validation errors
A 422 adds an errors object keyed by field, in Laravel's standard shape:
{
"message": "The given data was invalid.",
"code": "validation_failed",
"errors": {
"username": ["The username field is required."],
"proxy_id": ["The selected proxy id is invalid."]
}
}Plan limits
When your subscription plan does not cover what you asked for, you get a 403 with the plan that would:
{
"message": "Your plan does not include the CSFloat marketplace.",
"code": "plan_limit_reached",
"plan": {
"heading": "CSFloat is not in your plan",
"upgrade_url": "https://dashboard.steamlabs.dev/subscription"
}
}This is the one error worth handling specially, because it is not transient and retrying never fixes it. Surface plan.upgrade_url to whoever is running your integration.
The Steam account cap and the proxy cap use the same code rather than one of their own, and add the numbers so your client can say how far over it is (max_steam_accounts on the account cap, max_proxies on the proxy cap):
{
"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
}
}GET /api/v1/me tells you your limits up front, so a well-behaved client rarely sees this at all.
Being over the account cap
Holding more Steam accounts than your plan covers is a state you can end up in without doing anything: a plan downgrade or an expiry lowers the cap, and nothing is ever deleted to make your fleet fit it. While you are over the cap, every endpoint that starts new work returns plan_limit_reached, whatever the task type:
{
"message": "You have 2,500 Steam accounts and your plan covers 100. No new tasks can be started until you remove 2,400 of them or move to a plan that covers your fleet.",
"code": "plan_limit_reached",
"plan": {
"heading": "You are over your account limit",
"upgrade_url": "https://dashboard.steamlabs.dev/subscription"
}
}Nothing else changes. Your accounts, their items and their history stay exactly as they were, reads keep working, tasks already queued run to completion, and deliveries, settlements and confirmations for sales already made keep flowing. Remove accounts with DELETE /api/v1/accounts/bulk or move up a tier, and normal service resumes immediately.
Compare steam_accounts_used against max_steam_accounts on GET /api/v1/me to detect this before you fire.
Maintenance
While SteamLabs is under platform maintenance, endpoints that create new work (tasks, listings, trade sends) refuse with a 503:
{
"message": "This action is paused while SteamLabs is under maintenance. Work already running is unaffected; try again once maintenance ends.",
"code": "maintenance_mode",
"reason": "Scheduled worker upgrades, back within the hour"
}Unlike plan_limit_reached, this is transient: nothing about your request or plan is wrong, and the same call succeeds once maintenance ends. reason is the note the team wrote when enabling it, and can be null. There is no Retry-After header because the end time is not known.
Reads, cancels and deletes keep working throughout, and so do deliveries, settlements and confirmations for sales already made. GET /api/v1/me reports maintenance up front in its maintenance block, so a client can check before firing.
Every code
Authentication
| Code | Status | Meaning |
|---|---|---|
missing_api_key |
401 |
No Authorization header |
invalid_api_key |
401 |
Unknown, expired, or revoked key |
account_not_entitled |
403 |
Your account cannot use the API |
missing_scope |
403 |
Key lacks the permission. Adds required_scope |
rate_limit_exceeded |
429 |
Adds retry_after in seconds |
Requests
| Code | Status | Meaning |
|---|---|---|
validation_failed |
422 |
Adds errors keyed by field |
not_found |
404 |
No such record, or not yours |
Conflicts
There is no generic conflict code. A 409 always names what is in the way, so your client can tell "wait for a task to finish" apart from "you already have one of these".
| Code | Status | Meaning |
|---|---|---|
account_busy |
409 |
The account already has marketplace or trade-up work queued or running |
trade_task_pending |
409 |
A trade task is already queued or running for that account. Adds pending_task_id |
confirmation_task_pending |
409 |
A mobile confirmation task is already queued or running for that account |
move_already_running |
409 |
A move is already running for that storage unit |
inputs_unavailable |
409 |
Another contract reserved the items while yours was being staged |
generation_in_progress |
409 |
An AI profile generation batch is still running |
overage_quote_changed |
409 |
The confirmed AI profile charge is missing or stale. Adds a fresh quote |
integration_already_exists |
409 |
You already have a notification destination of that type. Update it instead |
integration_disabled |
409 |
The destination is turned off, so it cannot be tested |
nothing_to_check |
409 |
Every proxy in the selection was already being checked |
account_in_trade_protection |
409 |
The account is delivering an AssetPay sale still inside Steam's 7 day hold, so it cannot be deleted yet |
The first six clear on their own: poll the work that holds the resource, then try again. So does the last one, on Steam's clock rather than yours. The three before it describe state you have to change yourself, so retrying alone never helps. idempotency_key_in_flight is the one remaining 409, and it is in the idempotency table below.
Each endpoint's page lists the conflicts that endpoint can return.
Plan and limits
| Code | Status | Meaning |
|---|---|---|
plan_limit_reached |
403 |
Adds plan with the upgrade path. Covers user API access, feature entitlements, the Steam account and proxy caps, and being over the account cap (which blocks all new work) |
bulk_limit_exceeded |
422 |
Too many IDs in one call. Adds max |
insufficient_balance |
402 |
Your balance cannot cover the AI profile quote. Adds charge, balance, shortfall and currency fields |
Availability
| Code | Status | Meaning |
|---|---|---|
maintenance_mode |
503 |
Platform maintenance pauses new work. Adds reason. Transient, retry after maintenance ends |
Idempotency
| Code | Status | Meaning |
|---|---|---|
idempotency_key_required |
400 |
This endpoint needs an Idempotency-Key |
idempotency_key_invalid |
400 |
The key is longer than 255 characters |
idempotency_key_in_flight |
409 |
The same key is still being processed. Retry shortly |
Retrying
| Status | Retry? |
|---|---|
429 |
Yes, after Retry-After |
500, 502, 503, 504 |
Yes, with backoff |
409 idempotency_key_in_flight |
Yes, after a second or two |
400, 401, 402, 403, 404, 422 |
No. Fix the request or account balance |