nullroute Docs
On this page

NullRoute docs

Proxy gateway and tools API

One endpoint for residential, datacenter and mobile exits. The username picks the exit, the password is your API key.

Host
gw.nullroute.tools
Port
32424
Protocols
HTTP, HTTPS, SOCKS5, SOCKS4
Login
res-us : nr-… or sk-…

Port 32443 is the same gateway inside TLS. Always connect by the gateway hostname: DNS keeps the endpoint stable when servers change, and TLS verifies that hostname.

Start

Quickstart

  1. Get your API key. Create an account, then copy the master key (nr-…) from Dashboard → Settings. Keep it in an environment variable, not in source code.

    shell
    export NULLROUTE_KEY="nr-your-key"
  2. Build the proxy URL. Username = the exit you want, password = the key.

    format
    http://<username>:<api-key>@gw.nullroute.tools:32424
  3. Send a request through a residential US exit and check the IP it left from.

    curl
    curl -x "http://res-us:[email protected]:32424" \
      "https://api.ipify.org?format=json"
    
    # {"ip":"203.0.113.24"}

Proxy

Endpoint and login

Every protocol uses the same host and port; the gateway detects the protocol from the first byte. Port 32443 carries the same four protocols inside TLS.

ProtocolProxy URLNotes
HTTP / HTTPShttp://res-us:[email protected]:32424HTTPS sites go through CONNECT, so TLS stays end-to-end. The proxy URL itself is always http://.
SOCKS5socks5h://res-us:[email protected]:32424Username/password auth only. socks5h resolves DNS on the exit. TCP only, no UDP.
SOCKS4 / 4asocks4a://res-us%[email protected]:32424
socks4a://[email protected]:32424
SOCKS4 has no password field, so the key must travel inside the user id: percent-encode the colon (res-us%3AKEY) or send the key alone. The plain res-us:KEY@ form does not work in curl — it splits userinfo at the colon and the gateway sees only res-us.

Encrypted connection (TLS)

Port 32443 speaks HTTP, HTTPS CONNECT, SOCKS4/4a and SOCKS5 inside TLS. The login is the same; only the proxy scheme and port change. Connect by hostname, not by IP: the certificate is issued for gw.nullroute.tools, so a client that dials the raw address fails certificate validation.

curl
# https:// scheme = TLS to the gateway first, then the normal request
curl --proxy https://gw.nullroute.tools:32443 --proxy-user "res-us:$NULLROUTE_KEY" https://api.ipify.org

Credentials

UsernamePasswordResult
res-usnr-… or sk-…Standard form. Username selects the exit.
nr-… or sk-…emptyRotating residential exit in a random country — the same as res-any, not US. For tools that only have one login field.
res-usanythingOnly from an allowlisted IP. No key needed.

nr- is your account's master key. sk- keys are sub-keys with their own limits; give those to teammates, scripts and clients.

Proxy

Targeting

The username is <type>-<country>, then any of the verified filters: -st-, -city-, -zip-, -asn-, -os-. A sticky session (-sid-) replaces the filters.

