Proxylexus

API · v1

Your proxy users, your balance, from a script.

Issue the credentials your jobs connect with, build a targeted proxy line, and watch the balance move — from a script, with a key you create yourself. Everything here is scoped to the account that key belongs to; there is no parameter anywhere that names another.

Base URL
https://proxylexus.com/api/v1
Auth
Authorization: Bearer <key>
Format
JSON in, JSON out, UTF-8
Spec
/docs/openapi.json

The page and the spec are two renderings of one description of this API, so a client generated from the spec and a request copied off this page cannot disagree about a field, a status or a limit. Point Postman, an SDK generator or an agent straight at it.

Authentication

A key identifies exactly one account. Every route re-derives that account from the key and scopes every query to it, which is why nothing on this page takes an account id — and why a request that tries to supply one is refused rather than quietly ignored.

  1. 1 · Create it

    In the dashboard, under API keys. Name it after the automation that will use it — one key per job, so revoking one stops one thing. Up to 10 live keys per account.

  2. 2 · Store it, once

    The key is shown at creation and never again. We keep a digest of it, not the key, so there is no reveal button and support cannot recover one. Lost a key? Create another and revoke the old one.

  3. 3 · Send it in the header

    Authorization: Bearer <key> and nowhere else. Not a query string, not a cookie, not a custom header. A key in a URL is refused with credential_in_url whether or not it is valid.

What a key looks like

Two halves. The first, plx_a3f91b7c4d, is public: it is how a key is named in a log line or on a screen, and it is the only half that is ever safe to write down anywhere. The second is 256 bits of randomness and is the secret.

Revoking a key takes effect immediately — nothing caches this lookup, so the very next request with it fails. That is not true of a proxy user, which is delivered on a cache and takes about thirty seconds; the two are worth keeping straight.

plx_a3f91b7c4d_Kd8vQ2mR7wXpL4nZ1yB6tC0sE5uH9jF3aG7kM2oP1qS

First call
export PROXYLEXUS_API_KEY="plx_a3f91b7c4d_Kd8vQ2mR7wXpL4nZ1yB6tC0sE5uH9jF3aG7kM2oP1qS"

curl -H "Authorization: Bearer $PROXYLEXUS_API_KEY" \
     https://proxylexus.com/api/v1/account

No dependencies. Keep the key in the environment, never in the command — a shell history is a log too.

Reads the account behind the key. If this returns 200, everything else on this page will work.

What a key cannot do

A stolen key is strictly less dangerous than a stolen browser session, and that is a design constraint rather than a coincidence. Each of these is refused by the surface, not by a setting you could turn on.

Spend money
No endpoint buys traffic, changes a plan or touches a price. A key found in a leaked build log cannot charge a card, and there is no confirm-flag that unlocks it.
Create or revoke API keys
Key management is in the dashboard, behind your password. A key that could mint a key could not be revoked — you cut the first one and the second, minted moments earlier, keeps working. That is also why the ceiling is 10: whatever happens, every key is on one screen.
Change the account
No password change, no closure, no export. Those are the operations that make a password worth stealing, and a key is not a password with better ergonomics.
Read back a secret
Neither a proxy user’s secret nor an API key. Both are stored as digests only. No endpoint can return one, and no future endpoint can be added that does.
Attribute usage per proxy user
The meter counts bytes for the account, not per credential. There is no per-user or per-key byte figure anywhere, so rather than estimate one we do not offer it.
Be called from a browser
No CORS headers and no preflight handler, on purpose. An API key belongs on a server, and refusing the browser is a small push away from pasting one into front-end JavaScript.

Endpoints

Every request needs the bearer header. Every response is JSON, and every response is sent Cache-Control: no-store — these are account-scoped and must not sit in anything shared.

GET /api/v1/account

Read the account behind the key

The first call to make: it confirms the key works and tells you what it is attached to. Reading a balance is the one money-adjacent thing this API does, and it only reads.

There is no account id in the request and there is nowhere to put one. The key identifies exactly one account, every response is scoped to it, and a parameter that tried to say otherwise is refused with caller_supplied_identity.

Budget: 300 requests per 60 seconds, per key.

