Error codes
Every status the gateway returns, and what to do about each.
Errors raised by the gateway carry an x-wproxy-error header with a stable machine-readable code. Errors from the target site pass through untouched, so a 403 with no wproxy header came from the target, not from us.
Gateway errors
| Status | Code | Meaning | Action |
|---|---|---|---|
| 400 | malformed_username | A targeting flag could not be parsed | Check for stray hyphens inside flag values |
| 400 | city_without_country | -city- used with no -country- | Add the country flag |
| 402 | insufficient_balance | Balance or plan allowance exhausted | Top up, or raise the spend limit |
| 403 | flag_not_permitted | Sub-user is restricted from that country or product | Widen the sub-user's allow list |
| 407 | bad_credentials | Username or password rejected | Verify the pair; check the sub-user is enabled |
| 429 | concurrency_exceeded | Too many simultaneous connections | Lower concurrency or raise the ceiling |
| 502 | exit_failed | The exit dropped mid-request | Retry; this is credited automatically |
| 503 | no_exit_available | No exit matched with -strict set | Loosen targeting or drop -strict |
| 504 | exit_timeout | The target did not respond in time | Raise your timeout or retry |
5xx from us is free
Any request that fails inside our infrastructure — 502, 503, 504 with an x-wproxy-error header — is credited back automatically and appears as an adjustment line on the invoice. You never pay for our failures.
A retry policy that behaves
import time, requests
RETRYABLE = {429, 502, 503, 504}
def get(url, proxies, attempts=4):
for i in range(attempts):
try:
r = requests.get(url, proxies=proxies, timeout=30)
if r.status_code not in RETRYABLE:
return r
except requests.RequestException:
pass
time.sleep(min(2 ** i, 8)) # 1s, 2s, 4s, 8s
raise RuntimeError(f"gave up on {url}")Do not retry 402, 403 or 407
They are configuration problems, not transient faults. Retrying them wastes time and, in the case of 407, can trip our own brute-force protection.