ParameterFormatExampleNotes
Exit typeres · dc · mobdc-usResidential $0.60/GB, datacenter $0.20/GB, mobile $1.00/GB.
Country<type>-<cc>res-deISO 3166 two-letter code, lowercase. any picks a random country.
State<type>-<cc>-st-<state>res-us-st-texas or res-us-st-txUS: full name or two-letter code, lowercase. Other countries: the region code or name from /api/geo/availability.
City<type>-<cc>-city-<city>res-us-city-new_yorkCity in lowercase, punctuation and spaces become underscores (st_louis). Matched on the exit's geolocation, which is metro-wide: expect the city or its suburbs, not a street-level address. In the US a bare name means its biggest namesake (houston is Houston, TX); add the state for another one: res-us-st-pa-city-houston.
ZIP<type>-<cc>-zip-<zip>res-us-zip-100015 digits, US only. Matched by distance: the exit is geolocated within 25 km of the ZIP's centre, so it may sit in a neighbouring ZIP. Confirm with /api/exit-ip if the exact ZIP matters.
ASN<type>-<cc>-asn-<n>res-us-asn-7922One network only, e.g. AS7922 (Comcast). Digits, no AS prefix.
OS<type>-<cc>-os-<os>res-us-os-windowswindows, android, linux or macos.
Combinedfilters joined in one usernameres-us-st-texas-os-windowsEvery filter must match the same exit.
Session<type>-<cc>-sid-<id>res-us-sid-4821Same id keeps the same IP. Letters and digits. Can't be combined with the filters above. See sessions.
Smart retrysr_ prefixsr_res-usRetries blocked plain-HTTP responses. See smart retry.
examples
res-us                        # residential, United States, new IP per connection
dc-de                         # datacenter, Germany
mob-gb                        # mobile carrier IP, United Kingdom
res-any                       # residential, random country
res-us-city-chicago           # residential, Chicago
res-us-st-texas-os-windows    # residential, Texas, Windows exits
res-us-zip-10001              # residential, US ZIP 10001
res-us-asn-7922               # residential, AS7922 only
res-us-os-android             # residential, Android exits
res-us-sid-4821               # sticky: same IP while you reuse sid 4821
sr_res-us                     # smart retry on blocked responses
  • Can I combine filters? Yes: res-us-st-texas-os-windows, res-us-city-new_york-asn-7922. Every filter in the username must match the same exit.
  • What if nothing matches? The gateway verifies the filter against the exit before it hands traffic over. With no matching exit the connection gets 502 no exit matching filter and nothing is charged.
  • Which locations have exits right now? Ask /api/geo/availability: countries, states and cities where exits answered in the last 24 hours, with counts and a ready login for each. It never lists a place we haven't seen an exit in.
  • Filters and sticky sessions? Not together. -sid- pins one exit, so a username can have either filters or a session id.
  • Datacenter fallback. If the datacenter network has no exit in that country right now, a plain dc-<cc> or sticky dc-<cc>-sid-<id> login is served by a residential exit of the same country at the datacenter price. A sticky session stays on the network that gave it its first IP, even after datacenter comes back. Logins with filters (-st-, -city-, -zip-, -asn-, -os-) are never swapped.

In the tools API, where city, ASN and session are separate fields, precedence is city > ASN > session: if you send several, only the first one applies.

Proxy

Sessions

ModeUsernameBehaviour
Rotatingres-usEvery new connection gets a new exit IP. Requests on one keep-alive connection or tunnel keep its IP.
Stickyres-us-sid-4821Every connection with the same id uses the same exit IP for as long as that exit stays online.
  • New IP: change the id, e.g. res-us-sid-4822. Use random ids (8+ digits) so parallel workers don't share an exit by accident.
  • Self-healing: if a sticky exit dies (two failed connects in a row, or a tunnel that drops before any data), NullRoute moves the session to a new IP. The id keeps that new IP afterwards, so your session stays stable instead of failing.
  • Private ids: ids are scoped to your account. Your sid-1 and another customer's sid-1 are different exits.
  • Idle timeout: a connection with no traffic in either direction for 3 minutes is closed. A single connection lives at most 24 hours. Reconnecting with the same id gets the same IP.
  • Header instead of username: HTTP clients can send X-Session-Id: 4821. With curl and HTTPS sites use --proxy-header "X-Session-Id: 4821".

Proxy

Code examples

Replace nr-your-key or $NULLROUTE_KEY with your key. Each tab is copy-paste ready.

# HTTP proxy
curl -x "http://res-us:[email protected]:32424" https://api.ipify.org

# SOCKS5, DNS resolved on the exit
curl -x "socks5h://res-us:[email protected]:32424" https://api.ipify.org

# Sticky session in Germany
curl -x "http://res-de-sid-4821:[email protected]:32424" https://api.ipify.org

For a browser profile use a sticky username (-sid-), one id per profile, so the IP doesn't change between page loads.

Proxy

Smart retry