Request
curl -H "Authorization: Bearer $PROXYLEXUS_API_KEY" \
     https://proxylexus.com/api/v1/account
Response · 200
{
  "planId": "standard",
  "status": "active",
  "periodStart": "2026-08-14T09:12:44.108Z",
  "quotaBytes": 53687091200,
  "remainingBytes": 40265318400
}
Read the account behind the key response fields
FieldTypeMeaning
planIdstringThe bundle the most recent purchase came from. Informational — the money is the byte count.
statusstringAlways "active" here — a suspended or cancelled account leaves by the 409 below, which carries a status field of the same name. Read body.status on either path and you never have to know which one you are on.active
periodStartstringWhere the meter counts from. A purchase never moves it.
quotaBytesintegerBytes bought and not yet spent, at periodStart.
remainingBytesintegerWhat is left right now. This is the number to alert on.

Errors specific to this endpoint

409 not_provisioned
The account has never bought traffic, so there is no balance to report.
409 account_suspended
The subscription is suspended or cancelled. Carries status, the same field the 200 carries.
503 unavailable
The balance could not be read. We will not print a figure we could not take, and we will not send null either — an automation comparing null to a threshold pages a human during OUR outage.

GET /api/v1/usage

Read the meter

Bytes sent plus bytes received, counted on our own gateway as your connections pass through it. This is the endpoint to build an alert on.

When the meter cannot be read we answer 503 with the code unavailable — never a reading of zero. The distinction is the whole point of the branch: internally a reading we cannot take is treated as exhausted, because anything about to spend money should refuse on doubt. Printed on a dashboard, or fed to an automation that pages somebody, that same value is a lie about your balance. So it never leaves this endpoint as a number.

Budget: 300 requests per 60 seconds, per key.

Request
curl -H "Authorization: Bearer $PROXYLEXUS_API_KEY" \
     https://proxylexus.com/api/v1/usage
Response · 200
{
  "state": "ok",
  "usedBytes": 13421772800,
  "quotaBytes": 53687091200,
  "remainingBytes": 40265318400,
  "fractionUsed": 0.25,
  "billableBytes": 12884901888,
  "estimated": false
}
Read the meter response fields
FieldTypeMeaning
statestringwarn at 80% spent; exhausted means connections are no longer served.ok · warn · exhausted
usedBytesintegerSpent since periodStart.
quotaBytesintegerBought.
remainingBytesintegerquotaBytes minus usedBytes, floored at zero.
fractionUsednumberusedBytes over quotaBytes, 0 to 1.
billableBytesintegerThe settled portion — complete days only. Lower than usedBytes by however much of today has not settled.
estimatedbooleanTrue when the figure rests on an estimate rather than on attributable data. Do not reconcile an invoice against a true value.

Errors specific to this endpoint

503 unavailable
The meter could not be read. Your balance is unchanged and nothing has been spent — retry, and do not alert on it as an exhausted account. The body carries quotaBytes, which is known even when the meter is not.
409 not_provisioned
Nothing bought yet, so nothing to meter.

GET /api/v1/proxy-users

List your proxy users

Your automation’s inventory: every live proxy user on the account, oldest first. Revoked ones are not listed.

No secret is returned, and none can be. We store only a SHA-256 digest of it, so after the response that issued it there is nothing on our side to return. That is a property of the design rather than a gap in this endpoint — a secret we could read back is a secret a database dump would hand to somebody else.

Budget: 300 requests per 60 seconds, per key.

Request
curl -H "Authorization: Bearer $PROXYLEXUS_API_KEY" \
     https://proxylexus.com/api/v1/proxy-users
Response · 200
{
  "items": [
    {
      "id": 41,
      "label": "scraper_prod",
      "username": "8f3c9a1d4b7e2f6a5c0d9e81",
      "createdAt": "2026-09-01T08:31:02.884Z",
      "lastUsedAt": "2026-09-01T09:04:55.210Z"
    }
  ]
}
List your proxy users response fields
FieldTypeMeaning
itemsobject[]Live proxy users, oldest first.
items[].idintegerIdentifies the proxy user in the paths that take one.
items[].labelstringThe name you gave it.
items[].usernamestringThe credential half of the proxy username. Targeting is appended to it.
items[].createdAtstringWhen it was issued.
items[].lastUsedAtstring or nullWhen the gateway last authenticated it. Null until it is first used, and written at most once a minute.

