Docs
Proxy setup and API documentation.
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.
Jump to an example
Dedicated ports (one mobile device per customer) have their own page: setup, rotation and renewal.
Setting up an eSIM or SMS service? Follow the eSIM installation checklist or compare SMS service types. A data eSIM and a web-managed SMS number have different capabilities.
SMS & eSIM Reseller API — catalogue, wallet purchases and delivery →
On this page
On this page
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-es-rot-ondemand # residential, Spain, 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-<name>-rot-auto5 # prefer one carrier in Poland (a preference; we publish no carrier list yet)Username 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. Two names to keep straight: the API calls the pools mobile and residential, while the proxy username calls them mbl and peer. Compare mobile 4G/5G proxies and residential proxies, then check live country availability for your run. For your client, check the HTTP CONNECT requirements or SOCKS5 authentication and setup requirements before ordering.
| 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. We publish no list of carrier names yet, so leave it out unless you know the name. |
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-es-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 Requests
import os, requests
API = "https://api.portproof.org/v1"
H = {"Authorization": f"Bearer {os.environ['PORTPROOF_API_KEY']}"}
# balance, expiry and credentials (expires_at is null while GB never expire)
t = requests.get(f"{API}/traffic", headers=H, timeout=10).json()
c = t["credentials"]
print(t["gb_left"], "GB left; expires", t["expires_at"] or "never")
# 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.js fetch
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: 'residential', country: 'ES', 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();Find your client
Use the guide for the library that sends your request. Proxy arguments, authentication and timeout behaviour differ between clients. Filling in HTTPS_PROXY or a field labelled HTTPS proxy? Read why an HTTPS proxy setting takes an http:// address.
- Docker proxy settings
Distinguish daemon image pulls, build steps and requests in running containers.
- Proxy URL format
Original credential fields, component encoding and client-specific inputs.
- Plan parallel proxy work
Measured task duration, request pacing and account limits.
- Python pip
An isolated package-index check without installing packages or changing saved settings.
- npm CLI
An isolated registry check without installing packages or changing saved settings.
- PHP cURL
Authenticated CONNECT and separate gateway and destination errors.
- Python urllib
Standard-library requests, routing exceptions and hidden input.
- Python HTTPX
Synchronous and asynchronous clients.
- Python aiohttp
Async sessions, deadlines and cleanup.
- Scrapy
Request routing and crawl limits.
- Axios in Node.js
An authenticated HTTPS CONNECT agent.
- Go net/http
Standard-library transport settings.
- Postman desktop
Proxy settings and a small API check.
- PowerShell
A request-scoped gateway and prompted credentials.
- Firefox desktop
Native HTTP proxy settings and restoration steps.
- GNU Wget
Authenticated downloads with explicit limits.
- Chrome with FoxyProxy
An HTTP proxy for chosen sites, signed in by the extension.
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
- Keys start with
pk_and are created per project in the dashboard. Shown once. Send asAuthorization: Bearer. There is no sandbox: every key acts on your real balance, whatever its prefix. - Scopes
- A key holds the scopes chosen when it was created. Reading your balance and limits (
GET /traffic) needsports:readorports:write; the proxy password comes back only withports:write.ports:writealso protects generated credentials (build-urlandbuild-urls) and actions that spend or change something:test,regenerate,buy, checkout, wallet top-up, cancelling an order and cart writes.billing:readreads the account (/me), wallet, orders, invoices, affiliate figures and the cart. Account details, password, sessions and payouts are only changed from a signed-in dashboard session, never with a key. A key without the scope gets403 insufficient_scope. - 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": null, "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":"mobile","country":"US","rotation":"sticky","session":"cart7","protocol":"http"}'
# test a connection from our side: whether it works, the exit IP and the latency (6 per minute)
# it does not confirm the exit country: look the IP up in the geolocation database your target uses
curl -X POST https://api.portproof.org/v1/traffic/test -H "Authorization: Bearer $PORTPROOF_API_KEY" -H "Content-Type: application/json" \
-d '{"pool":"mobile","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)"
# buy whole GB from your account balance (key scope ports:write). accept_terms: true is required, as at checkout:
# it records that you accept the terms and the acceptable-use policy and ask for the service to start at once.
# The same Idempotency-Key with the same body replays the same order; a refused purchase stays under its key for 24 h,
# so after fixing the cause (for example a top-up) send a new key.
curl -X POST https://api.portproof.org/v1/traffic/buy -H "Authorization: Bearer $PORTPROOF_API_KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" -d '{"gb":25,"accept_terms":true}'
# prices: the per-GB ladder
curl https://api.portproof.org/v1/pricingGenerate connections in bulk
Use POST /v1/traffic/build-urls for 1–100 sticky connections. It validates the same pool, country and protocol as the single builder. A ports:write key is required because the response includes your proxy password.
curl -sS -X POST https://api.portproof.org/v1/traffic/build-urls \
-H "Authorization: Bearer $PORTPROOF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"pool":"residential","country":"any","protocol":"http","count":10,"session_prefix":"research"}'The response has a connections array with a username, URL, masked URL and code examples for each session. Names run from research_1 to research_10; repeat that prefix to reuse those sessions. Prefixes accept 1–48 lower-case letters, digits or underscores.
Generating URLs makes no purchase or gateway connection and consumes no GB. The connections share your existing balance and gateway ports. Distinct names do not guarantee distinct devices or IPs. Keep responses out of logs and shared files.
For an integration: read GET /traffic for balance, check GET /traffic/countries, build the connections, then configure your workers. Both builders use the read rate budget and need no idempotency header. Wallet purchases use POST /traffic/buy with a stable Idempotency-Key and explicit terms acceptance.
Webhooks
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) {
if (typeof header !== 'string') return false;
const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300 || typeof parts.v1 !== 'string') return false;
const expected = Buffer.from(createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex'));
const received = Buffer.from(parts.v1);
return received.length === expected.length && timingSafeEqual(received, expected);
}
// traffic events: usage.80pct, usage.cap_reached (once per purchase cycle, also sent by email)MCP
Connection passwords are masked by default. A URL build does not make a proxy request; test_connection uses some existing traffic. Wallet purchases need the user's confirmation and terms acceptance.
// stdio MCP server. buy_traffic needs confirm:true, accept_terms:true and idempotency_key.
// Ask the user to accept the terms, acceptable-use policy and immediate start of service first.
{
"mcpServers": {
"portproof": {
"command": "npx",
"args": ["-y", "@portproof/mcp"],
"env": { "PORTPROOF_API_KEY": "pk_...", "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.
Fix a 407 Proxy Authentication Required response, troubleshoot CONNECT tunnel failures or diagnose a refused or timed-out connection.
| 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 | insufficient_funds | API: the account balance does not cover a purchase from the API |
| 403 | insufficient_scope | API: the key lacks the scope (see Scopes above) |
| 403 | account_suspended | API: the account, or proxy traffic on it, is paused; no more GB can be bought until it is resumed. Write to support |
| 409 | idempotency_conflict | API: the same Idempotency-Key with a different body inside 24 h, or the same key sent twice at once (checkout and /traffic/buy) |
| 409 | trial_already_used | API: the trial can be bought once per customer |
| 409 | traffic_not_purchased | API: no traffic balance yet; buy GB first |
| 429 | rate_limited | API: read 600 / 5 min, write 120 / 5 min per key; POST /traffic/test 6 per minute |