Prefix the username with sr_. When a response looks blocked, NullRoute sends the request again from a fresh exit in the same country, up to 3 attempts, and returns the first clean response.

  • Counts as blocked: Cloudflare challenge (cf-mitigated: challenge, or 503 from Cloudflare), DataDome block, captcha or "access denied" pages, HTTP 429.
  • Costs $0.001 per request that needed a retry and then succeeded. First-try successes cost bandwidth only.
  • Works for plain http:// requests through the HTTP proxy. HTTPS tunnels and SOCKS are end-to-end encrypted, so the gateway can't read the response. For HTTPS targets, use POST /api/fetch, where retry is on by default.
curl
curl -x "http://sr_res-us:[email protected]:32424" http://example.com/

Proxy

Keys and limits

Sub-keys

Create sk-… keys in Dashboard → API keys and use them anywhere the master key works: as the proxy password or as the API bearer token. Disable or delete one without touching the master key. Each sub-key can have:

LimitWhat happens
Expiry dateStops working at the end of that day, UTC.
Traffic cap (GB)New connections are refused once the key has used its GB.
Spend cap ($)Same, by money spent instead of traffic.
Exit types and countriesUsernames outside the list are refused, e.g. mob-gb on a key limited to res.
Speed cap (Mbps)Shared by all connections of the key. Set on keys issued by NullRoute.

Limits are checked when a connection opens; a connection already running finishes. The account's daily spend limit (in API keys) applies to every key.

IP allowlist

Connections from an allowlisted IP or CIDR range don't need a key. Send only the exit as the username:

curl
# from an allowlisted server
curl -x http://[email protected]:32424 https://api.ipify.org

IPv4 or IPv6, single address or CIDR, up to 50 entries. SOCKS5 still needs a username and a password field: send res-us and any non-key password. Allowlisted traffic bills the master balance.

Proxy

Troubleshooting

You seeCauseWhat to do
407 Proxy Authentication Required (HTTP), or the connection closes right after login (SOCKS)Key missing, mistyped, disabled or expired; sub-key out of traffic or spend; no balance; daily spend limit hit; exit type or country not allowed for the sub-key. Over HTTP the reason is in the response body and the X-Proxy-Error header (invalid_credentials, insufficient_balance, quota_exceeded, key_expired, exit_not_allowed…).Check the key in API keys, top up in Billing, or raise the cap. The key goes in the password, not the username.
400 unknown exit type / unknown country codeThe username doesn't start with res, dc or mob, or the country isn't an ISO code.Use e.g. res-us, dc-de, mob-gb or res-any.
502 Bad Gateway, or SOCKS "general failure"No exit could reach the site after several attempts: site down, port closed, or no IPs left for that city / ASN.Retry. If it repeats, widen targeting (drop the filter, try another country).
502 no exit matching filterA verified filter (-st-, -city-, -zip-, -asn-, -os-) has no matching exit right now.For a city or state login the reply carries X-Proxy-Suggest: a ready login for the nearest city (or state, or country) with exits in the last 2 hours, e.g. res-us-st-tx-city-austin. Use it, widen the filter, or retry later. The request is not charged.
502 no exit available for this location right now (X-Proxy-Error: no_exit_for_location), or an HTTPS tunnel that closes right after the handshakeThe network has no exits in that country at the moment. Small territories are often not covered. A single refusal is retried through another exit; after 3 refusals within a minute the gateway answers this for 5 minutes without trying again.Pick a nearby country or another network type. The request is not charged.
curl: (97) Can't complete SOCKS4 connection (reply 91)The SOCKS4 user id was cut at the colon: socks4a://res-us:KEY@… and --proxy-user "res-us:KEY" both send only res-us, because SOCKS4 has no password field.Percent-encode the colon in the URL (socks4a://res-us%3AKEY@…) or send the key alone (socks4a://KEY@…).
Requests go to the US although you wanted another countryThe username has no country: an empty username (http://:KEY@…, http://KEY:@…), the key alone, or a bare res / dc / mob defaults to <type>-us.Put the exit first: http://res-de:KEY@…, or res-any for a random country. An empty password with a real username (res-us:) is rejected with 407.
Username with -sid- and a filter is refusedSticky sessions and verified filters can't be combined in one username. The gateway answers 400 with the body sid cannot be combined with geo filters.Send either the filters or -sid-<id>.
403 Forbidden, or SOCKS "not allowed"The destination host or port is blocked by network policy (e.g. SMTP).Not available through NullRoute.
IP changes on every requestRotating username, or your client opens a new connection per request.Add -sid-<id> to the username.
Sticky IP changedThe exit went offline and the session was moved to a new IP.Expected. Check the current IP with /api/exit-ip if you need it.
Connection drops after a pause3-minute idle timeout (or the 24-hour connection limit).Reconnect; same sid means same IP.
Your own IP shows on Windowscurl in PowerShell is Invoke-WebRequest, or -x was left out.Use curl.exe -x ….

Tools API

Authentication

The tools API runs checks through NullRoute exits and bills your balance per call. Send your key as a bearer token. Master keys (nr-…) and sub-keys (sk-…) both work; sub-key scope rules apply.

header
Authorization: Bearer nr-your-key
  • Base URL: https://nullroute.tools. JSON in, JSON out.
  • X-API-Key: nr-your-key also works. A key query or body field is accepted but ends up in logs, so prefer the header.
  • Every successful response has a charged field: what the call cost in USD.
  • Exit targeting fields (type, country, city, asn, session) mirror the username parameters. Defaults: res, us.

Tools API

Endpoints

POST

/api/fetch

$0.001 + bandwidth

Fetches a URL through an exit and returns status, headers, body and timings. Retries blocked responses on a new exit by default.

FieldTypeDescription
urlrequiredstringhttp or https URL.
methodstringDefault GET.
headersobjectRequest headers, up to 32.
bodystringRequest body.
type, country, city, asn, sessionstringExit targeting. Default res, us.
retrybooleanDefault true. Up to 3 attempts on fresh sticky exits; +$0.001 when a retry was needed.
follow_redirectsbooleanDefault true, up to 10 hops.
timeout_msnumberDefault 30000, range 1000–120000.
request
curl https://nullroute.tools/api/fetch \
  -H "Authorization: Bearer $NULLROUTE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com","country":"de"}'