POST /api/v1/proxy-users

Create a proxy user

Issues a credential against our gateway. This response is the only moment the secret exists in readable form anywhere in our system — store it before you do anything else. If you lose it, the answer is to create another and revoke this one, and that is the correct answer rather than a weakness.

Creating one spends nothing and reserves nothing, but it does open a channel that can spend, so it is the one endpoint gated on your balance: at zero it is refused with insufficient_balance while everything else on this page keeps working. The ceiling is 50 live proxy users per account.

A label may use letters, numbers, spaces and underscores. No dashes — the proxy username is dash-delimited, and one character class for both means the two can never drift into disagreeing about what is safe. An unknown field in the body is refused rather than ignored.

A label is taken for ever, not until you delete it. The uniqueness index deliberately covers deleted proxy users too, so that "scraper_prod spent 40 GB" is still unambiguous a year later — which means a create/delete loop burns names permanently. Label by run, not by role: scraper_prod_2026_09 can be re-created, scraper_prod cannot.

Budget: 30 requests per 60 seconds, per key.

Parameters
ParameterInRule
labelstringbody · required1 to 40 characters: letters, numbers, spaces and underscores, no dashes. Unique across every proxy user this account has ever created, deleted ones included — a name is not released by deleting it.
Request
curl -X POST -H "Authorization: Bearer $PROXYLEXUS_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{"label":"scraper_prod"}' \
     https://proxylexus.com/api/v1/proxy-users
Response · 201
{
  "id": 41,
  "label": "scraper_prod",
  "username": "8f3c9a1d4b7e2f6a5c0d9e81",
  "createdAt": "2026-09-01T08:31:02.884Z",
  "lastUsedAt": null,
  "secret": "u7Qm2XvL9pRt4Kd0YbN6cWzA1sJfH3eG"
}
Create a proxy user response fields
FieldTypeMeaning
idintegerIdentifies the proxy user in the paths that take one.
labelstringThe name you gave it.
usernamestringThe credential half of the proxy username. Targeting is appended to it.
createdAtstringWhen it was issued.
lastUsedAtstring or nullWhen the gateway last authenticated it. Null until it is first used, and written at most once a minute.
secretstringReturned here and never again. Not stored in readable form, not shown in the dashboard, not recoverable by support.

Errors specific to this endpoint

400 invalid_request
The body was not a JSON object, the label is missing or malformed, or an unknown field was sent. Carries field.
402 insufficient_balance
The balance is spent, so no new spending channel is opened. Carries remainingBytes. Nothing else about the account has changed, and every other endpoint here still answers.
409 conflict
The label is taken — including by a deleted proxy user, which carries field: "label" — or the account is at 50 live proxy users, which carries limit and current.
409 not_provisioned
The account has never bought traffic.
409 account_suspended
The subscription is suspended or cancelled. Carries status.
503 unavailable
The balance could not be confirmed, so nothing was created. Deliberately not a 402: a meter we could not read is doubt, and sending a customer with a full balance to a top-up page is the wrong kind of wrong.

DELETE /api/v1/proxy-users/{id}

Revoke a proxy user

Rotation is the reason most people want this API at all. Revoking is final: nothing can un-revoke a credential, because you still hold the secret and restoring it would restore a working proxy.

It does not stop instantly, and the response says so as a number rather than an adverb: stopsServingWithinSeconds is 30. The gateway authenticates every connection and therefore caches a credential that long before asking the database again, and it never cuts a connection already open. An automation that revokes and then asserts the proxy is dead has to wait that long first — which is why the figure is in the body, where it can be compared to a clock.

An id that belongs to somebody else returns not_found, exactly as an id that does not exist does. Ownership is part of the query rather than a check before it, so there is no window in between.

Budget: 30 requests per 60 seconds, per key.

