Skip to content
SteamLabs API

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

GET/api/v1/proxies

Your proxy pool, paginated and filterable.

API key required
Bash
curl "https://dashboard.steamlabs.dev/api/v1/proxies?status=healthy&per_page=2" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
JSON
{
    "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

GET/api/v1/proxies/{id}

One proxy.

API key required
POST/api/v1/proxies

Add a single proxy.

API key required
JSON
{
    "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:

JSON
{
    "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
    }
}
PATCH/api/v1/proxies/{id}

Change a proxy. Every field is optional. PUT does the same.

API key required

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.

DELETE/api/v1/proxies/{id}

Delete a proxy. Returns 204.

API key required

Import a list

POST/api/v1/proxies/import

Paste a proxy list, one per line.

API key required
JSON
{
    "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:port
  • host:port:user:pass
  • host:port@user:pass
  • user:pass@host:port

Blank lines and lines starting with # are ignored. The response tells you what happened:

JSON
{
    "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.

POST/api/v1/proxies/{id}/test

Queue a health check for one proxy.

API key required
POST/api/v1/proxies/test

Queue a health check across a selection.

API key required
JSON
{ "filters": { "status": ["untested", "unreachable"] } }

Both answer 202:

JSON
{
    "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
JSON
{
    "filters": { "status": "unreachable", "is_active": false },
    "max_logins_per_minute": null
}

These answer 200 with the size of the selection:

JSON
{ "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.

GET/api/v1/proxies/usage

Your totals for a window, and the chart behind them.

API key required
Bash
curl "https://dashboard.steamlabs.dev/api/v1/proxies/usage?range=7d" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
JSON
{
    "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
GET/api/v1/proxies/usage/top

The same window, broken down along one dimension.

API key required

dimension is required. limit defaults to 10 and caps at 100.

Bash
curl "https://dashboard.steamlabs.dev/api/v1/proxies/usage/top?dimension=proxies&range=24h&limit=3" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
JSON
{
    "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.

GET/api/v1/proxies/{id}/usage

One proxy's traffic, with what it was for and which accounts were behind it.

API key required

Takes the same window parameters. A proxy id belonging to someone else is a 404, the same as one that does not exist.

Bash
curl "https://dashboard.steamlabs.dev/api/v1/proxies/019fb42e-9a61-70d2-818a-f6a56593f3a5/usage?range=30d" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
JSON
{
    "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.

GET/api/v1/proxy-groups

Your groups, with member and account counts.

API key required
JSON
{
    "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.

GET/api/v1/proxy-groups/{id}

One group, with its member and account counts.

API key required

Returned bare, with no envelope, in the same shape a list row has.

POST/api/v1/proxy-groups

Create a group. Only name is required.

API key required
PATCH/api/v1/proxy-groups/{id}

Rename or recolor a group. PUT does the same.

API key required
DELETE/api/v1/proxy-groups/{id}

Delete the group. Its proxies are untouched. Returns 204.

API key required

Names are unique per account, and color is a #rrggbb hex string.

Members

POST/api/v1/proxy-groups/{id}/proxies

Add a selection of proxies to the group.

API key required
POST/api/v1/proxy-groups/{id}/proxies/remove

Remove a selection from the group.

API key required

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.