response
{
  "status": 200,
  "final_url": "https://example.com/",
  "headers": { "content-type": "text/html", "server": "cloudflare" },
  "body": "<!doctype html>…",
  "body_base64": null,
  "bytes": 1712,
  "truncated": false,
  "ttfb_ms": 412,
  "total_ms": 468,
  "attempts": 1,
  "blocked": null,
  "exit_type": "res",
  "country": "de",
  "charged": 0.0010005
}

Binary bodies come back in body_base64. Bodies over 8 MB are cut and flagged truncated. If every attempt is blocked you get the last response with blocked set (challenge, captcha, blocked, rate_limited) and pay bandwidth only.

POST

/api/geo-check

$0.002 / country + bandwidth

Loads one URL from up to 15 countries in parallel and tells you whether they all got the same content. Use it to spot geo-blocks and localized pages.

FieldTypeDescription
urlrequiredstringhttp or https URL.
countriesrequiredstring[]1–15 country codes, e.g. ["us","de","jp"].
typestringExit type for every country. Default res.
methodstringDefault GET.
timeout_msnumberPer country. Default 20000, max 60000.
request
curl https://nullroute.tools/api/geo-check \
  -H "Authorization: Bearer $NULLROUTE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com","countries":["us","de","jp"]}'
response
{
  "url": "https://example.com",
  "exit_type": "res",
  "results": [
    { "country": "us", "ok": true, "status": 200, "ttfb_ms": 380, "total_ms": 402,
      "final_url": "https://example.com/", "redirected": false, "title": "Example Domain",
      "content_length": 1256, "content_hash": "84238dfc8092e5d9c0dac8ef93371a07",
      "blocked": null, "fingerprint": ["Cloudflare"] },
    { "country": "jp", "ok": false, "error": "upstream_timeout" }
  ],
  "checked": 2,
  "consistent": true,
  "charged": 0.0040008
}

Only countries that answered are billed. consistent compares content_hash across countries that weren't blocked.

GET

/api/inspect

$0.001 + bandwidth

Follows redirects one hop at a time (up to 10) and reports the chain, the CDN or WAF in front of the site, and which security headers are set.

