# Proxies

> Manage your proxy pool and proxy groups from code: import, test, filter, 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, and change them in bulk.

Two scopes cover the whole domain. `proxies.read` for the listings, `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

```endpoint
method: GET
path: /api/v1/proxies
description: Your proxy pool, paginated and filterable.
auth: bearer
```

```bash tab=curl
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",
            "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.

## Read, create, update, delete

```endpoint
method: GET
path: /api/v1/proxies/{id}
description: One proxy.
auth: bearer
```

```endpoint
method: POST
path: /api/v1/proxies
description: Add a single proxy.
auth: bearer
```

```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
    }
}
```

```endpoint
method: PATCH
path: /api/v1/proxies/{id}
description: Change a proxy. Every field is optional. PUT does the same.
auth: bearer
```

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.

```endpoint
method: DELETE
path: /api/v1/proxies/{id}
description: Delete a proxy. Returns 204.
auth: bearer
```

## Import a list

```endpoint
method: POST
path: /api/v1/proxies/import
description: Paste a proxy list, one per line.
auth: bearer
```

```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.

```endpoint
method: POST
path: /api/v1/proxies/{id}/test
description: Queue a health check for one proxy.
auth: bearer
```

```endpoint
method: POST
path: /api/v1/proxies/test
description: Queue a health check across a selection.
auth: bearer
```

```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](/docs/api/en/concepts/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.

> [!IMPORTANT]
> A filter key these endpoints do not recognise is **refused**, not ignored. Send one and you get a `422` naming it:
>
> ```json
> { "message": "The given data was invalid.", "code": "validation_failed", "errors": { "filters": ["Unknown filter key: statuz. This endpoint accepts search, status, protocol, type, group, is_active, rotation_mismatch."] } }
> ```
>
> A dropped key would leave a narrower-looking selection than you wrote, and with nothing left to narrow on, `bulk/delete` would take the whole pool. The same `422` answers a filters object whose keys are all recognised but all empty, since that constrains nothing either. Only a filters object you deliberately left empty selects everything.

## 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.

```endpoint
method: GET
path: /api/v1/proxy-groups
description: Your groups, with member and account counts.
auth: bearer
```

```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`.

```endpoint
method: GET
path: /api/v1/proxy-groups/{id}
description: One group, with its member and account counts.
auth: bearer
```

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

```endpoint
method: POST
path: /api/v1/proxy-groups
description: Create a group. Only name is required.
auth: bearer
```

```endpoint
method: PATCH
path: /api/v1/proxy-groups/{id}
description: Rename or recolor a group. PUT does the same.
auth: bearer
```

```endpoint
method: DELETE
path: /api/v1/proxy-groups/{id}
description: Delete the group. Its proxies are untouched. Returns 204.
auth: bearer
```

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

### Members

```endpoint
method: POST
path: /api/v1/proxy-groups/{id}/proxies
description: Add a selection of proxies to the group.
auth: bearer
```

```endpoint
method: POST
path: /api/v1/proxy-groups/{id}/proxies/remove
description: Remove a selection from the group.
auth: bearer
```

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.
