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 |
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.
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 |
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 |
The first six clear on their own: poll the work that holds the resource, then try again. The last three 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. Also covers the Steam account and proxy caps |
bulk_limit_exceeded |
422 |
Too many IDs in one call. Adds max |
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, 403, 404, 422 |
No. Fix the request |