QueryTypeDescription
urlrequiredstringURL-encoded http or https URL.
type, country, city, asn, sessionstringExit targeting.
request
curl -G https://nullroute.tools/api/inspect \
  -H "Authorization: Bearer $NULLROUTE_KEY" \
  --data-urlencode "url=http://vercel.com" -d country=us
response
{
  "url": "http://vercel.com",
  "final_url": "https://vercel.com/",
  "status": 200,
  "redirects": [
    { "url": "http://vercel.com/", "status": 308, "location": "https://vercel.com/" },
    { "url": "https://vercel.com/", "status": 200, "location": null }
  ],
  "hops": 2,
  "headers": { "server": "Vercel", "x-vercel-id": "fra1::…", … },
  "fingerprint": ["Vercel"],
  "security": {
    "present": ["strict-transport-security", "content-security-policy", "x-content-type-options",
                "x-frame-options", "referrer-policy"],
    "missing": ["permissions-policy"],
    "score": 83
  },
  "title": "Vercel",
  "blocked": null,
  "ttfb_ms": 290,
  "total_ms": 351,
  "country": "us",
  "charged": 0.0010732
}
GET

/api/dns

$0.0002

Resolves a hostname over DNS-over-HTTPS through an exit, so you see the answers a visitor in that country gets.

QueryTypeDescription
namerequiredstringHostname, e.g. example.com.
typestringA (default), AAAA, CNAME, MX, TXT, NS, SOA, SRV, HTTPS.
countrystringExit country. Default us.
request
curl -H "Authorization: Bearer $NULLROUTE_KEY" \
  "https://nullroute.tools/api/dns?name=example.com&type=A&country=de"
response
{
  "name": "example.com",
  "type": "A",
  "country": "de",
  "answers": [
    { "name": "example.com", "type": "A", "ttl": 214, "data": "23.192.228.80" },
    { "name": "example.com", "type": "A", "ttl": 214, "data": "23.215.0.136" }
  ],
  "answer_count": 2,
  "charged": 0.0002
}
GET

/api/exit-ip

$0.0002

Returns the public IP a request with these targeting options leaves from. Pass session to check which IP a sticky session holds.

QueryTypeDescription
type, country, city, asn, sessionstringExit targeting. Default res, us.
request
curl -H "Authorization: Bearer $NULLROUTE_KEY" \
  "https://nullroute.tools/api/exit-ip?type=res&country=gb&session=4821"
response
{
  "exit_ip": "203.0.113.24",
  "exit_type": "res",
  "country": "gb",
  "session": "4821",
  "latency_ms": 640,
  "charged": 0.0002
}
POST

/api/proxy-test

$0.0005 / proxy

Checks whether proxies are alive, how fast they answer and which IP they exit from. Works with any provider's proxies, not just NullRoute.

FieldTypeDescription
proxiesrequiredstring[]1–200 lines: host:port, host:port:user:pass or scheme://user:pass@host:port. Schemes: http, https, socks4, socks5, socks5h.
timeout_msnumberPer proxy. Default 10000, max 30000.
request
curl https://nullroute.tools/api/proxy-test \
  -H "Authorization: Bearer $NULLROUTE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"proxies":["socks5://user:[email protected]:1080","198.51.100.9:8080"]}'
response
{
  "results": [
    { "proxy": "socks5://198.51.100.7:1080", "scheme": "socks5", "ok": true, "alive": true,
      "status": 200, "latency_ms": 312, "exit_ip": "198.51.100.7" },
    { "proxy": "http://198.51.100.9:8080", "scheme": "http", "ok": true, "alive": false,
      "latency_ms": 10004, "error": "timeout" }
  ],
  "tested": 2,
  "alive": 1,
  "dead": 1,
  "charged": 0.001
}

Lines that can't be parsed come back with "ok": false and aren't billed.

GET

/api/detect

$0.0001 / IP

Tells you whether an IP belongs to a known proxy or VPN provider, and which one. Use the bulk form for lists up to 1,000 addresses.