Parameters
ParameterInRule
idintegerpath · requiredThe proxy user id, from the list endpoint.
Request
curl -X DELETE -H "Authorization: Bearer $PROXYLEXUS_API_KEY" \
     https://proxylexus.com/api/v1/proxy-users/41
Response · 200
{
  "id": 41,
  "revoked": true,
  "stopsServingWithinSeconds": 30
}
Revoke a proxy user response fields
FieldTypeMeaning
idintegerThe proxy user that was revoked.
revokedbooleanTrue. The row is revoked, and nothing can un-revoke it.
stopsServingWithinSecondsintegerHow long the gateway may keep serving this credential. Wait it out before asserting the proxy is dead.

Errors specific to this endpoint

404 not_found
No live proxy user with that id on your account. Deleting one twice returns this the second time, which is not an error worth retrying.
400 invalid_request
The id in the path is not a positive integer. Distinct from 404: the URL is malformed rather than merely pointing at nothing. Carries field: "id".

POST /api/v1/proxy-users/{id}/connection

Assemble a proxy endpoint

Turns targeting into the line your client connects with. No network call happens — it is string assembly, validated against exactly the rules the gateway will apply when your connection arrives, so anything this accepts will connect and anything it refuses would have failed at connect time instead.

The line comes back as a TEMPLATE with the secret’s position named: replace secretPlaceholder with the secret you saved when you created the proxy user. We hold only a digest of it and cannot fill it in. A placeholder is returned rather than an unannounced dummy or a null because those two produce, respectively, a customer who pasted a placeholder into production and a client that rendered "null" into the password.

It also echoes the targeting back, after defaults and normalisation. Compare the echo with the username when something surprises you: rotation -1 appears in the echo and NOT in the username, and that is correct. Written out, rotation--1 makes the proxy answer 407; expressed by omission it means a new address per request, which is what -1 means and is also the default.

Why POST for something that reads nothing: targeting is a structured object with mutually exclusive fields, and the alternative is a query string. A query string is exactly where an API key must never go, and the surest way to stop somebody putting one there is to give them no URL to append it to.

Budget: 300 requests per 60 seconds, per key.

Parameters
ParameterInRule
idintegerpath · requiredThe proxy user whose credential the username is built on.
countrystringbody · requiredISO 3166-1 alpha-2, case-insensitive. Required on every connection — the network has no "anywhere".
proxyTypestringbodyDefaults to residential. All three bill at the same rate against the same balance.residential · mobile · datacenter
protocolstringbodyDefaults to http. `https` is REFUSED with a 400 rather than accepted: https:// as a proxy scheme means TLS to the proxy itself and there is no TLS listener, so a line built with it could never connect. https:// destinations are unaffected and are the normal case — they ride a CONNECT tunnel through either scheme.http · socks5
statestringbodyA state from the location catalogue. Mutually exclusive with city.
citystringbodyA city from the location catalogue. Letters, digits and underscores only — spaces become underscores, and a name with an accent or a dash has no representation in the username at all.
asnstringbodyDigits only, no AS prefix. There is no catalogue to browse — this is for somebody who already knows the number.
rotationintegerbody-1 is a new address per request and is the default. 0 is sticky. The rest are minutes.-1 · 0 · 5 · 10 · 15 · 20 · 60
sessionstringbodyNames a sticky session so you can come back to the same address. Requires rotation 0; letters, digits and underscores only.
Request
curl -X POST -H "Authorization: Bearer $PROXYLEXUS_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{"country":"DE","proxyType":"residential","rotation":0,"session":"job_42"}' \
     https://proxylexus.com/api/v1/proxy-users/41/connection
