Account proxies
The account proxies surface lets you register your own proxies
against your Driftstack account and route an
agent session’s traffic through one — so a
session browses from your egress IP instead of the default. Four
schemes can be registered: socks5, http, openvpn, and
wireguard.
Three of them can currently route a browser session: socks5
(dialled by the control plane) and openvpn / wireguard (tunnelled
at the browser host). http proxies can be stored and managed here,
but are not a session-dispatch target on this deployment — passing an
http proxy to a session create is refused with 400, not silently
ignored.
Proxy secrets are write-only: passwords (SOCKS5/HTTP), the OpenVPN
config blob (which embeds your certs/keys), and the WireGuard private
key are accepted on create/update, encrypted at rest under your
account’s key, and never returned in any response. Responses expose
has_password (a password is stored) and has_secret (a VPN secret is
stored) instead. Every endpoint is scoped to the calling account — you
can only see and use your own proxies.
What kind of proxy works
Profiles run on Driftstack’s servers, not on your computer. A proxy that works from your desk can still fail from there, and the desktop app’s Test button — which runs from your own machine — cannot tell the difference. What you need:
- A public address. Not
localhost,127.0.0.1, or a private-network address such as10.x.x.x,172.16.x.x–172.31.x.x, or192.168.x.x. A proxy on your own machine or office network tests fine from the app and is unreachable from Driftstack’s servers. - Username and password authentication. IP-allowlist access does not work: profiles run from Driftstack’s servers, not from your IP, so an allowlist that names your address never matches. Ask your provider for user/pass credentials.
- SOCKS5, OpenVPN, or WireGuard. These are the schemes that can carry a
session.
httpproxies can be saved but not used for a session.
Resource shape
{
"id": "a1b2c3d4-...",
"label": "amsterdam residential",
"scheme": "socks5",
"host": "proxy.example.com",
"port": 1080,
"username": "user",
"has_password": true,
"has_secret": false,
"created_at": "2026-06-16T09:15:00Z",
"updated_at": "2026-06-16T09:15:00Z"
}
scheme is one of socks5 | http | openvpn | wireguard. host,
port, and username are not secret. has_password / has_secret
are the only signals about the stored credentials; the plaintext is
never readable back. For VPN schemes, host/port are the display
endpoint (parsed from your .ovpn / wg0.conf).
List
GET /v1/account/me/proxies → { "data": [ ...proxy ] }
Required scope: account_owner.
Create
POST /v1/account/me/proxies
{
"label": "amsterdam residential",
"scheme": "socks5",
"host": "proxy.example.com",
"port": 1080,
"username": "user",
"password": "••••••"
}
scheme defaults to socks5. username/password are optional (some
SOCKS5 servers accept unauthenticated or username-only access). Returns
the created proxy metadata (no password) with 201.
Host safety: the host must be a public address. Private, loopback,
link-local, and cloud-metadata addresses (e.g. 127.0.0.1,
10.0.0.0/8, 169.254.169.254) are rejected with 400 — a proxy that
pointed at an internal address could be used to reach networks you
shouldn’t.
How many you can save: each tier caps the number of saved proxies on
the account. Crossing it on POST /v1/account/me/proxies returns 400
with Proxy limit reached (<cap>). Delete an existing proxy to add another. — note this is a 400, not the 429 Tier limit the profile
cap uses. Values mirror PROXIES_PER_TIER in @driftstack/api-types:
| Tier | Saved proxies |
|---|---|
free |
1 |
solo_manual |
10 |
team_manual |
25 |
agency_manual |
50 |
api_starter |
25 |
api_builder |
100 |
api_scale |
500 |
enterprise |
custom |
free gets exactly one and SOCKS5 only — OpenVPN and WireGuard need a
paid tier. The enterprise allowance is negotiated rather than a number
this page can print.
VPN proxies (OpenVPN / WireGuard)
For a VPN scheme, the secret config rides a nested block. host/port
are the display endpoint (most clients fill them from the parsed config).
OpenVPN — paste the full .ovpn as config_blob (must contain a
client directive and a remote <host> <port> directive; up to 256 KiB).
username/password are optional inline credentials:
{
"label": "frankfurt ovpn",
"scheme": "openvpn",
"host": "vpn.example.com",
"port": 1194,
"openvpn": {
"config_blob": "client\nremote vpn.example.com 1194\n...",
"username": "user",
"password": "••••••"
}
}
WireGuard — the private_key and peer_public_key are 44-char
base64 curve25519 keys; endpoint is host:port; address is the
interface address (e.g. 10.7.0.2/32); allowed_ips defaults to
0.0.0.0/0; dns is optional:
{
"label": "frankfurt wg",
"scheme": "wireguard",
"host": "vpn.example.com",
"port": 51820,
"wireguard": {
"private_key": "<44-char base64>",
"peer_public_key": "<44-char base64>",
"endpoint": "vpn.example.com:51820",
"address": "10.7.0.2/32",
"allowed_ips": "0.0.0.0/0",
"dns": "1.1.1.1"
}
}
The config_blob / private_key are write-only — the response returns
has_secret: true, never the secret. VPN proxies require encryption to
be configured server-side; if it isn’t, create returns 503.
Required scope: account_owner — a broad write key is not sufficient.
Update
PUT /v1/account/me/proxies/{id}
Every field is optional. For the password on a SOCKS5 or HTTP proxy:
- omit
password→ keep the existing one "password": null→ clear it"password": "..."→ set/replace it
VPN proxies are different. On a saved openvpn or wireguard proxy,
sending password at all — a new value or null — without also
sending scheme and the matching config block is rejected with 400:
A VPN password can only be changed by resubmitting the matching VPN configuration. The credential is wrapped together with the config, so
there is no way to rotate one without the other. To change a VPN
password, resubmit the full VPN body as you would on create.
404 if the id isn’t one of your proxies.
409 — Proxy changed concurrently. Retry the update. The update is a
compare-and-set on scheme: the route reads the saved proxy, then writes
only if the scheme is still what it read. If something else changed the
scheme in between — a second dashboard tab, a concurrent API call — the
write is refused rather than silently applied to a proxy that is no
longer the one you edited. Re-read the proxy and reissue the update. A
409 here means the row still exists; a 404 means it is gone.
Required scope: account_owner — a broad write key is not sufficient.
Delete
DELETE /v1/account/me/proxies/{id} → 204 (idempotent; 404 for an
unknown id).
Required scope: account_owner — a broad write key is not sufficient.
Test a proxy
POST /v1/account/me/proxies/{id}/test
Answers one question: would a session launched through this proxy work right
now? For a socks5 proxy that is the same check the launch gate runs — TCP
connect, SOCKS5 handshake, authentication, CONNECT, and a real request through
the tunnel — so a green test and a successful launch mean the same thing. Always
200; a proxy that fails the test is a result, not an error:
{ "ok": true, "latency_ms": 142 }
{
"ok": false,
"reason": "The proxy connected but could not reach the internet. Its upstream egress is blocked."
}
reason is a fixed sentence written for a person to read and act on, drawn from
the same four cases the launch gate reports (see
Why a launch is refused). It is not an enum — branch
on the reason field of a 422 instead. Raw socket, DNS, TLS, and remote proxy
response text never reach the API response.
A proxy that authenticates but cannot route is the case worth knowing about: it looks healthy to anything that only opens the port, and it fails every launch. This test reports it.
Two cases fall back to a plain TCP-reachability check, which confirms the port
answers and nothing more: an openvpn or wireguard wire (there is no
host:port to dial for the tunnel itself), and a deployment with no proxy
connectivity probe configured.
Required scope: account_owner — a broad write key is not sufficient.
Route a session through a proxy
Pass proxy_id when you
create an agent session:
{ "profile_id": "prof_...", "proxy_id": "a1b2c3d4-..." }
The session’s egress is routed through that proxy. The proxy_id must
be one of your account’s proxies (an unknown or not-owned id returns
404), and its scheme must be one that can route a session — an
http proxy returns 400 with a message naming the supported
schemes. Omit it to use the default egress.
The Driftstack desktop app manages this for you: add a proxy under Proxies, set it as a profile’s default, and launching the profile routes that session through it automatically.
Why a launch is refused
Before a session is created, Driftstack proves the proxy can actually carry it.
If it cannot, the create returns 422
errors.driftstack.dev/proxy-validation-failed and no session is created and
nothing is billed — the refusal happens before any browser starts.
The problem body carries a reason alongside the human detail:
{
"type": "https://errors.driftstack.dev/proxy-validation-failed",
"title": "Proxy validation failed",
"status": 422,
"detail": "The proxy connected but could not reach the internet — its upstream egress is blocked.",
"reason": "egress_blocked",
"resource": "proxy"
}
reason is a closed set. Branch on it — detail is prose and may be reworded.
| Reason | What it means | What fixes it |
|---|---|---|
unreachable |
Nothing answered at host:port. |
Check the host, the port, and that the proxy is online. |
auth_failed |
The proxy answered and rejected the username or password. | Re-enter the credentials. |
timeout |
The proxy accepted the connection but did not finish in time. | It is overloaded or half-down; retry, then change proxy. |
egress_blocked |
The proxy authenticated, then refused or failed to reach the destination. | Ask the provider — this is usually plan, quota, or an ACL. |
egress_blocked is the one that surprises people. The credentials are correct
and the proxy is up, so anything that only checks reachability calls it healthy;
the provider is simply declining to route. A provider that has suspended an
account, exhausted its bandwidth quota, or restricted destinations by ruleset
answers every CONNECT this way. Nothing on the Driftstack side will change it.
To distinguish “this proxy is broken” from “this provider is refusing everything”, test a second proxy from a different provider: if that one launches, the fault is the first provider’s.
Retrying a 422 with the same proxy will fail the same way — it is a statement
about the proxy, not a transient error, so the SDKs do not retry it.