Docs
One URL. The username does the targeting.
Connection format first, then the username tokens, snippets for curl, Python, Node and Playwright, and the API. Pool, country, rotation and session are all chosen in the proxy username, so one credential serves every request.
Connection format
The gateway takes HTTP and SOCKS5 on two ports with the same credentials. USERNAME stands for the account part of your username; it and the password appear in the dashboard right after your first purchase. The dashboard's connection builder writes these usernames for you and tests them.
# HTTP (CONNECT) on port 7000, SOCKS5 on port 7001; same username and password
http://<username>:<password>@gw.portproof.org:7000
socks5h://<username>:<password>@gw.portproof.org:7001
# <username> = USERNAME-<pool>-<country>[-sid-<name>][-carrier-<name>][-rot-<mode>][-ttl-<seconds>]
# lower case, hyphen separated, no hyphen inside a value
# USERNAME and the password (pak_••••) are in the dashboard under Proxies, and in GET /v1/traffic
USERNAME-mbl-us-rot-auto10 # mobile 4G/5G, United States, new exit every 10 minutes
USERNAME-peer-de-rot-ondemand # residential, Germany, new exit on every connection
USERNAME-any-fr-rot-auto60 # whichever pool answers in France, hourly
USERNAME-mbl-gb-sid-cart7-rot-sticky # sticky session "cart7": the same device while it is online
USERNAME-mbl-pl-carrier-play-rot-auto5 # prefer one carrier in PolandUsername tokens
Pool and country are positional; every other token is a name followed by its value. Values are lower case and never contain a hyphen.
| Token | Values | Meaning |
|---|---|---|
| pool | mbl · peer · any | Mobile 4G/5G, Residential, or whichever is available. Second position in the username. |
| country | us · gb · fr · … | Two-letter country code, lower case. Third position. Mobile 4G/5G serves us, gb, fr, nl, pl and ge (Georgia, the country). |
| rot | ondemand · auto5 · auto10 · auto20 · auto60 · sticky | When the exit changes: every connection, on a timer in minutes, or held for a session. Default auto10. |
| sid | [a-z0-9_] up to 64 characters | Session name. Required with rot-sticky: the same sid returns to the same device while it is online. |
| ttl | 60 to 2592000 (seconds) | How long a sticky session is kept. Set once per sid; it cannot be changed afterwards. |
| carrier | lower-case name without spaces or hyphens | Prefer one mobile carrier. A soft filter: when nothing matches, the gateway widens to the country. |
Rotation and sessions
- New IP each connection
- rot-ondemand
- Every 5 minutes
- rot-auto5
- Every 10 minutes
- rot-auto10
- Every 20 minutes
- rot-auto20
- Every 60 minutes
- rot-auto60
- Sticky session
- rot-sticky
- A sticky session needs both
sid-<name>androt-sticky. Use one session name per logical visitor; letters, digits and underscore. - A sticky session keeps the same device. The carrier may still change the IP of that device. We do not sell fixed IP addresses on this product.
- A country with no device online answers with a gateway error (502). A carrier is a preference: when nothing matches, the gateway widens to the country.
- Traffic is counted in both directions at the gateway; the balance in the dashboard and the API refreshes every five minutes.
curl
Keep the password in an environment variable; never paste it into shared scripts.
# your exit address, as the destination sees it
curl -x "http://USERNAME-mbl-us-rot-auto10:$PROXY_PASSWORD@gw.portproof.org:7000" https://api.portproof.org/v1/echo-ip
# SOCKS5 with remote DNS
curl -x "socks5h://USERNAME-peer-de-rot-ondemand:$PROXY_PASSWORD@gw.portproof.org:7001" https://api.portproof.org/v1/echo-ip
# a sticky session: both requests leave through the same device
curl -x "http://USERNAME-mbl-gb-sid-cart7-rot-sticky:$PROXY_PASSWORD@gw.portproof.org:7000" https://api.portproof.org/v1/echo-ip
curl -x "http://USERNAME-mbl-gb-sid-cart7-rot-sticky:$PROXY_PASSWORD@gw.portproof.org:7000" https://api.portproof.org/v1/echo-ipPython
import os, requests
API = "https://api.portproof.org/v1"
H = {"Authorization": f"Bearer {os.environ['PORTPROOF_API_KEY']}"}
# balance, expiry and credentials
t = requests.get(f"{API}/traffic", headers=H, timeout=10).json()
c = t["credentials"]
print(t["gb_left"], "GB left, expires", t["expires_at"])
# build the username yourself, or let the API validate it for you
username = f"{c['username_base']}-mbl-us-rot-auto10"
proxy = f"http://{username}:{c['password']}@{c['host']}:{c['http_port']}"
r = requests.get("https://api.portproof.org/v1/echo-ip", proxies={"http": proxy, "https": proxy}, timeout=30)
print("exit:", r.json())Node
import { ProxyAgent, fetch as ufetch } from 'undici';
const API = 'https://api.portproof.org/v1';
const H = { authorization: `Bearer ${process.env.PORTPROOF_API_KEY}`, 'content-type': 'application/json' };
// the API builds and validates the URL: pool, country, rotation, optional session, protocol
const built = await (await fetch(`${API}/traffic/build-url`, {
method: 'POST',
headers: H,
body: JSON.stringify({ pool: 'peer', country: 'DE', rotation: 'ondemand', protocol: 'http' }),
})).json();
// undici's ProxyAgent speaks HTTP CONNECT
const agent = new ProxyAgent(built.url);
console.log(await (await ufetch('https://api.portproof.org/v1/echo-ip', { dispatcher: agent })).json());Playwright
One browser context per sticky session keeps cookies and exit device together.
import { chromium } from 'playwright';
// one sticky session per browser context: the same device for the whole visit
const session = `run${Date.now().toString(36)}`;
const browser = await chromium.launch();
const context = await browser.newContext({
proxy: {
server: 'http://gw.portproof.org:7000',
username: `USERNAME-mbl-us-sid-${session}-rot-sticky`,
password: process.env.PROXY_PASSWORD,
},
locale: 'en-US',
timezoneId: 'America/New_York',
});
const page = await context.newPage();
await page.goto('https://api.portproof.org/v1/echo-ip');
console.log(await page.textContent('body'));
await browser.close();Other tools
Anything that accepts an HTTP or SOCKS5 proxy with a username and password works. Neutral technical configs only; see the acceptable-use policy.
# Cloud browsers and scrapers take the same three values: server, username, password
{ "proxy": { "server": "http://gw.portproof.org:7000",
"username": "USERNAME-peer-us-rot-ondemand",
"password": "pak_••••" } }
# Scrapy settings.py
HTTPPROXY_ENABLED = True
# then per request: meta={"proxy": "http://USERNAME-peer-us-rot-ondemand:PASSWORD@gw.portproof.org:7000"}API and authentication
- Production API
- https://api.portproof.org/v1
- This deployment
- https://api.portproof.org/v1 · OpenAPI at https://api.portproof.org/docs
- Keys
pk_live_…andpk_test_…, scoped, created per project in the dashboard. Shown once. Send asAuthorization: Bearer.- Conventions
- Idempotency-Key on every write, cursor pagination, RFC 9457 problem details, per-key rate limits in response headers.
- Gateway
- gw.portproof.org · 7000 http · 7001 socks5
Traffic API
Everything the dashboard shows is an API call. Buying from the API uses your account balance; every other payment method runs through checkout.
# balance, expiry, credentials and limits
curl https://api.portproof.org/v1/traffic -H "Authorization: Bearer $PORTPROOF_API_KEY"
# -> { "gb_total": 25, "gb_used": 3.2, "gb_left": 21.8, "expires_at": "...", "enabled": true,
# "credentials": { "host": "gw.portproof.org", "http_port": 7000, "socks_port": 7001, "username_base": "...", "password": "pak_••••" },
# "limits": { ... } }
# countries with devices online now, per pool (cached 60 s)
curl https://api.portproof.org/v1/traffic/countries -H "Authorization: Bearer $PORTPROOF_API_KEY"
# build and validate a proxy URL with snippets
curl -X POST https://api.portproof.org/v1/traffic/build-url -H "Authorization: Bearer $PORTPROOF_API_KEY" -H "Content-Type: application/json" \
-d '{"pool":"mbl","country":"US","rotation":"sticky","session":"cart7","protocol":"http"}'
# test a connection from our side: exit country and latency (6 per minute)
curl -X POST https://api.portproof.org/v1/traffic/test -H "Authorization: Bearer $PORTPROOF_API_KEY" -H "Content-Type: application/json" \
-d '{"pool":"mbl","country":"US","rotation":"auto10"}'
# new password (the old one keeps working for about 30 seconds)
curl -X POST https://api.portproof.org/v1/traffic/regenerate -H "Authorization: Bearer $PORTPROOF_API_KEY" -H "Idempotency-Key: $(uuidgen)"
# prices: the per-GB ladder
curl https://api.portproof.org/v1/pricingWebhooks
Create webhooks in the dashboard or with POST /v1/webhooks; the secret is shown once. Deliveries retry with exponential backoff for 24 hours.
// verify Portproof-Signature: t=<unix>,v1=<hex hmac-sha256(secret, t + "." + body)>
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
return Math.abs(Date.now() / 1000 - Number(parts.t)) < 300 &&
timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}
// traffic events: usage.80pct, usage.cap_reached (once per purchase cycle, also sent by email)MCP
// stdio MCP server: the same routes as tools; buy_traffic needs confirm:true + idempotency_key
{
"mcpServers": {
"portproof": {
"command": "npx",
"args": ["-y", "@portproof/mcp"],
"env": { "PORTPROOF_API_KEY": "pk_live_...", "PORTPROOF_API_URL": "https://api.portproof.org/v1" }
}
}
}
// tools: get_pricing, get_traffic, list_countries, build_proxy_url, test_connection, buy_traffic (account balance), get_statusErrors and limits
The first block comes from the proxy gateway on your connection; the second from the API.
| Status | code | Meaning |
|---|---|---|
| 407 | E_AUTH_INVALID | gateway: wrong username base or password (regenerated passwords replace the old one after about 30 seconds) |
| 407 | E_CAP_EXCEEDED | gateway: the GB balance is used up or expired; buy more to continue |
| 400 | E_USERNAME_PARSE | gateway: the username is malformed (upper case, a hyphen inside a value, unknown token) |
| 429 | E_RATE_LIMITED_CONN | gateway: too many parallel connections; back off and retry |
| 429 | E_SESSION_LIMIT | gateway: too many sticky sessions open at once |
| 502 | E_NO_STOCK_COUNTRY | gateway: no device online in that pool and country right now |
| 400 | validation_failed | API: body or query failed validation; errors[] lists paths |
| 400 | idempotency_key_required | API: write without Idempotency-Key |
| 402 | kyb_required | API: the order would pass the monthly volume that needs a business check |
| 403 | insufficient_scope | API: the key lacks the scope |
| 409 | idempotency_conflict | API: same key, different body inside 24 h |
| 429 | rate_limited | API: read 600 / 5 min, write 120 / 5 min per key; POST /traffic/test 6 per minute |