RequestParametersDescription
GET /api/detectipOne address. Master key only.
POST /api/detect/bulkips: string[]Up to 1,000 addresses, de-duplicated before billing. 10 calls per minute.
request
curl https://nullroute.tools/api/detect/bulk \
  -H "Authorization: Bearer $NULLROUTE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ips":["198.51.100.7","203.0.113.24","10.0.0.1"]}'
response
{
  "results": [
    { "ip": "198.51.100.7", "status": "proxy", "proxy": true, "providers": ["ExampleVPN"] },
    { "ip": "203.0.113.24", "status": "clean", "proxy": false, "providers": [] },
    { "ip": "10.0.0.1", "status": "unknown", "proxy": false, "providers": [] }
  ],
  "checked": 2,
  "proxies": 1,
  "clean": 1,
  "charged": 0.0002
}

Status is proxy, clean, unknown (private address or no data, free) or invalid (not an IP, free).

GET

/api/geo/availability

free

Where exits actually are: countries, states and cities where exits of a network answered in the last 1–24 hours, with the number of distinct exit IPs and a ready-to-use login for each place. Built from our own probes, live checks and traffic, located with the same geo database the filters are verified against. Served from memory and refreshed every minute, so polling it every minute from a panel is fine. To be sure a place has an exit at this moment, use /api/geo/check.

QueryTypeDescription
typestringres (default), dc or mob.
countrystringTwo-letter code. Without it the answer lists countries; with it, states and cities.
statestringOptional, with country: only that state and its cities.
hours1–24Window. Default 24. Smaller is fresher: hours=2 lists only places seen in the last two hours.
min_exitsintegerDrop places with fewer exits in the window. Default 1. min_exits=5 keeps the places a customer can rotate in.
formatstringjson (default) or csv: login,type,country,state,city,exits,last_seen,network, one line per country, or per state and city.
request
curl -H "Authorization: Bearer $NULLROUTE_KEY" \
  "https://nullroute.tools/api/geo/availability?type=res&country=us&state=tx"
response
{
  "type": "res",
  "country": "us",
  "window_hours": 24,
  "exits": 412,
  "states": [
    { "code": "tx", "name": "Texas", "exits": 412, "last_seen": 1793088000, "login": "res-us-st-tx" }
  ],
  "cities": [
    { "code": "houston", "name": "Houston", "state": "tx", "exits": 96,
      "last_seen": 1793087940, "login": "res-us-st-tx-city-houston" }
  ]
}

exits counts distinct exit IPs seen in the window; a state's count includes exits whose city is unknown, so cities add up to less than their state. Residential pools rotate: a place seen hours ago can be empty now, and the gateway then answers 502 no exit matching filter without charging. Without country the answer is countries: code, exits, last_seen, login. With type=dc and no country that list also covers countries the datacenter network has no exit in, and each item carries network: "res" when the country is served from residential, "dc" when it is datacenter-native. updated_at is when the numbers were last refreshed. GET /api/geo/cities is a reference list of place names, not availability.

GET

/api/geo/check

free

Can this login be served right now? The gateway runs the same verification a connection with this username would, at the moment you ask, and keeps the exits it found ready for the connection that follows. Use it before you hand a location to a customer or list it in your panel.

RequestParametersDescription
GET /api/geo/checkloginOne login, e.g. res-us-st-tx-city-houston, mob-de, res-us-os-windows.
POST /api/geo/checklogins: string[]Up to 100 logins, answered in request order.
request
curl https://nullroute.tools/api/geo/check \
  -H "Authorization: Bearer $NULLROUTE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"logins":["res-us-st-tx-city-houston","res-us-st-wy","mob-de"]}'
response
{
  "checked": 3,
  "available": 2,
  "results": [
    { "login": "res-us-st-tx-city-houston", "available": true, "verified_exits": 14, "network": "res" },
    { "login": "res-us-st-wy", "available": false, "verified_exits": 0 },
    { "login": "mob-de", "available": true, "verified_exits": null, "network": "mob" }
  ]
}