Response · 200
{
  "proxyUser": { "id": 41, "label": "scraper_prod" },
  "host": "gateway.proxylexus.com",
  "port": 10800,
  "protocol": "http",
  "username": "8f3c9a1d4b7e2f6a5c0d9e81-type-residential-country-DE-rotation-0-session-job_42",
  "connectionTemplate": "http://8f3c9a1d4b7e2f6a5c0d9e81-type-residential-country-DE-rotation-0-session-job_42:{secret}@gateway.proxylexus.com:10800",
  "secretPlaceholder": "{secret}",
  "targeting": {
    "proxyType": "residential",
    "country": "DE",
    "protocol": "http",
    "rotation": 0,
    "session": "job_42"
  }
}
Assemble a proxy endpoint response fields
FieldTypeMeaning
proxyUserobjectWhich credential this line was built on.
proxyUser[].idintegerThe id from the path.
proxyUser[].labelstringIts label, so a log line can name the job.
hoststringOur gateway. Always ours — your traffic is metered as it passes through it.
portintegerOne port for both protocols: the gateway reads the first byte and works out which you are speaking.
protocolstringWhat you asked for.http · socks5
usernamestringCredential and targeting, dash-delimited. Use it as-is; it is 255 bytes at most, which is what SOCKS5 can carry.
connectionTemplatestringThe whole line, with secretPlaceholder standing in for the password. Substitute and connect.
secretPlaceholderstringThe exact substring to replace. Returned rather than documented as a constant, so your code substitutes what we actually used instead of what a page said last year.
targetingobjectWhat we understood, after defaults and normalisation: country upper-cased, proxyType and protocol defaulted. Only the fields that apply are present.
targeting[].proxyTypestringDefaulted to residential when you did not send one.residential · mobile · datacenter
targeting[].countrystringUpper-cased.
targeting[].protocolstringDefaulted to http.http · socks5
targeting[].statestringPresent only if you sent one.
targeting[].citystringPresent only if you sent one.
targeting[].asnstringPresent only if you sent one.
targeting[].rotationintegerPresent only if you sent one. -1 appears here and never in the username.-1 · 0 · 5 · 10 · 15 · 20 · 60
targeting[].sessionstringPresent only if you sent one.

Errors specific to this endpoint

400 invalid_request
Country missing or not two letters; an unknown parameter; state and city together; a session without rotation 0; a name with a dash or an accent; a username over 255 bytes. Always carries field.
404 not_found
No live proxy user with that id on your account. An id belonging to somebody else answers identically — a 403 would confirm the row exists.

GET /api/v1/locations

List what is stocked

Countries, and the cities and states available inside them, for one proxy type. You need this to know that DE is stocked for mobile before you build a connection that targets it.

Every place comes back as a pair: name is for showing a human, value is the string to put in city or state. They differ because the proxy username is dash-delimited, so a place name is only usable if it survives that — spaces become underscores, and a name carrying a dash, an apostrophe or an accent cannot be carried at all. Send value and a connection always builds; retype name and it may not.

A name that cannot be carried is left out and counted in omitted, rather than dropped silently. That count is there so a customer who cannot find Düsseldorf can see that we know it is missing, instead of concluding we do not stock Germany properly. Countries are never omitted — a country code is two ASCII letters and always survives.

The states array is a list of country-level subdivisions, not a mapping of cities to the state they sit in. Pick from one array or the other, never both.

Cache it. It changes on the order of weeks, every call reaches the network behind us, and the budget here is a tenth of the read budget for exactly that reason. Fetch it at start-up, keep it, refetch daily at most. The example below is abridged to one country; the live response carries every country stocked for that type.

Budget: 30 requests per 60 seconds, per key.

Parameters
ParameterInRule
typestringqueryDefaults to residential.residential · mobile · datacenter
Request
curl -H "Authorization: Bearer $PROXYLEXUS_API_KEY" \
     "https://proxylexus.com/api/v1/locations?type=residential"
Response · 200
{
  "type": "residential",
  "items": [
    {
      "countryCode": "DE",
      "name": "Germany",
      "cities": [
        { "name": "Berlin", "value": "Berlin" },
        { "name": "Frankfurt am Main", "value": "Frankfurt_am_Main" },
        { "name": "Hamburg", "value": "Hamburg" }
      ],
      "states": [
        { "name": "Bavaria", "value": "Bavaria" },
        { "name": "Hesse", "value": "Hesse" }
      ],
      "omitted": { "cities": 14, "states": 2 }
    }
  ]
}
List what is stocked response fields
FieldTypeMeaning
typestringThe type you asked for.residential · mobile · datacenter
itemsobject[]One entry per stocked country, ordered by name.
items[].countryCodestringWhat to send as country.
items[].namestringCountry name, for display.
items[].citiesobject[]Ordered by name. Send the value, show the name.
items[].cities[].namestringThe place, spelled the way a person writes it.
items[].cities[].valuestringThe same place, encoded for the username. Send this as city.
items[].statesobject[]Same pairs, for state. Use a state instead of a city, never as well as one.
items[].states[].namestringThe subdivision, spelled the way a person writes it.
items[].states[].valuestringThe same subdivision, encoded. Send this as state.
items[].omittedobjectHow many names under this country the username grammar cannot express. Almost always accented ones.
items[].omitted[].citiesintegerCities left out of the list above.
items[].omitted[].statesintegerStates left out of the list above.

