Whitelist (IP Auth)
Bind source IPs to a Proxio credential for passwordless authentication, with optional default geo and session settings. List, add, batch add, and remove bindings. Up to 50 per credential, 30 additions per minute. Covers INVALID_IP, IP_ALREADY_BOUND, and IP_UNAVAILABLE.
IP authentication lets a credential authenticate by source IP instead of a password: connect from a whitelisted IP and the gateway trusts you without credentials in the proxy URL. Each binding can also carry default geo and session settings applied when the request doesn't specify its own.
Bindings live under a credential. Up to 50 per credential.
Scopes: read for GET, write for add and remove.
List bindings
GET /services/{id}/credentials/{credId}/whitelist returns the full list (up
to 50), no cursor.
curl https://dashboard.proxio.net/api/v1/services/clpkg_2a9x/credentials/clsub_7h2k/whitelist \
-H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"import requests
BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
resp = requests.get(
f"{BASE}/services/clpkg_2a9x/credentials/clsub_7h2k/whitelist",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=15,
)
resp.raise_for_status()
print(resp.json()["data"])const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
const res = await fetch(
`${BASE}/services/clpkg_2a9x/credentials/clsub_7h2k/whitelist`,
{ headers: { Authorization: `Bearer ${API_KEY}` } },
)
console.log((await res.json()).data)200 response:
{
"data": [
{
"id": "clbind_3k9v",
"ip": "203.0.113.5",
"default_country": "us",
"default_state": null,
"default_city": "newyork",
"default_sticky": true,
"default_sesstime": 10,
"created_at": "2026-07-16T12:00:00.000Z"
}
],
"meta": { "request_id": "req_8Ke2jP4mQ" }
}Add a binding
POST /services/{id}/credentials/{credId}/whitelist.
Body
| Field | Type | Notes |
|---|---|---|
ip | string | Public, routable IP to whitelist. Required. |
default_country | string | null | ISO2 country applied by default. |
default_state | string | null | ISO 3166-2 state code. |
default_city | string | null | City slug. |
default_sticky | boolean | Whether requests default to a sticky session. Defaults to false. |
default_sesstime | number | Default session window, 1 to 90 minutes. Only stored when default_sticky is true. |
default_country, default_state, and default_city each pass through the
same normalization the username grammar
uses: lowercase, with everything outside a-z0-9 stripped (no dashes
preserved). That's a correct mechanical slug for a city name ("New York" ->
newyork), but default_state isn't a slug of the state's name, it's the
ISO 3166-2 code, and normalization can't derive one from the other
("California" slugs to california, not the actual code ca). Don't
hand-type a display name into default_state. Send the exact code value
GET /locations gives you for country, state, and
city alike: those codes are already lowercase and dash-free, so
normalization is a no-op on them and the stored default matches what the
gateway expects.
default_sesstime depends on default_sticky
The two are not independent. If default_sticky is false or omitted,
default_sesstime is discarded and stored as null, without a warning and
without an error: the request still returns 201, and the binding you get
back has "default_sesstime": null.
To set a session window, send both fields together:
{ "default_sticky": true, "default_sesstime": 10 }. Read
default_sesstime back from the response rather than assuming the value you
sent was kept.
curl -X POST \
https://dashboard.proxio.net/api/v1/services/clpkg_2a9x/credentials/clsub_7h2k/whitelist \
-H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "ip": "203.0.113.5", "default_country": "us", "default_city": "newyork", "default_sticky": true, "default_sesstime": 10 }'import uuid
import requests
BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
resp = requests.post(
f"{BASE}/services/clpkg_2a9x/credentials/clsub_7h2k/whitelist",
headers={
"Authorization": f"Bearer {API_KEY}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"ip": "203.0.113.5",
"default_country": "us",
"default_city": "newyork",
"default_sticky": True,
"default_sesstime": 10,
},
timeout=15,
)
resp.raise_for_status()
print(resp.status_code, resp.json()["data"]) # 201import { randomUUID } from "node:crypto"
const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
const res = await fetch(
`${BASE}/services/clpkg_2a9x/credentials/clsub_7h2k/whitelist`,
{
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(),
},
body: JSON.stringify({
ip: "203.0.113.5",
default_country: "us",
default_city: "newyork",
default_sticky: true,
default_sesstime: 10,
}),
},
)
console.log(res.status, (await res.json()).data) // 201201 response returns the created binding in the list-item shape above.
IP validation and conflicts
Only a public, routable address can be whitelisted. Anything else is rejected
with INVALID_IP (400); details[0].reason is
INVALID_IP when the address is malformed, or PRIVATE_OR_RESERVED for any
address that isn't eligible, which covers private, reserved, loopback,
link-local, and CGNAT ranges. If the IP is already
whitelisted on one of your own credentials, the call returns
IP_ALREADY_BOUND (409) with
details: [{ "credential_id": ... }] naming that credential. An IP that is
unavailable for whitelisting is rejected with
IP_UNAVAILABLE (409), with no reason given. Hitting the
per-credential cap returns
LIMIT_REACHED (409) with
details: [{ "limit": 50 }].
Additions are rate-limited
Whitelist additions are limited to 30 per 60 seconds per account (on top of
your per-key rate limit). Exceeding it returns
RATE_LIMITED (429) with a Retry-After
header.
Batch add bindings
POST /services/{id}/credentials/{credId}/whitelist/batch adds up to 50
IPs to one credential in a single request, instead of 50 round trips against
the 30-per-minute add budget above. Same scope (write), same field-level
validation as adding one.
Body
| Field | Type | Notes |
|---|---|---|
ips | array | Required, 1 to 50 items. Each item is either a bare IP string, or the same object POST /whitelist takes (ip plus the default_* fields). Mix and match freely in one array. |
curl -X POST \
https://dashboard.proxio.net/api/v1/services/clpkg_2a9x/credentials/clsub_7h2k/whitelist/batch \
-H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "ips": ["203.0.113.5", { "ip": "203.0.113.6", "default_country": "us" }] }'import uuid
import requests
BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
resp = requests.post(
f"{BASE}/services/clpkg_2a9x/credentials/clsub_7h2k/whitelist/batch",
headers={
"Authorization": f"Bearer {API_KEY}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={"ips": ["203.0.113.5", {"ip": "203.0.113.6", "default_country": "us"}]},
timeout=15,
)
resp.raise_for_status()
body = resp.json()
print(body["meta"]) # {'requested': 2, 'created': 2, 'failed': 0}import { randomUUID } from "node:crypto"
const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
const res = await fetch(
`${BASE}/services/clpkg_2a9x/credentials/clsub_7h2k/whitelist/batch`,
{
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(),
},
body: JSON.stringify({ ips: ["203.0.113.5", { ip: "203.0.113.6", default_country: "us" }] }),
},
)
const body = await res.json()
console.log(body.meta) // { requested: 2, created: 2, failed: 0 }200 response (not 201, see the callout below):
{
"data": [
{
"index": 0,
"ip": "203.0.113.5",
"status": "created",
"binding": {
"id": "clbind_3k9v", "ip": "203.0.113.5", "default_country": null,
"default_state": null, "default_city": null, "default_sticky": false,
"default_sesstime": null, "created_at": "2026-07-16T12:00:00.000Z"
},
"error": null
},
{
"index": 1,
"ip": "203.0.113.6",
"status": "error",
"binding": null,
"error": {
"code": "IP_ALREADY_BOUND",
"message": "This IP is already whitelisted on one of your own credentials.",
"doc_url": "https://docs.proxio.net/docs/api/errors#ip_already_bound",
"details": [{ "credential_id": "clsub_2b8n" }]
}
}
],
"meta": { "requested": 2, "created": 1, "failed": 1, "request_id": "req_8Ke2jP4mQ" }
}Status semantics: 200 either way, per-item is where success or failure lives
A request-level failure, a malformed body, an unknown credential, or the
batch throttle below, is an ordinary error envelope (400, 404, or
429), nothing in the batch ran. Once the request is accepted, every item
gets an answer and the response is always 200, even when every single
item failed: meta.failed equal to meta.requested is how you detect
that, not the HTTP status. data[] is in request order, one entry per
input, each carrying exactly one of binding or error (the other is
null), so a generated client has one fixed shape to type. An item's
error is field-for-field what POST /whitelist would
have returned for that IP on its own, INVALID_IP,
IP_ALREADY_BOUND,
IP_UNAVAILABLE, or
LIMIT_REACHED once the 50-per-credential
cap is hit partway through the array (every item after that point is a
LIMIT_REACHED error without a validation attempt).
Its own throttle, on top of the shared one
A batch call costs one token from the same 30-per-minute add budget
every single add shares, not one token per IP, plus one token from a
separate budget of 3 batch calls per 60 seconds per account. Both must
have room or the whole request is refused with
RATE_LIMITED (429) and a Retry-After
header, before anything in the batch runs. The combination bounds how many
IPs one account can probe per minute to a fixed multiple of the single-add
budget, a flat one-token cost per batch would have let one call submit far
more IPs per minute than adding them one at a time ever could.
Retrying without a key re-runs every item
Idempotency-Key is accepted here, not required. Send one and a retry
with the same key replays the exact stored result, no items are re-run.
Retry the same body without a key, after a timeout where you don't know
whether the first attempt landed, for example, and every item runs again
from scratch: an IP that already succeeded comes back as an
IP_ALREADY_BOUND item error (harmless, but it is a real per-item error,
not a skip), while an IP that hadn't been reached yet (say, because the
per-credential cap was hit partway through the first attempt) gets a fresh
try. Send an Idempotency-Key with every batch call and reuse it on retry
if you want the original result back untouched instead of re-running the
whole array.
Remove a binding
DELETE /services/{id}/credentials/{credId}/whitelist/{bindingId} returns
204 No Content.
curl -X DELETE \
https://dashboard.proxio.net/api/v1/services/clpkg_2a9x/credentials/clsub_7h2k/whitelist/clbind_3k9v \
-H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"import requests
BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
resp = requests.delete(
f"{BASE}/services/clpkg_2a9x/credentials/clsub_7h2k/whitelist/clbind_3k9v",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=15,
)
print(resp.status_code) # 204const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
const res = await fetch(
`${BASE}/services/clpkg_2a9x/credentials/clsub_7h2k/whitelist/clbind_3k9v`,
{ method: "DELETE", headers: { Authorization: `Bearer ${API_KEY}` } },
)
console.log(res.status) // 204Update means delete and re-add
There's no PATCH for a binding in v1. To change an IP or its defaults, delete the binding and add a new one.
Related pages
Proxy List
Generate ready-to-use Proxio proxy lines with GET /services/{id}/proxy-list. Targeting, including ASN, is embedded in the username, output as txt, json, or csv, with the full username grammar, every query parameter (count, sesstime, retry, retry_rotate, session_id, asn), sticky vs rotating, and practical recipes.
Sessions
List and rotate a Proxio credential's active sticky sessions. GET returns every active session with remaining TTL, DELETE expires one or all. Rotation is rate-limited per credential.