verified_exits is how many checked exits are ready for that filter (null for a plain country login, which needs no verification). network says which exit network actually serves the login — dc, res or mob — so a dc login running on the residential fallback shows res. A check takes up to 12 s when nothing is warm. Answers are shared for 30 s, and an account can check 300 logins a minute; past that an item gets "error": "rate_limited" (429 on GET). Other errors: invalid_login, session_not_location (a -sid- login pins one IP, it isn't a place).

Resellers

Reseller API

Issue proxy keys to your customers from your own shop or bot. Each key gets an expiry, a speed cap and an optional traffic cap; its traffic is charged to your balance at your wholesale price.

  • Base URL: https://nullroute.tools/api/v1/reseller. JSON in, JSON out.
  • Auth: Authorization: Bearer nr-…, your master key from Settings. Sub-keys are refused with 401 use_master_key.
  • Accounts without reseller access get 403 not_a_reseller; a paused reseller account gets 403 reseller_disabled.
  • No access yet? Unlock it from Billing for a one-off $170 — that buys the rights only (API, wholesale rates, client keys); traffic keeps billing your balance. An admin can re-price it or attach your domain afterwards.
  • When your balance reaches $0, every key you issued stops authenticating. When a key expires, is disabled or runs out of traffic, its open tunnels close within about 30 seconds.
shell
export PROXY_API_KEY="op-your-master-key"
export API="https://nullroute.tools/api/v1/reseller"
RequestDoes
GET /balanceBalance, wholesale price per GB and GB left per exit type.
POST /keysCreate a key. Returns 201 and the key object.
GET /keysList keys. Query: status=active|expired|disabled, limit (default 100, max 1000), offset.
GET /keys/:keyOne key. 404 not_found if it isn't yours.
PATCH /keys/:keyExtend, disable, re-enable, change speed, traffic cap or label.
DELETE /keys/:keyDelete the key. Returns {"success": true}.
GET /keys/:key/usageHourly traffic. Query hours, default 24, max 2160.

Create a key

FieldTypeDescription
hours or daysnumberLifetime. Default 1 day, max 365 days. If both are sent, hours wins.
speed_mbpsnumberShared by all the key's connections. 0 = no cap, max 10000.
traffic_gbnumberTraffic cap. 0 or absent = unlimited.
allowed_typesstringComma list of res, dc, mob. Empty = all.
labelstringYour note, e.g. the customer's order id.
request · 2 hours, 500 Mbps
curl -X POST $API/keys \
  -H "Authorization: Bearer $PROXY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"hours":2,"speed_mbps":500,"label":"order-1042"}'
response · 201
{
  "key": "sk-3f9c…",
  "label": "order-1042",
  "active": true,
  "expired": false,
  "created_at": "2026-10-05T12:00:00Z",
  "expires_at": "2026-10-05T14:00:00Z",
  "speed_mbps": 500,
  "traffic_limit_gb": null,
  "traffic_used_gb": 0.0,
  "cost_usd": 0.0,
  "allowed_types": "",
  "online": false,
  "now_mbps": 0.0,
  "connect": {
    "host": "gw.nullroute.tools",
    "port": 32424,
    "username_example": "res-any",
    "http": "http://res-any:sk-3f9c…@gw.nullroute.tools:32424",
    "socks5": "socks5://res-any:sk-3f9c…@gw.nullroute.tools:32424",
    "line": "gw.nullroute.tools:32424:res-any:sk-3f9c…"
  }
}

Send connect.line or connect.http to the customer as is. They can change res-any to any targeting username. Errors: 400 invalid_duration, invalid_speed, invalid_traffic_gb, invalid_allowed_types; 402 insufficient_balance when your balance is $0 or less.

Manage keys

PATCH takes any of extend_hours (added to the current expiry, or to now if the key already expired), active, speed_mbps, traffic_gb, label, and returns the updated key object.

curl
K=sk-3f9c…

# Extend by 24 hours
curl -X PATCH $API/keys/$K -H "Authorization: Bearer $PROXY_API_KEY" \
  -H "Content-Type: application/json" -d '{"extend_hours":24}'

# Disable now (re-enable with {"active":true})
curl -X PATCH $API/keys/$K -H "Authorization: Bearer $PROXY_API_KEY" \
  -H "Content-Type: application/json" -d '{"active":false}'

# Delete
curl -X DELETE $API/keys/$K -H "Authorization: Bearer $PROXY_API_KEY"

# Balance and hourly usage
curl $API/balance -H "Authorization: Bearer $PROXY_API_KEY"
curl "$API/keys/$K/usage?hours=24" -H "Authorization: Bearer $PROXY_API_KEY"
GET /balance
{
  "balance_usd": 42.5,
  "wholesale_per_gb": { "res": 0.35, "dc": 0.12, "mob": 0.6 },
  "gb_left": { "res": 121.43, "dc": 354.17, "mob": 70.83 },
  "gateway": "gw.nullroute.tools"
}
GET /keys/:key/usage
{
  "points": [
    { "hour": "2026-10-05T12:00:00Z", "bytes": 73400320 },
    { "hour": "2026-10-05T13:00:00Z", "bytes": 18874368 }
  ]
}

Hours are UTC. Hours without traffic are left out.

Example: sell a key from a bot

python
import os, requests

API = "https://nullroute.tools/api/v1/reseller"
HEADERS = {"Authorization": f"Bearer {os.environ['PROXY_API_KEY']}"}

def sell_key(order_id: str, hours: int, mbps: int = 0, gb: float = 0) -> str:
    """Call after payment. Returns the text to send to the customer."""
    r = requests.post(f"{API}/keys", headers=HEADERS, timeout=15, json={
        "label": order_id, "hours": hours,
        "speed_mbps": mbps, "traffic_gb": gb,
    })
    if r.status_code == 402:
        raise RuntimeError("Reseller balance is empty: top up before selling")
    r.raise_for_status()
    k = r.json()
    return (f"Your proxy (valid until {k['expires_at']} UTC):\n"
            f"{k['connect']['line']}\n"
            f"HTTP: {k['connect']['http']}")

# bot.send_message(chat_id, sell_key("tg-551-1042", hours=24, mbps=100))

Reference

Pricing and limits

Prepaid balance in USD; top up from $5 in the dashboard. Proxy traffic is billed per byte in both directions at the exit type's rate.

Exit typeUsernamePer GB
Residentialres$0.60
Datacenterdc$0.20
Mobilemob$1.00
ToolPricePer minuteBatch limit
POST /api/fetch$0.001 + bandwidth3008 MB body
Smart retry+$0.001 when a retry succeeds—3 attempts
POST /api/geo-check$0.002 / country + bandwidth6015 countries
GET /api/inspect$0.001 + bandwidth12010 redirects
GET /api/dns$0.0002300—
GET /api/exit-ip$0.0002120—
POST /api/proxy-test$0.0005 / proxy30200 proxies
/api/detect$0.0001 / IP120 · bulk 101,000 IPs

"Bandwidth" is the response size at the exit type's per-GB rate. Rate limits are per account; going over returns 429 rate_limited. A daily spend limit and per-key caps are set in Dashboard → API keys.

Reference

API errors

API errors are JSON with an HTTP status and a stable code:

response
HTTP/1.1 402 Payment Required
{ "error": "insufficient_balance" }
StatusCodeWhat to do
400invalid_url, invalid_schemeSend a full http or https URL.
400invalid_type, invalid_country, invalid_city, invalid_asn, invalid_sessionCheck the targeting fields against the username format.
400invalid_name, invalid_ip, missing_ipSend a valid hostname or IP address.
400no_countries, too_many_countries, no_proxies, too_many_proxies, no_ips, too_many_ipsStay within the batch limits in pricing and limits.
401missing_api_key, invalid_api_keySend Authorization: Bearer <key> with a current key.
401key_disabled, key_expiredRe-enable the sub-key or create a new one.
402insufficient_balanceTop up in Billing.
402spend_limit_reached, quota_exceededDaily limit or sub-key cap hit. Raise it, or wait for 00:00 UTC.
403exit_not_allowedThe sub-key doesn't allow that exit type or country.
429rate_limitedBack off and retry after a few seconds.
502target_unreachable, probe_failed, resolve_failed, too_many_redirectsThe exit couldn't complete the request. Retry; you aren't charged.

Proxy connection errors (407, 502, 403) are covered in troubleshooting.