Errors specific to this endpoint

400 invalid_request
type was not one of residential, mobile, datacenter. Carries field: "type".
503 unavailable
The catalogue could not be read. Retry, or keep using the copy you cached — it changes on the order of weeks, which is exactly why you should have one.

Using the proxy user

The connection endpoint returns the whole line with one slot left open: secretPlaceholder marks where the password goes, and the password is the secret you saved when you created the proxy user. We hold only a digest of it, so that substitution is the one step we cannot do for you. Replace the placeholder the response gave you rather than one copied from a page, and every client below takes the result verbatim.

Proxy line
http://8f3c9a1d4b7e2f6a5c0d9e81-type-residential-country-DE-rotation-0-session-job_42:{secret}@gateway.proxylexus.com:10800

What POST /proxy-users/{id}/connection gives you, with the password’s slot named.

Both protocols answer on gateway.proxylexus.com:10800: the gateway reads the first byte and works out whether you are speaking HTTP or SOCKS5, so switching is a change of scheme and not of address. The examples fetch an address echo, because the only output that proves anything is the address the destination actually saw.

Request through the proxy
curl -x "http://8f3c9a1d4b7e2f6a5c0d9e81-type-residential-country-DE-rotation-0-session-job_42:u7Qm2XvL9pRt4Kd0YbN6cWzA1sJfH3eG@gateway.proxylexus.com:10800" \
     https://api.ipify.org

No dependencies. curl takes a socks5:// line at -x just as happily.

A job, end to end

Check the balance, issue a credential, build a targeted line, use it, revoke it. The three failures worth telling apart are handled differently, because they need different answers: 429 waits, 402 stops — no amount of retrying makes a balance appear — and a 503 is ours to recover from, not yours.

In a real automation you would create the proxy user once and keep the secret, not create and revoke one per run. It is written this way so the script leaves nothing behind.

Python
import os
import time

import requests

API = "https://proxylexus.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['PROXYLEXUS_API_KEY']}"}


def call(method, path, body=None):
    """One request, with the three failures worth telling apart."""
    for attempt in range(4):
        r = requests.request(method, API + path, headers=HEADERS, json=body, timeout=30)
        if r.ok:
            return r.json()

        # Errors are always JSON here, but something in front of us may not be.
        err = r.json() if "json" in r.headers.get("content-type", "") else {}
        code = err.get("code", "")

        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "5")))
            continue
        if code in ("unavailable", "internal"):
            time.sleep(2 ** attempt)          # ours, and retryable
            continue
        raise SystemExit(f"{r.status_code} {code}: {err.get('error', r.text)}")

    raise SystemExit("gave up after four attempts")


account = call("GET", "/account")
if account["remainingBytes"] <= 0:
    raise SystemExit("no traffic left - top up before running this")

# The secret is in THIS response and in no other, ever. Store it before anything else.
#
# The label is unique per run because labels are unique per account: a second run with a fixed
# "demo_job" answers 409 label_taken, which would make this example work exactly once. Revoking
# frees the label again, so a real job can reuse a stable name -- this one cannot assume you
# revoked.
label = f"demo_job_{int(time.time())}"
user = call("POST", "/proxy-users", {"label": label})
secret = user["secret"]

endpoint = call(
    "POST",
    f"/proxy-users/{user['id']}/connection",
    {"country": "DE", "proxyType": "residential", "rotation": 0, "session": "job_42"},
)

