Skip to content
NextProxyNextProxyDocs

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 tokenRefresh token
Prefixat_rt_
LengthPrefix plus 43 random charactersPrefix plus 43 random characters
TransportAuthorization: Bearer at_...HttpOnly cookie
Default lifetime15 minutes12 hours
With "remember me"Unchanged30 days

Logging in

POST/api/v1/auth/login
shell
curl -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:

POST/api/v1/auth/login/two-factor
shell
curl -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:

POST/api/v1/auth/refresh
shell
curl -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:

json
{ "data": { } }

Failure:

json
{
  "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:

ParameterDefaultMaximum
page1none
pageSize20100

Response:

json
{
  "data": {
    "items": [],
    "page": 1,
    "pageSize": 20,
    "totalItems": 137,
    "totalPages": 7
  }
}

Division of labour between API keys and access tokens

Access tokenAPI key
ScopeAll /api/v1 client endpointsOnly the Extract API
Belongs toA site userOne proxy sub-account (one key each)
TransportAuthorization: BearerQuery parameter ?apikey=
Lifetime15 minutesLong-lived, until revoked
Extra requirementOrigin on writesSource IP must be whitelisted

They are not interchangeable.

A usable client wrapper

python
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

POST/api/v1/auth/logout
POST/api/v1/auth/logout-all

logout invalidates only the current token pair; logout-all invalidates every session for that user. Changing the password revokes old sessions automatically.

Common problems

SymptomCause
403 origin_rejectedA write without Origin, or a value not on the trusted list (exact match)
401 though the token looks rightThe access token expired (15 minutes only)
Refresh failsThe refresh cookie wasn't sent (-b cookies.txt) or has expired
Login returns mfaTicket instead of a tokenTwo-factor is enabled; complete the second step
MFA ticket invalidIt lasts 5 minutes; restart the login flow
429Risk-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.

Did this page solve your problem?