Skip to main content

Error envelope

Every error response uses the same JSON shape:
{
  "error": {
    "code": "rate_limited",
    "message": "Sustained rate limit exceeded — retry after 12 seconds",
    "details": {
      "retry_after_seconds": 12,
      "tier": "pro",
      "limit_rpm_60s": 3000
    }
  }
}
code is a stable machine-readable identifier; message is human-readable and may change. Always switch on code, not message.

Status codes

StatusCodeMeaningRetry?
400bad_requestMalformed request body or invalid IP formatNo — fix the request
401unauthorizedAPI key / gateway auth missing or invalidNo — fix credentials
403forbiddenKey tier does not permit this endpoint or fieldNo — upgrade tier
404not_foundResource doesn’t exist (rare)No
422validation_errorPydantic validation failedNo — fix payload
429rate_limitedToo many requestsYes — see headers
500internal_errorUnexpected server errorYes — exponential backoff
503unhealthyService degradedYes — exponential backoff
On the IP endpoints, a 400 bad_request carries a more specific code in the envelope — one of invalid_ip_reserved (RFC-reserved / non-global address), invalid_ip_integer_form (bare integer not accepted), or invalid_ip_format (unparseable). Switch on these subcodes when you need to distinguish why an IP was rejected.

Rate-limit headers

Every response carries the current rate-limit state:
X-RateLimit-Limit: 3000
X-RateLimit-Remaining: 2997
X-RateLimit-Window: 60
A 429 response adds two headers telling you when to retry:
Retry-After: 12                 # seconds to wait
X-RateLimit-Reset: 1714339200   # Unix timestamp when the next slot frees
X-RateLimit-Limit is your request ceiling, expressed as requests per 60-second window (not per second). X-RateLimit-Remaining is how many requests remain in the current window. X-RateLimit-Window is the window length in seconds. On a 429, Retry-After is the recommended wait in seconds and X-RateLimit-Reset is the Unix timestamp when the next slot frees (≈ now + Retry-After). Because this is a sliding-window limiter, the exact reset instant is only defined at denial time, so X-RateLimit-Reset appears on 429 responses only — prefer Retry-After for backoff.

Retry strategy

For transient errors (429, 500, 503):
import httpx, time, random

def lookup_with_retry(ip, key, max_attempts=5):
    for attempt in range(max_attempts):
        resp = httpx.get(
            f"https://wxaintel.wxapros.com/api/v1/vpn/ip/{ip}",
            headers={"X-API-Key": key},
            timeout=10,
        )
        if resp.status_code == 200:
            return resp.json()
        if resp.status_code == 429:
            wait = int(resp.headers.get("Retry-After", 1))
            time.sleep(wait)
            continue
        if resp.status_code in (500, 503):
            backoff = 2 ** attempt + random.random()
            time.sleep(backoff)
            continue
        # 4xx other than 429 → don't retry
        resp.raise_for_status()
    raise RuntimeError(f"Exhausted {max_attempts} retries for {ip}")
Don’t retry 4xx errors other than 429. They indicate a problem with your request, and retrying just wastes quota.

Circuit breaking

For high-volume integrations, wrap calls in a circuit breaker so a brief outage doesn’t cascade into your application:
from circuitbreaker import circuit

@circuit(failure_threshold=5, recovery_timeout=30)
def lookup(ip, key):
    # ...
We aim for 99.5 % (Pro) / 99.9 % (Business) / 99.95 % (Enterprise) availability, but caller-side circuit breakers are still recommended.