# The line comes back with the secret's slot named, because we hold only a digest of it.
# Substitute the placeholder the response gives you, never one written down here.
proxy = endpoint["connectionTemplate"].replace(endpoint["secretPlaceholder"], secret)

seen = requests.get(
    "https://api.ipify.org",
    proxies={"http": proxy, "https": proxy},
    timeout=30,
)
print("exit address:", seen.text)

# Rotation is create, switch, wait, revoke - in that order. The DELETE response says how long
# the gateway may still serve this credential; nothing cuts a connection already open.
revoked = call("DELETE", f"/proxy-users/{user['id']}")
print("revoked; stops serving within", revoked["stopsServingWithinSeconds"], "seconds")

pip install requests

Limits, stated up front

Each of these is something you would otherwise find out at three in the morning. We would rather you read it now and design around it.

Destinations are limited to ports 80, 443, 8080, 8443
Anything else is refused at the gateway, on both the HTTP and the SOCKS5 path. Without that rule the product is also an open mail relay and an SSH jump host, and the complaints land on the addresses every customer shares.
There is no https:// proxy scheme
As a proxy scheme, https:// means TLS to the proxy itself, and there is no TLS listener to speak it to. The protocol parameter still accepts the value, because it mirrors the grammar the network behind us publishes — so it is possible to build a line that has nothing to connect to. Use http:// or socks5://, both on the same port. None of this touches your destinations: https:// URLs are the normal case and ride a CONNECT tunnel over the http:// line, encrypted end to end. We cannot read them and do not want to.
UDP is refused
A SOCKS5 client asking for UDP ASSOCIATE, or for BIND, gets "command not supported" at the handshake. The path to the network is a TCP tunnel, so no answer at that handshake could make UDP work — and claiming it and then dropping the datagrams is worse than saying no.
A proxy user’s secret is shown once
It exists in the response that created it and nowhere else afterwards. We keep a SHA-256 digest, which is enough to check a connection and not enough to reconstruct anything. No endpoint can return it, support cannot recover it, and a dump of our database does not hand anyone working proxy access.
A top-up is not instant at the gateway
The gateway caches an account’s balance for about fifteen seconds. After a purchase, connections may be refused for a few seconds more before the new balance is seen. Nothing is lost and nothing needs reissuing — wait, then retry.
Revoking a proxy user takes up to 30 seconds
The gateway authenticates every connection and therefore holds a credential that long before asking the database again, and it never cuts a connection already open. The DELETE response returns the figure as stopsServingWithinSeconds so an automation can compare it to a clock rather than to an adverb. Revoking an API KEY is a different promise: nothing caches that table, so the very next API call with it fails.
Country is required, and city and state are exclusive
Every connection names a country. Then a city or a state, never both — sending both is a request whose meaning depends on which one a parser reads last, so it is refused instead. Send the value the location catalogue gives you rather than a name you typed: names are letters, digits and underscores, spaces become underscores, and a place carrying an accent, a dash or an apostrophe cannot be expressed in a proxy username at all. Those are left out of the catalogue and counted, so you can see what is missing instead of guessing.
A deleted proxy user keeps its name for ever
Labels are unique across every proxy user an account has ever created, deleted ones included. That is deliberate — it is what keeps "scraper_prod spent 40 GB" unambiguous a year later — and it means a create-and-delete loop burns names permanently. Label by run rather than by role: scraper_prod_2026_09 can be created again, scraper_prod cannot.
Usage is account-wide, and today has not settled
usedBytes moves as traffic passes; billableBytes counts complete days only, so the two differ by however much of today has happened. Reconcile against billableBytes. Neither figure splits by proxy user, country or destination — the meter records bytes and a timestamp, and our privacy policy commits to not keeping the addresses you connect to.
One account, many connections, one ceiling
Up to 50 live proxy users and 10 live API keys per account, neither of them charged for. The gateway also caps how many connections one account may hold open at once, because every proxy user is another parallel channel spending the same balance.

What you may point the proxies at is unchanged by the fact that a script asked. The same policy applies whether a connection came from the dashboard or from a cron job.

Acceptable use

Errors

