Authentication and conventions
How access and refresh tokens work, the Origin requirement, the uniform response envelope, and pagination conventions.
Everything shared across the client API lives here: how to obtain tokens, why write operations get blocked with 403, what the error shape looks like, and how pagination works. Individual endpoints are in the endpoint list.
Two kinds of token
| Access token | Refresh token | |
|---|---|---|
| Prefix | at_ | rt_ |
| Length | Prefix plus 43 random characters | Prefix plus 43 random characters |
| Transport | Authorization: Bearer at_... | HttpOnly cookie |
| Default lifetime | 15 minutes | 12 hours |
| With "remember me" | Unchanged | 30 days |
Logging in
/api/v1/auth/logincurl -X POST 'https://api.example.com/api/v1/auth/login' \
-H 'Origin: https://console.example.com' \
-H 'Content-Type: application/json' \
-c cookies.txt \
-d '{"email":"you@example.com","password":"YOUR_PASSWORD","remember":true}'
-c cookies.txt saves the refresh cookie, which you'll need to refresh the token later.
Accounts with two-factor enabled
A successful password check does not return an access token directly; it returns an MFA ticket valid for 5 minutes, requiring a second call:
/api/v1/auth/login/two-factorcurl -X POST 'https://api.example.com/api/v1/auth/login/two-factor' \
-H 'Origin: https://console.example.com' \
-H 'Content-Type: application/json' \
-c cookies.txt \
-d '{"ticket":"MFA_TICKET","code":"123456"}'
The TOTP parameters are standard: SHA1, 6 digits, 30-second period, with a validation window of the current period ±1 (allowing 30 seconds of clock skew).
Refreshing the token
An access token lasts only 15 minutes, so long-running programs need to refresh periodically:
/api/v1/auth/refreshcurl -X POST 'https://api.example.com/api/v1/auth/refresh' \
-H 'Origin: https://console.example.com' \
-b cookies.txt -c cookies.txt
-b sends the refresh cookie; -c saves the possibly rotated replacement.
Write operations require Origin
Matching is an exact string comparison, not a prefix or domain match. https://console.example.com and https://console.example.com/ differ, as do http:// and https://.
The cookies themselves are HttpOnly with SameSite=Lax; production additionally enforces Secure and the __Host- prefix.
The uniform response envelope
Success:
{ "data": { } }
Failure:
{
"error": {
"code": "machine_readable_code",
"message": "human-readable explanation",
"fields": { "FieldName": "validationTag" }
}
}
code is a stable machine-readable identifier and is what your program should branch on, not message (which changes as copy is revised). fields appears only on validation failures.
Pagination
The general convention:
| Parameter | Default | Maximum |
|---|---|---|
page | 1 | none |
pageSize | 20 | 100 |
Response:
{
"data": {
"items": [],
"page": 1,
"pageSize": 20,
"totalItems": 137,
"totalPages": 7
}
}
Division of labour between API keys and access tokens
| Access token | API key | |
|---|---|---|
| Scope | All /api/v1 client endpoints | Only the Extract API |
| Belongs to | A site user | One proxy sub-account (one key each) |
| Transport | Authorization: Bearer | Query parameter ?apikey= |
| Lifetime | 15 minutes | Long-lived, until revoked |
| Extra requirement | Origin on writes | Source IP must be whitelisted |
They are not interchangeable.
A usable client wrapper
import threading
import time
import requests
class NextProxyClient:
"""Client API wrapper with automatic token refresh.
Access tokens last 15 minutes, so replace them before expiry rather than
waiting for a 401 — the latter wastes one request on every rollover.
"""
# Refresh this many seconds early, leaving room for the round trip
REFRESH_MARGIN = 120
def __init__(self, base_url: str, origin: str) -> None:
self._base = base_url.rstrip("/")
self._origin = origin
self._session = requests.Session()
self._token: str | None = None
self._expires_at = 0.0
self._lock = threading.Lock()
def login(self, email: str, password: str, *, remember: bool = True) -> None:
payload = {"email": email, "password": password, "remember": remember}
data = self._post("/api/v1/auth/login", payload)
if "mfaTicket" in data:
raise RuntimeError("two-factor is enabled; use login_with_totp")
self._store_token(data)
def login_with_totp(self, email: str, password: str, code: str) -> None:
data = self._post(
"/api/v1/auth/login",
{"email": email, "password": password, "remember": True},
)
ticket = data.get("mfaTicket")
if not ticket:
self._store_token(data)
return
# The MFA ticket is valid for 5 minutes only
data = self._post(
"/api/v1/auth/login/two-factor",
{"ticket": ticket, "code": code},
)
self._store_token(data)
def get(self, path: str, **params) -> dict:
response = self._session.get(
self._base + path,
params=params or None,
headers=self._headers(),
timeout=30,
)
return self._unwrap(response)
def post(self, path: str, payload: dict) -> dict:
return self._post(path, payload, authenticated=True)
def _post(self, path: str, payload: dict, *, authenticated: bool = False) -> dict:
headers = self._headers() if authenticated else {"Origin": self._origin}
response = self._session.post(
self._base + path, json=payload, headers=headers, timeout=30,
)
return self._unwrap(response)
def _headers(self) -> dict[str, str]:
self._ensure_token()
return {
"Authorization": f"Bearer {self._token}",
# Writes require a trusted Origin or they fail with 403 origin_rejected
"Origin": self._origin,
}
def _ensure_token(self) -> None:
with self._lock:
if self._token and time.time() < self._expires_at - self.REFRESH_MARGIN:
return
response = self._session.post(
self._base + "/api/v1/auth/refresh",
headers={"Origin": self._origin},
timeout=30,
)
self._store_token(self._unwrap(response))
def _store_token(self, data: dict) -> None:
self._token = data["accessToken"]
# The server returns seconds; fall back to the 15-minute default
self._expires_at = time.time() + data.get("expiresIn", 900)
@staticmethod
def _unwrap(response: requests.Response) -> dict:
payload = response.json()
if response.ok:
return payload.get("data", payload)
error = payload.get("error", {})
raise RuntimeError(
f"HTTP {response.status_code} {error.get('code', 'unknown')}: "
f"{error.get('message', response.text[:200])}"
)
client = NextProxyClient("https://api.example.com", "https://console.example.com")
client.login("you@example.com", "YOUR_PASSWORD")
print(client.get("/api/v1/wallet"))
Logging out
/api/v1/auth/logout/api/v1/auth/logout-alllogout invalidates only the current token pair; logout-all invalidates every session for that user. Changing the password revokes old sessions automatically.
Common problems
| Symptom | Cause |
|---|---|
403 origin_rejected | A write without Origin, or a value not on the trusted list (exact match) |
401 though the token looks right | The access token expired (15 minutes only) |
| Refresh fails | The refresh cookie wasn't sent (-b cookies.txt) or has expired |
Login returns mfaTicket instead of a token | Two-factor is enabled; complete the second step |
| MFA ticket invalid | It lasts 5 minutes; restart the login flow |
429 | Risk-control rate limiting; see Limits and quotas |
Proxy-port errors are separate
The 401 / 403 responses here concern REST authentication or access checks. Proxy HTTP / CONNECT uses 407 for credentials, 400 for options, 402 for quota, 403 for account/policy, 429 for concurrency, 500 for internal errors, 502 for connection failure, 503 for unavailable service and 504 for connection timeout. Read X-NextProxy-Error for the cause. SOCKS5 uses its negotiation codes. See the error reference.