Endpoints
Proxies
Manage your proxy pool and proxy groups from code: import, test, filter, read bandwidth, and act on thousands at once.
Everything the Proxies page does, over HTTP: add proxies one at a time or paste in ten thousand, group them, test them, change them in bulk, and read how much bandwidth they moved.
Two scopes cover the whole domain. proxies.read for the listings and the usage figures, proxies.write for everything that changes the pool. Groups share them, because a group is just a way of organizing proxies.
Passwords are write-only
No response ever contains a proxy password, not even when you read a single proxy. You get has_password instead.
That is deliberate. You already have the credential (you pasted it in), so returning it buys you nothing, while a leaked read-only key would otherwise hand over a working set of third-party credentials. Send a new password to change one, or null to clear it.
List proxies
/api/v1/proxiesYour proxy pool, paginated and filterable.
curl "https://dashboard.steamlabs.dev/api/v1/proxies?status=healthy&per_page=2" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"data": [
{
"id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
"protocol": "http",
"host": "res-eu-01.example.net",
"port": 8080,
"type": "static",
"username": "slabs",
"has_password": true,
"max_logins_per_minute": 12,
"is_active": true,
"notes": null,
"status": "healthy",
"latency_ms": 184,
"exit_ip": "185.22.10.4",
"country": "NL",
"exit_ip_samples": null,
"distinct_exit_ips": null,
"rotation_mismatch": false,
"consecutive_failures": 0,
"last_error": null,
"last_checked_at": "2026-07-30T14:02:11+00:00",
"last_used_at": "2026-07-30T15:41:52+00:00",
"bytes_sent_total": 412884201,
"bytes_received_total": 4128842010,
"last_traffic_at": "2026-07-30T15:44:03+00:00",
"created_at": "2026-07-01T09:15:00+00:00",
"updated_at": "2026-07-30T14:02:11+00:00",
"groups": [
{ "id": "019fb430-1c22-73a4-9f0e-2b7c5d1e8a44", "name": "EU pool", "color": "#2563eb" }
]
}
],
"meta": { "page": 1, "per_page": 2, "total": 1, "last_page": 1 }
}Filters
| Parameter | Values |
|---|---|
search |
Host, host:port, or a bare port |
status |
untested, checking, healthy, unreachable, steam_blocked. Repeat it (status[]=) to match several |
protocol |
http, socks5 |
type |
static, rotating |
group |
A group id, or ungrouped |
is_active |
true or false |
rotation_mismatch |
true for proxies that claim to rotate but measured as a single exit IP |
sort |
newest (default), oldest, host, latency, last_checked |
rotation_mismatch is worth a look now and then. A proxy labelled rotating that returns the same exit IP on every connection funnels your whole fleet through one address, and Steam throttles the lot.
bytes_sent_total, bytes_received_total and last_traffic_at are lifetime figures, counted on the wire and never reset. They are not the same numbers as the usage endpoints below: those sum rollups that are swept after 95 days, these keep counting, so a proxy can report terabytes here and an empty 90-day window. last_traffic_at is "when did this proxy last move a byte, ever", where last_used_at is when a task last picked it.
Read, create, update, delete
/api/v1/proxies/{id}One proxy.
/api/v1/proxiesAdd a single proxy.
{
"protocol": "socks5",
"host": "res-eu-02.example.net",
"port": 1080,
"type": "static",
"username": "slabs",
"password": "hunter2",
"max_logins_per_minute": 12,
"is_active": true,
"notes": "second EU block",
"group_ids": ["019fb430-1c22-73a4-9f0e-2b7c5d1e8a44"]
}Returns 201 with the proxy. A duplicate endpoint (same protocol, host, port and username) is refused with 422, so re-posting the same proxy never quietly creates a second one.
Creating goes through your plan's proxy cap. Over it, you get 403 with plan_limit_reached and the upgrade path:
{
"message": "Your plan covers 500 proxies.",
"code": "plan_limit_reached",
"plan": {
"heading": "Proxy limit reached",
"upgrade_url": "https://steamlabs.example/subscription",
"max_proxies": 500,
"remaining": 0
}
}/api/v1/proxies/{id}Change a proxy. Every field is optional. PUT does the same.
Omitted fields are left alone, including password. Send "password": null to clear it. Sending group_ids replaces the proxy's groups, so an empty array removes it from all of them.
/api/v1/proxies/{id}Delete a proxy. Returns 204.
Import a list
/api/v1/proxies/importPaste a proxy list, one per line.
{
"proxy_list": "1.1.1.1:8000\n2.2.2.2:8000:user:pass\nsocks5://3.3.3.3:1080",
"default_protocol": "http",
"type": "static",
"is_active": true,
"group_ids": ["019fb430-1c22-73a4-9f0e-2b7c5d1e8a44"]
}Accepted line formats, each with an optional http:// or socks5:// prefix that overrides default_protocol:
host:porthost:port:user:passhost:port@user:passuser:pass@host:port
Blank lines and lines starting with # are ignored. The response tells you what happened:
{
"imported": 2,
"skipped": 1,
"invalid_count": 1,
"invalid": [{ "line": 4, "value": "definitely-not-a-proxy" }]
}skipped is endpoints your pool already had. A line the parser cannot read does not fail the batch: a 60,000-line paste with three typos imports 59,997 proxies and hands you the three. Only the first 50 unreadable lines come back, invalid_count has the total.
Re-importing yesterday's list is safe. You can also send an Idempotency-Key header for exact replay of the original response.
The import also goes through your plan's proxy cap, and is refused whole: if the new endpoints in the list would take you past the cap, nothing is written and you get the same 403 plan_limit_reached shape as the create endpoint. Endpoints your pool already holds do not count against the batch.
Test proxies
Checks run on the worker fleet, so these endpoints hand you a task to follow rather than a result. Both need an Idempotency-Key header.
/api/v1/proxies/{id}/testQueue a health check for one proxy.
/api/v1/proxies/testQueue a health check across a selection.
{ "filters": { "status": ["untested", "unreachable"] } }Both answer 202:
{
"task_id": "019fb457-d631-71a5-9042-d663a53bc51e",
"accounts_affected": 34,
"proxies_affected": 34
}Proxies already mid-check are skipped, so proxies_affected can be lower than your selection. If nothing needed checking you get 409 nothing_to_check.
Both test endpoints refuse with 503 maintenance_mode during platform maintenance, like every other endpoint that queues new work on the fleet. See Errors.
Bulk changes
Every bulk endpoint takes either proxy_ids (up to 1,000, 422 bulk_limit_exceeded above that) or a filters object using the exact keys the listing accepts. Narrow the list until it looks right, then post that same filter object.
Send one or the other, never both: they describe two different selections, and guessing which you meant is not a risk worth taking.
| Endpoint | What it does |
|---|---|
POST /api/v1/proxies/bulk/activate |
Sets is_active to true |
POST /api/v1/proxies/bulk/deactivate |
Sets is_active to false |
POST /api/v1/proxies/bulk/login-limit |
Sets max_logins_per_minute, or clears it with null |
POST /api/v1/proxies/bulk/groups/add |
Adds the selection to every group in group_ids |
POST /api/v1/proxies/bulk/groups/remove |
Removes it from them |
POST /api/v1/proxies/bulk/delete |
Deletes the selection |
{
"filters": { "status": "unreachable", "is_active": false },
"max_logins_per_minute": null
}These answer 200 with the size of the selection:
{ "proxies_affected": 128 }No task id, because there is nothing to wait for. However large the selection, one statement covers it.
An empty "filters": {} means every proxy you own. That is the deliberate equivalent of "select all" in the dashboard, so read it twice before sending it to the delete endpoint.
Bandwidth usage
The worker fleet counts wire bytes on every connection it opens, tagged with the proxy it dialled, the account it was working on, and what the work was. A drain folds those counters into hourly and daily rollups about once a minute, so what you read here is a couple of minutes behind live at worst. measured_at and updated_at tell you how far behind, which is the difference between "no traffic" and "not folded yet".
Hourly detail is kept for 7 days, daily totals for 95. Every figure is a plain byte count in decimal units (1 kB is 1,000 bytes), so divide by 1e9 for GB. Reading usage takes proxies.read, the same scope as reading the pool.
/api/v1/proxies/usageYour totals for a window, and the chart behind them.
curl "https://dashboard.steamlabs.dev/api/v1/proxies/usage?range=7d" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"range": {
"preset": "7d",
"from": "2026-08-29T00:00:00+00:00",
"to": "2026-09-04T00:00:00+00:00",
"granularity": "day",
"timezone": "UTC"
},
"summary": {
"bytes_sent": 4200000000,
"bytes_received": 42000000000,
"bytes_total": 46200000000,
"connections": 18422,
"proxies_with_traffic": 128,
"proxies_in_scope": 340,
"average_per_proxy": 360937500,
"busiest_bucket": { "start": "2026-09-01T00:00:00+00:00", "bytes": 13200000000 },
"month_to_date": 61400000000,
"projected_month": 460500000000
},
"series": {
"labels_start": [
"2026-08-29T00:00:00+00:00", "2026-08-30T00:00:00+00:00", "2026-08-31T00:00:00+00:00",
"2026-09-01T00:00:00+00:00", "2026-09-02T00:00:00+00:00", "2026-09-03T00:00:00+00:00",
"2026-09-04T00:00:00+00:00"
],
"sent": [600000000, 600000000, 600000000, 1200000000, 600000000, 600000000, 0],
"received": [6000000000, 6000000000, 6000000000, 12000000000, 6000000000, 6000000000, 0]
},
"measured_at": "2026-09-04T09:40:26+00:00",
"updated_at": "2026-09-04T09:41:12+00:00"
}average_per_proxy divides by the proxies that carried traffic, not by the ones you own. Adding two hundred idle proxies is spare capacity, not a collapse in throughput.
month_to_date and projected_month are null together on the first day of a month, and that means "not answerable yet" rather than zero: there is no completed day to extrapolate from. Later in the month projected_month alone is null while every byte so far landed today, for the same reason.
The series is zero-filled across the whole window, so a quiet day is a 0 rather than a missing point, and the three arrays are always the same length.
Choosing a window
| Parameter | Values |
|---|---|
range |
24h, 7d (default), 30d, 90d |
from, to |
Dates or timestamps for a window of your own. Send both |
group_by |
hour or day, to pin the grain |
Everything is UTC. A date without an offset is read as UTC, and a bucket is a UTC hour or a UTC day, so from=2026-09-01 means that UTC day wherever you are.
from and to are both inclusive bucket starts, which is also how they come back in range. to is the start of the last bucket, not the end of the window, so it lines up with the last entry of series.labels_start without an off-by-one.
group_by pins the grain, it does not change it. A window is read hourly when it is short and recent, and daily otherwise, so sending group_by gets you a 422 instead of a surprise when the grain you need is not the grain you would get. A client charting hours should find that out here, not by drawing 90 points labelled wrongly.
These are the refusals, all of them 422 with the usual validation_failed shape:
| What you sent | Why it is refused |
|---|---|
range together with from/to |
Two different ideas of the window, and guessing which you meant is not a risk worth taking |
from without to, or to without from |
Half a window is a half-typed date |
| A window longer than 92 days | The longest chart we answer for |
A from older than 95 days |
Usage that old has been swept, and zero-filling it would read as a quiet week |
group_by=hour on a window read a day at a time |
Hourly rows are kept for 7 days and cover at most two days at once |
/api/v1/proxies/usage/topThe same window, broken down along one dimension.
dimension is required. limit defaults to 10 and caps at 100.
curl "https://dashboard.steamlabs.dev/api/v1/proxies/usage/top?dimension=proxies&range=24h&limit=3" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"range": {
"preset": "24h",
"from": "2026-09-03T10:00:00+00:00",
"to": "2026-09-04T09:00:00+00:00",
"granularity": "hour",
"timezone": "UTC"
},
"dimension": "proxies",
"data": [
{
"proxy_id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
"endpoint": null,
"proxy": {
"id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
"host": "res-eu-01.example.net",
"port": 8080,
"protocol": "http",
"status": "healthy"
},
"bytes_sent": 412884201,
"bytes_received": 4128842010,
"bytes_total": 4541726211,
"connections": 1884,
"unattributed": false,
"shared_pool": false
},
{
"proxy_id": "019fb4a0-2c10-71ab-90f2-5c1d7e3b9a02",
"endpoint": null,
"proxy": null,
"bytes_sent": 22044820,
"bytes_received": 220448200,
"bytes_total": 242493020,
"connections": 310,
"unattributed": false,
"shared_pool": true
},
{
"proxy_id": null,
"endpoint": "gw-old.example.net:8080",
"proxy": null,
"bytes_sent": 1204482,
"bytes_received": 12044820,
"bytes_total": 13249302,
"connections": 42,
"unattributed": true,
"shared_pool": false
}
]
}Dimensions
dimension |
One row carries |
|---|---|
proxies |
proxy_id, endpoint, proxy (id, host, port, protocol, status), bytes_sent, bytes_received, bytes_total, connections, unattributed, shared_pool |
accounts |
steam_account_id, steam_account (id, username, persona_name), tasks, the three bytes_*, connections |
sources |
source, label, category, the three bytes_*, connections |
tasks |
task_id, type, steam_account_id, steam_account (id, username), finished_at, the three bytes_* |
groups |
proxy_group_id, proxy_group (id, name, color), proxies, the three bytes_* |
proxy, steam_account and proxy_group are null when the row's subject has since been deleted. Usage history outlives the rows it describes, so the bytes stay and the name goes.
On the proxies dimension there is a second reason for a null proxy: shared_pool is true. Those bytes are yours and count in your totals, but they travelled through a proxy from the admin-managed shared pool, which you do not own. You keep the proxy_id (a stable key for lining two windows up) and the endpoint behind it stays out of the response, the same way /proxies/{id}/usage answers 404 for it.
sources and groups are complete breakdowns rather than top lists, bounded by your own data, so limit does not apply to them.
Three dimensions carry an extra key beside data:
dimension |
Extra keys | What they say |
|---|---|---|
accounts |
tasks_window_truncated, tasks_retention_days |
The tasks count on each row reaches back only as far as tasks are kept. null there means "not answerable", never "none" |
tasks |
window_truncated, retention_days |
The same limit, applied to the rows themselves |
groups |
overlaps |
Always true, see the note below |
Unattributed rows are traffic the worker could not tie to a proxy in your pool. They carry the host:port it actually dialled in endpoint, they have no proxy_id, and they are never dropped from a breakdown or left out of the totals: bytes nobody can account for are the most interesting bytes on the page. In the groups breakdown they land under the ungrouped row, since traffic with no proxy can belong to no group.
The tasks dimension reads the tasks table rather than a usage rollup, so it is bounded by task retention (14 days by default) even when your window is 90. Its bytes come from the last attempt of each task, so a task that was re-queued reports only what its final run moved.
/api/v1/proxies/{id}/usageOne proxy's traffic, with what it was for and which accounts were behind it.
Takes the same window parameters. A proxy id belonging to someone else is a 404, the same as one that does not exist.
curl "https://dashboard.steamlabs.dev/api/v1/proxies/019fb42e-9a61-70d2-818a-f6a56593f3a5/usage?range=30d" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"proxy": {
"id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
"host": "res-eu-01.example.net",
"port": 8080,
"protocol": "http"
},
"range": {
"preset": "30d",
"from": "2026-08-06T00:00:00+00:00",
"to": "2026-09-04T00:00:00+00:00",
"granularity": "day",
"timezone": "UTC"
},
"summary": {
"bytes_sent": 412884201,
"bytes_received": 4128842010,
"bytes_total": 4541726211,
"connections": 1884,
"proxies_with_traffic": 1,
"proxies_in_scope": 1,
"average_per_proxy": 4541726211,
"busiest_bucket": { "start": "2026-08-28T00:00:00+00:00", "bytes": 402118004 },
"month_to_date": 1204118002,
"projected_month": 9030885015
},
"series": {
"labels_start": ["2026-08-06T00:00:00+00:00"],
"sent": [12884201],
"received": [128842010]
},
"by_source": [
{
"source": "refresh_details",
"label": "Refresh details",
"category": "account",
"bytes_sent": 208442100,
"bytes_received": 2084421000,
"bytes_total": 2292863100,
"connections": 1204
},
{
"source": "session_idle",
"label": "Idle session",
"category": "account",
"bytes_sent": 204442101,
"bytes_received": 2044421010,
"bytes_total": 2248863111,
"connections": 680
}
],
"top_accounts": {
"rows": [
{
"steam_account_id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
"steam_account": { "id": "019fb42e-9a61-70d2-818a-f6a56593f3a5", "username": "farm_017", "persona_name": "Ada" },
"tasks": 41,
"bytes_sent": 104221050,
"bytes_received": 1042210500,
"bytes_total": 1146431550,
"connections": 402
}
],
"tasks_window_truncated": true,
"tasks_window_days": 14
}
}series.labels_start is shortened above. It always holds one entry per bucket in the window, 30 of them here.
A source is a task type wherever the bytes belong to one, and otherwise one of session_idle, boost, proxy_check, poll:csfloat, poll:marketcsgo, unattributed or other. A value this API has never heard of is echoed back as itself rather than folded into other, so a newer fleet shows up in your chart instead of hiding in it.
Usage for the shared proxy pool as a whole is an admin view in the dashboard, not part of this API. A shared-pool proxy you were served is a 404 here, but its bytes are yours and are counted in your own totals and breakdowns, where it appears as a row with shared_pool: true and no endpoint on it.
The tasks count beside each account on this page counts only the tasks that ran through this proxy, so it lines up with the bytes next to it. An account bound to several proxies has its other work counted on their pages, not here.
Proxy groups
Groups are named buckets, and a proxy can be in several. Accounts can pin a group, in which case they log in only through its members, so editing a group takes effect on those accounts immediately.
/api/v1/proxy-groupsYour groups, with member and account counts.
{
"data": [
{
"id": "019fb430-1c22-73a4-9f0e-2b7c5d1e8a44",
"name": "EU pool",
"color": "#2563eb",
"proxies_count": 240,
"accounts_count": 18,
"created_at": "2026-07-01T09:15:00+00:00",
"updated_at": "2026-07-20T11:00:00+00:00"
}
],
"meta": { "page": 1, "per_page": 50, "total": 1, "last_page": 1 }
}Takes search, sort (name by default, plus newest, oldest, proxies_count) and per_page.
/api/v1/proxy-groups/{id}One group, with its member and account counts.
Returned bare, with no envelope, in the same shape a list row has.
/api/v1/proxy-groupsCreate a group. Only name is required.
/api/v1/proxy-groups/{id}Rename or recolor a group. PUT does the same.
/api/v1/proxy-groups/{id}Delete the group. Its proxies are untouched. Returns 204.
Names are unique per account, and color is a #rrggbb hex string.
Members
/api/v1/proxy-groups/{id}/proxiesAdd a selection of proxies to the group.
/api/v1/proxy-groups/{id}/proxies/removeRemove a selection from the group.
Both take the same proxy_ids or filters selection as the bulk endpoints, and both answer { "proxies_affected": n }. Adding is idempotent: proxies already in the group are left alone rather than duplicated.
To read a group's members, list proxies filtered by it: GET /api/v1/proxies?group={id}. That way paging, sorting and filtering work exactly as they do everywhere else.