Every non-2xx answer from an endpoint below is the same two fields. code is the machine half and is frozen for the life of v1 — branch on it. error is a sentence for whoever reads the logs, and it may be reworded at any time; if you match on it, a copy edit becomes an outage.

One exception, and it is worth coding for: calling a path with a method it does not serve — DELETE on a list, say — is answered by the framework before any of this runs, so you get a bare 405 with no JSON body at all. Treat a missing code as a bug in the call rather than a new error kind.

Some responses carry one more field: field on a validation failure, status on a suspended account. Handle a code you have never seen by falling back on the HTTP status — new codes will be added, and that is not a breaking change.

Error body
{
  "error": "Traffic balance exhausted. Top up to create proxy users again.",
  "code": "insufficient_balance"
}
Every error code and what to do about it
CodeHTTPWhat to do
invalid_request400Fix the request. Carries `field` when we can say which one is wrong. Retrying unchanged will fail identically.
credential_in_url400Move the key into the Authorization header, then revoke it and mint another.
caller_supplied_identity400Drop the parameter. There is no supported way to ask for another account.
unauthorized401Stop and alert a human. Retrying spends the failed-authentication budget for your whole address.
insufficient_balance402Top up. Reading usage and revoking still work at zero; only creating is refused.
not_found404The id does not exist, or does not belong to you. The two are deliberately indistinguishable.
conflict409A ceiling was reached or a label is taken. Revoke something, or pick another label.
not_provisioned409The account has never bought traffic. Nothing to retry until somebody does.
account_suspended409Alert a human. Carries `status`, so your alert can say which.
rate_limited429Wait Retry-After seconds, then retry. Do not retry immediately.
unavailable503Retry with backoff. This is us, not you — and never a reading of zero.
internal500Retry with backoff. If it persists, contact support.

These come back from any endpoint, before it runs at all — unauthorized, credential_in_url, caller_supplied_identity, rate_limited — along with unavailable, internal, which mean the request never reached what it needed. Handle those once, in whatever wraps your requests, rather than seven times.

Rate limits

Counted per key, not per address. An automation is one address making thousands of calls, so an address-keyed limit would either throttle a legitimate build runner or bound nothing at all. One budget per kind of work:

read · 300/60s
Five a second sustained, which is far past anything a dashboard-shaped automation does.
write · 30/60s
Creating thirty proxy users a minute against a ceiling of 50 is already a loop that has gone wrong, and every one it makes is another channel spending the same balance.
catalogue · 30/60s
Tighter, because every call reaches the network behind us. The catalogue changes on the order of weeks — read it once and cache it.

Failed authentications are counted differently — per source address, 30 in 15 minutes — because a wrong key has no key to count against. The count is cleared the moment a key works, so a runner that was redeployed with a corrected key is not still serving the old one’s sentence.

Headers
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 30
RateLimit-Remaining: 0
RateLimit-Reset: 44          # seconds from now, NOT a timestamp
Retry-After: 44

RateLimit-Limit and RateLimit-Remaining come back on every response. RateLimit-Reset and Retry-After appear only on a 429, and both are seconds from now rather than a timestamp — the ambiguity that otherwise makes a back-off either instant or set to 1970.

Treat the headers as the contract and the numbers above as today’s value. Limits may be tuned; the headers will always say what they currently are.

What v1 promises

For the life of v1

  • A field keeps its name, its type and its meaning. Bytes stay bytes — remainingBytes will never quietly become gigabytes.
  • A code keeps its meaning.
  • An endpoint keeps its path, its method and its success status.
  • Authentication stays Authorization: Bearer.

And what it does not

  • New fields will appear in responses. Ignore the ones you do not recognise.
  • New code values will appear. Fall back on the HTTP status.
  • New endpoints and new optional parameters will appear.
  • The error sentence may be reworded at any time. Never parse it.
  • Rate limits may be tuned. Read the headers.

A change that breaks one of the four promises above is a v2, and v1 would run beside it for a stated period rather than being switched off. Which is also why a key records when it was last used: it is how we know whose automation to write to before anything moves.

Next

Create a key, run the first call, and you are three lines from an automation.

API documentation · Proxylexus