ProxioDocs
API Reference

Rate Limits

Proxio API rate limits are documented and per-key. 120 requests per minute by default, a per-key override, and 60 per minute for unauthenticated discovery. Every response carries X-RateLimit-* headers, and 429s include Retry-After.

Rate limits on the Proxio API are documented, per-key, and visible on every response. You never have to guess your budget, it's in the headers.

The numbers

ScopeLimitApplies to
Default per key120 requests / 60sEvery authenticated /v1 endpoint.
Per-key overrideCustomSet on an individual key to use less than the default. When set, it replaces the default for that key. Raising a key above the default is done by Proxio support, not self-service.
Discovery60 requests / 60s per IPThe unauthenticated discovery route (a GET on the base URL) and GET /openapi.json.

The budget is per API key, not per account. A read-only key powering a dashboard and a CI key running jobs have independent budgets, so one can't starve the other. Every request counts against the limit, not just failures.

The default of 120 per minute is roughly two requests per second sustained. You can lower a key's limit yourself in Settings → API keys, useful for a key you want to keep deliberately gentle. If a heavy integration needs more than the default, contact support to raise it, no code change is required on your side once it's set.

Endpoint-specific limits

A few write endpoints carry an extra throttle on top of the per-key budget:

OperationExtra limitScope
Whitelist additions (add a binding)30 / 60sper account
Session rotation (rotate one or all)30 / 60sper credential

Both return RATE_LIMITED (429) with a Retry-After header when exceeded, just like the per-key limit.

Headers

Once your key authenticates, every /v1 response carries the current limit state, success or error:

HeaderExampleMeaning
X-RateLimit-Limit120Your budget for the current window.
X-RateLimit-Remaining118Requests left in the current window.
X-RateLimit-Reset1752745860Unix seconds when the window fully resets.
Retry-After12On 429, and also on a 502 or 503. Seconds to wait before retrying.

Watch X-RateLimit-Remaining and slow down as it approaches zero, rather than waiting to be told no.

Auth failures carry no rate-limit headers

The limit is measured per API key, so it can only be evaluated after the key is identified. Authentication runs first, which means a response that fails at that stage has no X-RateLimit-* headers at all: UNAUTHENTICATED, INVALID_API_KEY, EXPIRED_API_KEY, REVOKED_API_KEY, ACCOUNT_SUSPENDED, and IP_NOT_ALLOWED. Read the headers defensively and fall back to your own backoff when they're absent. Everything past authentication, including 404s, validation errors, and 429s, does carry them.

A rejected request still costs budget, unless the rejection is the limit itself

A request is counted the moment the key is authenticated, before the scope check runs. So a call with a valid key but the wrong scope returns INSUFFICIENT_SCOPE (403) and still consumes one unit of the window. A loop retrying a scope error will exhaust the budget and start getting 429s instead. Fix the key's scopes rather than retrying.

The one rejection that does not cost anything is a 429 from this same limiter. A request the rate limiter itself turns away is given its unit back, so retrying a 429 never digs the hole deeper the way retrying a scope error does. This is also what makes honoring Retry-After reliable: the wait it tells you is solved to be the point your budget has genuinely recovered enough to admit the next request, so a client that backs off for exactly that long, sends nothing in between, and then retries once succeeds. It's not a rounded-down guess.

What a 429 looks like

When you exceed the limit, the API returns 429 with the standard error envelope and a Retry-After header:

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded for this API key.",
    "doc_url": "https://docs.proxio.net/docs/api/errors#rate_limited",
    "request_id": "req_8Ke2jP4mQ"
  }
}

RATE_LIMITED is retryable: honor Retry-After, then resend the same request.

Retry-After on 502 and 503

UPSTREAM_ERROR (502) and SERVICE_UNAVAILABLE (503) also carry a Retry-After header now, but just as retryable, and this is the same header telling you the same thing: wait this long, then resend. Unlike a 429, a 502 or 503 does spend a unit of your budget: only a request refused by the limiter itself is refunded, so a run of upstream failures still eats into what you can send. In practice that's a flat 2 seconds on a 502 and 5 seconds on a 503, a 502 is usually one bad upstream hop that clears almost immediately, a 503 means something took longer to recover. Read the header rather than hardcoding either number, a specific failure is free to send its own value and the header is what actually reaches you. The retry loop below, built for 429, works unmodified for these two: swap the status check and it's the same pattern.

Handling 429 in code

Respect Retry-After and back off. These helpers retry a GET a few times, sleeping for the server-provided delay:

# curl --retry treats 429 as retryable and honors Retry-After automatically.
curl --retry 5 --retry-all-errors --retry-delay 0 \
  "https://dashboard.proxio.net/api/v1/services?limit=20" \
  -H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
import time
import requests

BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}


def get_with_retry(path, params=None, max_attempts=5):
    for attempt in range(max_attempts):
        resp = requests.get(f"{BASE}{path}", headers=HEADERS, params=params, timeout=15)
        if resp.status_code != 429:
            resp.raise_for_status()
            return resp.json()
        # Honor Retry-After; fall back to exponential backoff if it's absent.
        wait = int(resp.headers.get("Retry-After", 2 ** attempt))
        time.sleep(wait)
    raise RuntimeError("Rate limited after retries")


print(get_with_retry("/services", {"limit": 20})["data"])
const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
const HEADERS = { Authorization: `Bearer ${API_KEY}` }

const sleep = (ms) => new Promise((r) => setTimeout(r, ms))

async function getWithRetry(path, maxAttempts = 5) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const res = await fetch(`${BASE}${path}`, { headers: HEADERS })
    if (res.status !== 429) {
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      return res.json()
    }
    // Honor Retry-After; fall back to exponential backoff if it's absent.
    const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt)
    await sleep(wait * 1000)
  }
  throw new Error("Rate limited after retries")
}

const { data } = await getWithRetry("/services?limit=20")
console.log(data)

Add jitter for fleets

If many workers share one key, add a small random jitter on top of Retry-After so they don't all retry on the same tick. Spreading heavy automation across multiple keys also multiplies your total budget, since limits are per key.

On this page