Docs
Connect, target, measure.
First the gateway and the credential grammar, then the customer REST API.
01Quick start
Three steps from an empty account to traffic through the NRTH network. Everything below can be done in the customer panel or over the REST API.
- Register and load balance
Create an account at https://nrth.solutions/kayit, pick a package under Balance & Orders, and send the transfer with the order reference (looks like
NRTH-7K3Q2) in the description. Support marks the order paid and the GB land on your balance. You pay per GB (1 GB = 1024³ bytes); every connection counts at least 512 bytes. - Create a proxy user
A proxy user is a username + secret pair for the gateway. Create one under Proxy users; the secret is shown once, so copy it right away. One account can hold up to 200 proxy users (one per project, per machine, per customer of yours — however you like); each can get its own GB cap, default country and connection limit.
curl -s "https://nrth.solutions/api/v1/proxy-users" \ -X POST \ -H "Authorization: Bearer $NRTH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"username":"cus-7h2k9q","label":"scraper-1","quota_gb":20,"allow_targeting":true,"default_country":"TR"}' - Connect
Point your tool at
tunnel.nrth.solutions:1337with the username and the secret. HTTP and SOCKS5 share the port — the gateway detects the protocol. Append_country-XXto the secret to choose a country and_session-nameto keep the same IP.# HTTP proxy curl -x http://tunnel.nrth.solutions:1337 -U 'cus-7h2k9q:aB3dE6fG9hJ2kL5m' https://api.ipify.org # Türkiye, same IP for 30 minutes curl -x http://tunnel.nrth.solutions:1337 -U 'cus-7h2k9q:aB3dE6fG9hJ2kL5m_country-TR_session-office1_lifetime-30' https://api.ipify.org # SOCKS5 (socks5h: the gateway resolves the hostname) curl -x socks5h://cus-7h2k9q:aB3dE6fG9hJ2kL5m@tunnel.nrth.solutions:1337 https://api.ipify.org
The response of the IP check is the exit IP of the NRTH network, never your own. If you see your own address, the proxy settings did not take effect.
02Connection guides
Every tool needs the same four values. HTTPS targets go through a CONNECT tunnel end-to-end, so there is no certificate to install.
| Setting | Value |
|---|---|
| Host | tunnel.nrth.solutions |
| Port | 1337 (HTTP and SOCKS5) |
| Username | the proxy username, e.g. cus-7h2k9q |
| Password | the secret, optionally followed by parameters: aB3dE6fG9hJ2kL5m_country-TR_session-office1 |
| Target DNS | Resolved by the network, so use socks5h:// / "proxy DNS" options where they exist. |
curl
curl -x http://tunnel.nrth.solutions:1337 -U 'cus-7h2k9q:aB3dE6fG9hJ2kL5m_country-TR' https://api.ipify.org?format=json
curl -x socks5h://cus-7h2k9q:aB3dE6fG9hJ2kL5m_country-TR@tunnel.nrth.solutions:1337 https://api.ipify.org?format=json
# for a whole shell session
export https_proxy=http://cus-7h2k9q:aB3dE6fG9hJ2kL5m@tunnel.nrth.solutions:1337
-U user:passkeeps you from URL-encoding the password; in a URL,@and:inside the password must be percent-encoded (secrets never contain them, parameters never need it).-vprints the proxy handshake: a407means wrong credentials, a403carries the reason in its body (see Errors).- Use
socks5h://, notsocks5://: withsocks5hthe hostname is resolved by the network, so geo-restricted sites see a matching DNS resolver. --connect-timeout 15 --max-time 60are sensible defaults; "Connection refused" means a wrong host or port, not wrong credentials.
Python (requests)
import requests
PROXY = "http://cus-7h2k9q:aB3dE6fG9hJ2kL5m_country-TR@tunnel.nrth.solutions:1337"
proxies = {"http": PROXY, "https": PROXY} # both keys: the https key governs https targets
with requests.Session() as s:
s.trust_env = False # ignore HTTP_PROXY/HTTPS_PROXY from the environment
r = s.get("https://api.ipify.org?format=json", proxies=proxies, timeout=(5, 30))
r.raise_for_status()
print(r.json())
# SOCKS5: pip install "requests[socks]" then PROXY = "socks5h://cus-7h2k9q:aB3dE6fG9hJ2kL5m@tunnel.nrth.solutions:1337"
- Always pass
timeout=; without it a stalled connection waits forever. - Catch
requests.exceptions.ProxyError(gateway refused: check the 403 body), thenTimeout, thenHTTPErrorfrom the target. - For retries use
HTTPAdapter(max_retries=Retry(total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504])). - Each
Sessionreuses the TCP connection to the gateway; a sticky session name in the password keeps the exit IP across requests.
Node.js (axios, fetch)
import axios from 'axios';
const client = axios.create({
proxy: { protocol: 'http', host: 'tunnel.nrth.solutions', port: 1337, auth: { username: 'cus-7h2k9q', password: 'aB3dE6fG9hJ2kL5m_country-TR' } },
timeout: 15_000,
});
const { data } = await client.get('https://api.ipify.org?format=json');
console.log(data);
// SOCKS5 with axios: npm i socks-proxy-agent (always proxy: false when an agent is used)
import { SocksProxyAgent } from 'socks-proxy-agent';
const agent = new SocksProxyAgent('socks5h://cus-7h2k9q:aB3dE6fG9hJ2kL5m@tunnel.nrth.solutions:1337');
const socks = axios.create({ httpAgent: agent, httpsAgent: agent, proxy: false, timeout: 15_000 });
// fetch (Node 18+): npm i undici
import { ProxyAgent } from 'undici';
const token = 'Basic ' + Buffer.from('cus-7h2k9q:aB3dE6fG9hJ2kL5m_country-TR').toString('base64');
const dispatcher = new ProxyAgent({ uri: 'http://tunnel.nrth.solutions:1337', token });
const r = await fetch('https://api.ipify.org?format=json', { dispatcher, signal: AbortSignal.timeout(15_000) });
console.log(await r.json());
encodeURIComponent()the password if you build a proxy URL by hand; theauthobject form needs no encoding.- The global
fetchin Node ignoresHTTP_PROXY; pass a dispatcher as above, orsetGlobalDispatcher(dispatcher)once.
Playwright
import { chromium } from 'playwright';
const browser = await chromium.launch({
proxy: { server: 'http://tunnel.nrth.solutions:1337', username: 'cus-7h2k9q', password: 'aB3dE6fG9hJ2kL5m_country-TR_session-pw1_lifetime-30', bypass: 'localhost,127.0.0.1' },
});
const page = await browser.newPage();
await page.goto('https://api.ipify.org?format=json', { timeout: 60_000 });
console.log(await page.textContent("body"));
await browser.close();
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(proxy={"server": "http://tunnel.nrth.solutions:1337", "username": "cus-7h2k9q", "password": "aB3dE6fG9hJ2kL5m_country-TR"})
page = browser.new_page()
page.goto("https://api.ipify.org?format=json", timeout=60_000)
print(page.text_content("body"))
browser.close()
- One proxy per context: launch with
proxy: { server: 'per-context' }and pass the real proxy to eachnewContext()— on Windows Chromium needs this launch flag for per-context proxies. - Use HTTP, not SOCKS5: Chromium cannot send a username/password over SOCKS5. If you must use SOCKS5, whitelist your IP (see IP whitelist) and connect without credentials.
- A browser opens many connections per page; a sticky session name keeps them on one exit IP, which most sites expect.
Puppeteer
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ args: ['--proxy-server=http://tunnel.nrth.solutions:1337'] });
const page = await browser.newPage();
await page.authenticate({ username: 'cus-7h2k9q', password: 'aB3dE6fG9hJ2kL5m_country-TR_session-pp1' }); // before goto, on every page
await page.goto('https://api.ipify.org?format=json', { timeout: 60_000 });
console.log(await page.evaluate(() => document.body.innerText));
await browser.close();
page.authenticate()only works for HTTP proxies. For SOCKS5 either run a local forwarder (e.g. theproxy-chainpackage’sanonymizeProxy) or use the IP whitelist and drop the credentials.headless: falseis the fastest way to see what the proxy really returns while debugging.
Selenium (Python, Chrome)
Chrome has no flag for proxy credentials, so Selenium users ship a tiny extension that sets the proxy and answers the authentication prompt. The simpler alternative on a fixed IP: whitelist it and start Chrome with just --proxy-server — that also works headless.
import json, os, tempfile, zipfile
from selenium import webdriver
HOST, PORT = "tunnel.nrth.solutions", 1337
USER, PASSWORD = "cus-7h2k9q", "aB3dE6fG9hJ2kL5m_country-TR"
manifest = {"manifest_version": 3, "name": "NRTH proxy", "version": "1.0",
"permissions": ["proxy", "webRequest", "webRequestAuthProvider"],
"host_permissions": ["<all_urls>"], "background": {"service_worker": "bg.js"}}
bg = f"""
chrome.proxy.settings.set({{ value: {{ mode: "fixed_servers", rules: {{
singleProxy: {{ scheme: "http", host: {json.dumps(HOST)}, port: {PORT} }}, bypassList: ["localhost", "127.0.0.1"] }} }}, scope: "regular" }});
chrome.webRequest.onAuthRequired.addListener(
() => ({{ authCredentials: {{ username: {json.dumps(USER)}, password: {json.dumps(PASSWORD)} }} }}),
{{ urls: ["<all_urls>"] }}, ["blocking"]);
"""
ext = os.path.join(tempfile.mkdtemp(), "nrth-proxy.zip")
with zipfile.ZipFile(ext, "w") as z:
z.writestr("manifest.json", json.dumps(manifest))
z.writestr("bg.js", bg)
options = webdriver.ChromeOptions()
options.add_extension(ext)
driver = webdriver.Chrome(options=options)
driver.get("https://api.ipify.org?format=json")
print(driver.find_element("tag name", "body").text)
driver.quit()
- Extensions do not load with
--headless=new; use the whitelist route for headless runs, or Playwright. - Firefox: set
network.proxy.type = 1,network.proxy.http/network.proxy.sslto the host and port in a profile; the first request prompts for credentials, which Selenium cannot type — again, the whitelist avoids the prompt.
Scrapy
# settings.py
NRTH_PROXY = "http://cus-7h2k9q:aB3dE6fG9hJ2kL5m_country-TR@tunnel.nrth.solutions:1337"
DOWNLOADER_MIDDLEWARES = {"myproject.middlewares.NrthProxyMiddleware": 350}
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0 Safari/537.36"
# middlewares.py
class NrthProxyMiddleware:
def __init__(self, proxy):
self.proxy = proxy
@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings.get("NRTH_PROXY"))
def process_request(self, request, spider):
request.meta.setdefault("proxy", self.proxy) # per-request override: set request.meta["proxy"] yourself
- Scrapy’s built-in
HttpProxyMiddlewareturns the credentials in the URL into theProxy-Authorizationheader; nothing else is needed for HTTP. - For one exit IP per spider run put
_session-<name>in the password; for one per request build a different session name inprocess_request. - SOCKS5 needs the
scrapy-socksdownload handlers and asocks5h://URL.
Chrome and Firefox (manual)
Chrome uses the operating system’s proxy. Windows: Settings → Network & internet → Proxy → Manual proxy setup → address tunnel.nrth.solutions, port 1337. macOS: System Settings → Network → your connection → Proxies → Web Proxy (HTTP) and Secure Web Proxy (HTTPS), both tunnel.nrth.solutions:1337. The first page load asks for the username and password; the password field takes the parameters too (aB3dE6fG9hJ2kL5m_country-TR_session-home).
Firefox has its own settings: Settings → search "proxy" → Network Settings → Manual proxy configuration → HTTP Proxy tunnel.nrth.solutions port 1337, tick "Also use this proxy for HTTPS". For SOCKS: SOCKS Host tunnel.nrth.solutions port 1337, SOCKS v5, tick "Proxy DNS when using SOCKS v5".
- System-wide proxies affect every app on the machine; switch back to "no proxy" when done.
- Per-site rules and quick switching: a proxy-switcher extension (FoxyProxy for Firefox, ZeroOmega / SwitchyOmega for Chrome) that stores the username and password — SOCKS5 with a password only works through such an extension in Chrome.
Android
Android’s Wi-Fi proxy setting has no field for a username and password, so there are two ways:
- IP whitelist (no app): add your network’s public IP to the proxy user’s whitelist, then Wi-Fi → your network → Modify → Advanced → Proxy: Manual, host
tunnel.nrth.solutions, port1337. Only Wi-Fi, and only apps that honour the system proxy (browsers do; many apps do not). - A proxy client app (VPN-style, routes all apps, works on mobile data): any app that supports HTTP or SOCKS5 proxies with authentication, e.g. Postern or Drony. Enter
tunnel.nrth.solutions, port1337, username, password with parameters, and a rule to route everything.
Force-stop apps (or toggle airplane mode) after changing the proxy; otherwise open connections keep going direct.
iOS
Wi-Fi proxy (HTTP, built in): Settings → Wi-Fi → (i) next to your network → Configure Proxy → Manual → Server tunnel.nrth.solutions, Port 1337, Authentication on, username and password. Wi-Fi only; some apps ignore it.
All apps / mobile data / SOCKS5: a VPN-style proxy app such as Shadowrocket (paid) or Potatso (free, SOCKS5 only) with the same host, port and credentials. Disconnect it when you are done so the phone goes direct again.
03Credential grammar
Targeting is written into the password: the secret, then any number of _key-value tokens. Order does not matter. Nothing goes into the username.
aB3dE6fG9hJ2kL5m
aB3dE6fG9hJ2kL5m_country-TR
aB3dE6fG9hJ2kL5m_country-TR_session-office1_lifetime-30
aB3dE6fG9hJ2kL5m_country-US_city-new.york_hardsession-k2
aB3dE6fG9hJ2kL5m_country-DE_geosource-maxmind_device-windows
Rules: keys are lower case letters; values are 1–64 characters of A-Z a-z 0-9 . , -; the secret never contains _, so the first _ always starts the parameters. A token the gateway does not accept rejects the whole request (HTTP 400 / SOCKS5 reply 0x02) — nothing is silently ignored.
| Token | Meaning | Notes |
|---|---|---|
country-XX | ISO 3166-1 alpha-2, upper case: country-TR, country-US. The full list: GET /api/v1/locations. | Any proxy user with targeting allowed |
region-name | Region / state, lower case, spaces as dots: region-california. Combine with country. | Names from /locations when lists are available |
city-name | City, lower case, spaces as dots: city-istanbul, city-new.york. Combine with country; an unavailable city answers 400. | Same |
continent-name | africa, asia, europe, north.america, oceania, south.america. | |
session-name | Sticky session: the same name keeps the same exit IP for lifetime minutes (default 30). 1–64 characters of A-Z a-z 0-9 . , -. Names are private to your proxy user. | See Sessions |
hardsession-name | Keeps the IP as long as the device stays online — no lifetime, no automatic change. | No lifetime |
lockedsession-name | Locked to one IP, never substituted: when that IP goes offline the request fails instead of moving. | No lifetime |
lifetime-30 | Minutes a session keeps its IP: 1–1440, default 30. Out of range → 400. | Only with session |
isp-code | Operator / carrier shortcode, lower case (e.g. isp-turkcell). Codes vary per country; ask support for the current list. | Expert |
asn-AS15169 | Autonomous system, AS + digits. | Expert |
zip-90210 | Postal code, letters and digits without spaces (zip-SW1A1AA, zip-10115). | Expert |
geosource-maxmind | Which geolocation database decides what counts as "in TR": ipapi (default) or maxmind, lower case. Use the one your target site uses. | Expert |
latency-500 | Maximum round-trip to the exit, in ms. Below 300 the pool gets very small. | Expert; not with extended |
fraudscore-5 | Maximum reputation score of the exit: 0, 5 or 10 (0 = cleanest IPs only). | Expert; not with extended |
device-windows | Exit device’s TCP fingerprint: windows, unix (Linux/Android) or apple (iOS/macOS). | Expert; not with extended |
activesince-60 | Exit must have been online for at least this many minutes (60–240 is the useful range; above 300 the pool shrinks sharply). | Expert |
extended-1 | Opens the extended pool (several times larger, more variance). Cannot be combined with other expert parameters. | Expert |
adblock-1 | Blocks known ad and tracker hosts at the exit. |
Accepted keys, complete: country, region, city, continent, isp, asn, zip, geosource, session, hardsession, lockedsession, lifetime, latency, fraudscore, device, activesince, extended, adblock. Not available on NRTH (always 400): mode, cache, cacheduration, cacheignoreheader, http3, udp — and every unknown key.
Defaults and the targeting permission
- A proxy user can carry default parameters (
country-TR, orcountry-TR_city-istanbul): they are applied whenever the password has no geo token at all. Session tokens cannot be defaults. - With targeting disabled (
allow_targeting: false) every token is refused with 400; only the defaults set by the owner apply. Useful when you hand a proxy user to someone else and want to pin the country. - Expert parameters narrow the pool. Start without them, add one at a time, and watch the share of failed connections.
04Sessions and rotation
Without a session token every connection gets a fresh exit IP — that is the default and the right choice for most crawling. When a site needs to see one visitor, pin the IP with a session.
| Type | Token | Behaviour | Ends when |
|---|---|---|---|
| Rotating | (none) | New IP on every connection. | — |
| Sticky | session-name + lifetime-N | Same IP for N minutes (1–1440, default 30); if the exit drops, a replacement is chosen so your work continues. | After lifetime, or when you change the name. |
| Hard | hardsession-name | Same IP as long as the exit device is online; no automatic change. | When the exit goes offline, or you change the name. |
| Locked | lockedsession-name | One IP, never substituted; a request fails rather than moving to another exit. | Same; expect failures and retry with a new name. |
- Names are yours alone. The gateway maps
office1of your proxy user to an internal id; another customer usingoffice1lands on a different IP, and the same name always maps to the same session for you. - To rotate, change the name.
session-a1→session-a2is an instant new IP; no API call needed. Generating a list withsession=stickygives you one random name per line for exactly this. - Forced rotation of a live name:
POST /api/v1/sessions/{name}/rotateasks the network to drop the IP behind the name. It answerssupported: falsewhile the network does not offer it — then change the name instead. - Browsers and HTTP/2 clients open many parallel connections; give the whole page one session name or the site sees several IPs at once.
- Sessions do not survive the proxy user’s secret rotation or a disabled state: both close the connections.
05IP whitelist authentication
Tools that cannot send a proxy password (Android Wi-Fi, headless Chrome, some SDKs) can authenticate by their source address instead: add the address to a proxy user’s whitelist and connect without credentials.
- HTTP: send the request without a
Proxy-Authorizationheader. A whitelisted address is treated as that proxy user; anything else gets407. - SOCKS5: offer the "no authentication" method (
0x00). It is accepted only for a whitelisted address; otherwise the gateway answers0xFF(no acceptable method) and closes. - Entries are single addresses (
203.0.113.5,2001:db8::7) or blocks: at least/16for IPv4 and/48for IPv6. Up to 50 per proxy user. - An address can belong to one proxy user across the whole service: a credential-less connection must map to exactly one account to bill. If a shared office IP is already registered to someone else, use credentials.
- A credential-less connection carries no password, hence no
_key-valuetokens: set the proxy user’s default parameters (country-TR) for targeting. Sessions are not available this way. auth_mode: ipmakes a proxy user accept only whitelisted addresses — its secret stops working. Use it when the secret has to live on a machine you do not fully control.- Dynamic home connections change their address; whitelist the block your ISP uses or stick to credentials. Behind carrier-grade NAT (mobile data) thousands of people share one address — do not whitelist it.
curl -s "https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB/whitelist" \
-X POST \
-H "Authorization: Bearer $NRTH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ip":"203.0.113.5","note":"office"}'
# then, from that address, no credentials at all
curl -x http://tunnel.nrth.solutions:1337 https://api.ipify.org?format=json
06Gateway errors
The gateway answers every refusal itself with a short text/plain line in Turkish and English, so you can match on the English half. It never relays anything from the network behind it.
HTTP proxy
| Status | Body | Meaning / what to do |
|---|---|---|
407 Proxy Authentication Required | (empty; Proxy-Authenticate: Basic realm="nrth") | No or wrong credentials, a rotated secret, a proxy user in ip mode, or an address that is not whitelisted. |
403 | NRTH: bakiye bitti / balance exhausted | Account balance is 0 — buy a package. |
403 | NRTH: bakiye süresi doldu / balance expired | The balance validity passed; a new package extends it. |
403 | NRTH: kota doldu / quota exhausted | This proxy user’s own GB cap is used up; raise it or reset it in the panel. |
403 | NRTH: hesap kapalı / account disabled | The proxy user is disabled. |
403 | NRTH: hesap süresi doldu / account expired | The proxy user’s expiry date passed. |
403 | NRTH: hesap askıda / account suspended | The account is suspended; check your tickets. |
403 | NRTH: çok fazla bağlantı / too many connections | The proxy user’s max_connections is reached. |
403 | NRTH: eşzamanlı bağlantı sınırı / concurrency limit reached | The account-wide concurrency limit is reached. |
400 | NRTH: geçersiz proxy parametresi / invalid proxy parameter: <key> | A token the gateway refuses: unknown key, not available on NRTH, targeting disabled for this proxy user, or lifetime out of range. The key is named. |
400 | NRTH: geçersiz proxy parametresi / invalid proxy parameter | The network could not satisfy the combination (e.g. an unknown city). |
400 | NRTH: geçersiz istek / bad request | Not a proxy request (a bare path instead of CONNECT or an absolute URL). |
502 | NRTH: üst bağlantı kurulamadı / upstream unavailable | The network could not connect to the target or timed out — retry, or try another exit (new session name). |
502 | NRTH: gateway upstream auth error | A problem on our side between the gateway and the network. Retry in a minute; if it persists, open a ticket. |
SOCKS5
| Stage | Code | Meaning |
|---|---|---|
| Method selection | 0xFF | No acceptable method: you offered no username/password and your address is not whitelisted. |
| Authentication | 0x01 | Wrong username or secret (or the proxy user is in ip mode). |
| Reply | 0x00 | Connected. |
| Reply | 0x02 | Not allowed: any of the 403 reasons above, or a refused parameter (the 400 reasons). |
| Reply | 0x05 | Connection refused by the network or the target. |
| Reply | 0x01 | General failure (target unreachable, timeout). |
| Reply | 0x07 | Only CONNECT is supported — no BIND, no UDP ASSOCIATE. |
| Reply | 0x08 | Unsupported address type. |
SOCKS5 carries no text, so when a SOCKS client is refused, repeat the request with curl over HTTP to read the reason.
07REST API
Base URL https://nrth.solutions/api/v1. JSON in and out, snake_case fields, timestamps in ISO 8601 (UTC), sizes in bytes as integers with a _gb twin where it helps (1 GB = 1024³ bytes). Everything the customer panel does, this API does too.
Authentication
Create a key on the panel’s API page; it is shown once and can be regenerated any time (the old key dies immediately). Send it as a Bearer token. GET /packages, GET /locations and GET /openapi.json work without a key.
export NRTH_API_KEY=nrth_…
curl -s https://nrth.solutions/api/v1/me -H "Authorization: Bearer $NRTH_API_KEY"import os, requests
BASE = "https://nrth.solutions/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['NRTH_API_KEY']}"}
r = requests.get(f"{BASE}/me", headers=HEADERS, timeout=15)
r.raise_for_status()
print(r.json()["balance_gb"])const BASE = 'https://nrth.solutions/api/v1';
const HEADERS = { Authorization: `Bearer ${process.env.NRTH_API_KEY}`, 'Content-Type': 'application/json' };
const r = await fetch(`${BASE}/me`, { headers: HEADERS });
console.log((await r.json()).balance_gb);There are no rate limits, no pagination tokens to juggle (lists take limit and offset), and CORS is open (*), so the API can be called from a browser page as well. Keep the key out of client-side code you ship to others, though: it controls your whole account.
Errors
Every failure is an RFC 9457 problem document (Content-Type: application/problem+json). Branch on code, show detail to humans. Validation failures are 422, missing or bad keys 401, unknown or foreign ids 404 (the API never reveals whether an id exists for someone else), conflicts 409.
{
"type": "https://nrth.solutions/docs#api-errors:username_taken",
"title": "Username taken",
"status": 409,
"detail": "Pick another proxy username; usernames are global.",
"instance": "/api/v1/proxy-users",
"code": "username_taken"
}
| Code | HTTP | Meaning |
|---|---|---|
unauthorized | 401 | Send your API key as Authorization: Bearer <key>. Keys are created on the API page of the customer panel. |
invalid_api_key | 401 | The API key is unknown, was regenerated, or belongs to a closed account. |
forbidden | 403 | This action is not allowed for your account. |
account_banned | 403 | This account is closed. |
customer_inactive | 403 | The account is suspended; proxy users cannot be created while it is suspended. |
not_found | 404 | No such resource, or it belongs to another account. |
no_such_endpoint | 404 | The path or method does not exist in this API. See /api/v1/openapi.json. |
invalid_json | 400 | The request body is not valid JSON. |
payload_too_large | 413 | The request body exceeds 64 KB. |
unsupported_media_type | 415 | Send request bodies as application/json. |
invalid_query | 422 | A query parameter has an invalid value. |
validation | 422 | A field has an invalid value. |
internal | 500 | Something went wrong on our side. The incident is logged; try again or open a ticket. |
username_taken | 409 | Pick another proxy username; usernames are global. |
invalid_username | 422 | Usernames are 3–32 characters: a-z 0-9 . _ -. |
too_many_proxy_users | 422 | An account can have at most 200 proxy users. |
invalid_default_params | 422 | Default parameters use the credential grammar without session keys, e.g. country-TR. |
invalid_ip | 422 | Give an IPv4/IPv6 address or a CIDR block. |
cidr_too_wide | 422 | Blocks must be /16 or narrower for IPv4 and /48 or narrower for IPv6. |
whitelist_full | 422 | A proxy user can have at most 50 whitelist entries. |
ip_exists | 409 | This address is already on the whitelist of this proxy user. |
ip_in_use | 409 | This address is registered to another proxy user; use credentials instead. |
invalid_session_name | 422 | Session names are 1–64 characters: A-Z a-z 0-9 . , -. |
rotate_failed | 502 | The network did not accept the rotation request; retry in a moment. |
package_not_found | 404 | No visible package with that id. |
invalid_method | 422 | method must be bank or crypto. |
method_not_offered | 422 | This payment method is not currently offered. |
too_many_pending_orders | 422 | At most 10 orders can be pending; cancel one or wait for it to be handled. |
order_not_pending | 409 | Only pending orders can be cancelled. |
subject_required | 422 | A ticket needs a subject. |
subject_too_long | 422 | Subjects are at most 120 characters. |
body_required | 422 | A message body is required. |
body_too_long | 422 | Messages are at most 5000 characters. |
invalid_priority | 422 | priority must be normal or high. |
too_many_open_tickets | 422 | At most 20 tickets can be open at once. |
invalid_day | 422 | Days are written YYYY-MM-DD. |
targeting_not_allowed | 422 | This proxy user has targeting disabled; country, session and other parameters are refused. |
invalid_secret | 422 | The proxy secret is 16 letters and digits with no underscore. |
invalid_params | 422 | A parameter key is unknown, not available, or not allowed here. |
Field-specific validation codes follow a pattern: invalid_<field>, <field>_required, <field>_too_long. invalid_params carries details.key, the offending token key.
Account
Balance and account state.
Account
GET /api/v1/me
Balance, expiry, status and limits of the account the key belongs to.
Response 200: Account — The account.
Errors — 401: unauthorized, invalid_api_key
curl -s "https://nrth.solutions/api/v1/me" \
-H "Authorization: Bearer $NRTH_API_KEY"r = requests.get("https://nrth.solutions/api/v1/me", headers=HEADERS, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/me', { headers: HEADERS });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Usage
Daily traffic per proxy user.
Daily usage
GET /api/v1/usage
Daily rows for a date range (default: the last 30 days, Istanbul days). Rows of the current and previous day are computed live, so they change while traffic flows.
| Parameter | In | Type | Meaning |
|---|---|---|---|
from | query | string | First day, YYYY-MM-DD. |
to | query | string | Last day (inclusive), YYYY-MM-DD. |
proxy_user | query | string | Proxy user id or username to filter by. |
Response 200: UsageResponse — Rows and totals.
Errors — 401: unauthorized · 404: not_found · 422: invalid_from, invalid_to, invalid_day
curl -s "https://nrth.solutions/api/v1/usage?from=2026-10-01&to=2026-10-07" \
-H "Authorization: Bearer $NRTH_API_KEY"r = requests.get("https://nrth.solutions/api/v1/usage", headers=HEADERS, params={"from": "2026-10-01", "to": "2026-10-07"}, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/usage?from=2026-10-01&to=2026-10-07', { headers: HEADERS });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Proxy users
The credentials used at the gateway and their IP whitelists.
List proxy users
GET /api/v1/proxy-users
Every proxy user of the account with its state and usage.
Response 200: ProxyUserList — List.
Errors — 401: unauthorized
curl -s "https://nrth.solutions/api/v1/proxy-users" \
-H "Authorization: Bearer $NRTH_API_KEY"r = requests.get("https://nrth.solutions/api/v1/proxy-users", headers=HEADERS, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/proxy-users', { headers: HEADERS });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Create a proxy user
POST /api/v1/proxy-users
Creates a proxy user and returns its secret once. At most 200 per account.
Body: JSON ProxyUserInput (optional).
| Field | Type | Meaning |
|---|---|---|
username | string | Optional; a cus-xxxxxx name is generated when omitted. Usernames are global, 3–32 characters, a-z 0-9 . _ -. |
label | string | Free text label (≤ 64). |
note | string | Private note (≤ 500). |
quota_gb | number | null | Per-proxy-user cap in GB; null or omitted = unlimited (the account balance still limits). |
max_connections | integer | null | Simultaneous connection cap; null = unlimited. |
allow_targeting | boolean | Default true. When false every _key-value parameter is refused. |
default_country | string | ISO 3166-1 alpha-2; becomes default_params: country-XX. |
default_params | string | Full default parameter string (country-TR_city-istanbul); overrides default_country. No session keys. |
auth_mode | password | ip | password (default) or ip. |
enabled | boolean | Default true. |
Response 201: ProxyUserCreated — Created; secret appears only here.
Errors — 401: unauthorized · 403: customer_inactive · 409: username_taken · 422: invalid_username, invalid_quota_gb, invalid_max_connections, invalid_default_country, invalid_default_params, invalid_auth_mode, too_many_proxy_users
curl -s "https://nrth.solutions/api/v1/proxy-users" \
-X POST \
-H "Authorization: Bearer $NRTH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"username":"cus-7h2k9q","label":"scraper-1","quota_gb":20,"allow_targeting":true,"default_country":"TR"}'r = requests.post("https://nrth.solutions/api/v1/proxy-users", headers=HEADERS, json={"username": "cus-7h2k9q", "label": "scraper-1", "quota_gb": 20, "allow_targeting": True, "default_country": "TR"}, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/proxy-users', { method: 'POST', headers: HEADERS, body: JSON.stringify({"username":"cus-7h2k9q","label":"scraper-1","quota_gb":20,"allow_targeting":true,"default_country":"TR"}) });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Get a proxy user
GET /api/v1/proxy-users/{id}
One proxy user by id.
| Parameter | In | Type | Meaning |
|---|---|---|---|
id * | path | string | Proxy user id (usr_…). |
Response 200: ProxyUser — The proxy user.
Errors — 401: unauthorized · 404: not_found
curl -s "https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB" \
-H "Authorization: Bearer $NRTH_API_KEY"r = requests.get("https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB", headers=HEADERS, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB', { headers: HEADERS });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Update a proxy user
PATCH /api/v1/proxy-users/{id}
Changes only the fields you send. Disabling cuts open connections at once.
| Parameter | In | Type | Meaning |
|---|---|---|---|
id * | path | string | Proxy user id (usr_…). |
Body: JSON ProxyUserPatch.
| Field | Type | Meaning |
|---|---|---|
label | string | Label. |
note | string | Note. |
enabled | boolean | Enable / disable (open connections are cut on disable). |
quota_gb | number | null | New cap in GB; null = unlimited. |
max_connections | integer | null | null = unlimited. |
allow_targeting | boolean | Targeting permission. |
default_country | string | null | ISO code or null to clear. |
default_params | string | null | Parameter string or null to clear. |
auth_mode | password | ip | password or ip. |
expires_at | timestamp (ISO 8601) | null | Expiry, or null for never. |
Response 200: ProxyUser — Updated proxy user.
Errors — 401: unauthorized · 404: not_found · 422: invalid_quota_gb, invalid_max_connections, invalid_default_country, invalid_default_params, invalid_auth_mode, invalid_expires_at, invalid_body
curl -s "https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB" \
-X PATCH \
-H "Authorization: Bearer $NRTH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"label":"scraper-1 (eu)","quota_gb":50,"default_country":"DE"}'r = requests.patch("https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB", headers=HEADERS, json={"label": "scraper-1 (eu)", "quota_gb": 50, "default_country": "DE"}, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB', { method: 'PATCH', headers: HEADERS, body: JSON.stringify({"label":"scraper-1 (eu)","quota_gb":50,"default_country":"DE"}) });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Delete a proxy user
DELETE /api/v1/proxy-users/{id}
Deletes the proxy user, its whitelist and cuts its connections. Usage history stays in the daily rows.
| Parameter | In | Type | Meaning |
|---|---|---|---|
id * | path | string | Proxy user id (usr_…). |
Response 204: JSON — Deleted.
Errors — 401: unauthorized · 404: not_found
curl -s "https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB" \
-X DELETE \
-H "Authorization: Bearer $NRTH_API_KEY"r = requests.delete("https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB", headers=HEADERS, timeout=15)
r.raise_for_status()
print(r.status_code) # 204const r = await fetch('https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB', { method: 'DELETE', headers: HEADERS });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(r.status); // 204Rotate the secret
POST /api/v1/proxy-users/{id}/rotate-secret
Generates a new secret and returns it once; the old one stops working immediately.
| Parameter | In | Type | Meaning |
|---|---|---|---|
id * | path | string | Proxy user id (usr_…). |
Response 200: ProxyUserCreated — New secret.
Errors — 401: unauthorized · 404: not_found
curl -s "https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB/rotate-secret" \
-X POST \
-H "Authorization: Bearer $NRTH_API_KEY"r = requests.post("https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB/rotate-secret", headers=HEADERS, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB/rotate-secret', { method: 'POST', headers: HEADERS });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());List whitelisted IPs
GET /api/v1/proxy-users/{id}/whitelist
Addresses that authenticate as this proxy user without credentials.
| Parameter | In | Type | Meaning |
|---|---|---|---|
id * | path | string | Proxy user id (usr_…). |
Response 200: WhitelistList — Entries.
Errors — 401: unauthorized · 404: not_found
curl -s "https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB/whitelist" \
-H "Authorization: Bearer $NRTH_API_KEY"r = requests.get("https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB/whitelist", headers=HEADERS, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB/whitelist', { headers: HEADERS });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Whitelist an IP
POST /api/v1/proxy-users/{id}/whitelist
Adds an address or block. An address can be registered to one proxy user only, across all accounts. At most 50 per proxy user.
| Parameter | In | Type | Meaning |
|---|---|---|---|
id * | path | string | Proxy user id (usr_…). |
Body: JSON WhitelistInput.
| Field | Type | Meaning |
|---|---|---|
ip * | string | IPv4/IPv6 address or CIDR block (at least /16 for IPv4, /48 for IPv6). |
note | string | Optional note (≤ 120). |
Response 201: WhitelistEntry — Created entry.
Errors — 401: unauthorized · 404: not_found · 409: ip_exists, ip_in_use · 422: invalid_ip, cidr_too_wide, whitelist_full
curl -s "https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB/whitelist" \
-X POST \
-H "Authorization: Bearer $NRTH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ip":"203.0.113.5","note":"office"}'r = requests.post("https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB/whitelist", headers=HEADERS, json={"ip": "203.0.113.5", "note": "office"}, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB/whitelist', { method: 'POST', headers: HEADERS, body: JSON.stringify({"ip":"203.0.113.5","note":"office"}) });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Remove a whitelisted IP
DELETE /api/v1/proxy-users/{id}/whitelist/{ip_id}
Removes one entry.
| Parameter | In | Type | Meaning |
|---|---|---|---|
id * | path | string | Proxy user id (usr_…). |
ip_id * | path | integer | Whitelist entry id. |
Response 204: JSON — Removed.
Errors — 401: unauthorized · 404: not_found
curl -s "https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB/whitelist/17" \
-X DELETE \
-H "Authorization: Bearer $NRTH_API_KEY"r = requests.delete("https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB/whitelist/17", headers=HEADERS, timeout=15)
r.raise_for_status()
print(r.status_code) # 204const r = await fetch('https://nrth.solutions/api/v1/proxy-users/usr_01JAN3V7K2Q9X8M4D6W1R5T0YB/whitelist/17', { method: 'DELETE', headers: HEADERS });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(r.status); // 204Generator
Plain-text proxy lists.
Generate a proxy list
GET /api/v1/generate
Plain text, one proxy per line, like a proxy generator. The gateway never stores secrets, so pass the proxy user’s secret as secret=; without it every line carries **** where the secret goes. Sticky lines get a random session name each; use session_name to fix one.
| Parameter | In | Type | Meaning |
|---|---|---|---|
proxy_user * | query | string | Proxy user id or username. |
secret | query | string | The proxy user’s secret. Omit to get **** placeholders. |
country | query | string | ISO 3166-1 alpha-2 country, e.g. TR. |
region | query | string | Region name (spaces become dots). |
city | query | string | City name (spaces become dots). |
session | query | rotating | sticky | hard | locked | rotating (new IP per request), sticky (_session- + lifetime), hard (_hardsession-), locked (_lockedsession-). |
session_name | query | string | Use this session name on every line instead of random ones. |
lifetime | query | integer | Sticky session lifetime in minutes (sticky only). |
protocol | query | http | socks5 | Scheme for the url format; the port is the same for both. |
format | query | url | userpass_hostport | hostport_userpass | userpass_hostport_colon | user:pass@host:port | host:port:user:pass | user:pass:host:port | Line format: url → http://user:pass@host:port, userpass_hostport → user:pass@host:port, hostport_userpass → host:port:user:pass, userpass_hostport_colon → user:pass:host:port. The templates themselves are accepted as aliases. |
amount | query | integer | Number of lines. |
params | query | string | Extra key-value tokens joined with _, e.g. isp-turkcell_device-windows (no session or lifetime keys). |
download | query | 1 | 1 adds a Content-Disposition attachment header (nrth-proxies.txt). |
Response 200: text/plain — Proxy list.
Errors — 401: unauthorized · 404: not_found · 422: invalid_session, invalid_protocol, invalid_format, invalid_amount, invalid_country, invalid_region, invalid_city, invalid_lifetime, lifetime_not_applicable, invalid_session_name, invalid_params, invalid_secret, targeting_not_allowed
curl -s "https://nrth.solutions/api/v1/generate?proxy_user=cus-7h2k9q&secret=aB3dE6fG9hJ2kL5m&country=TR&session=sticky&lifetime=30&amount=3" \
-H "Authorization: Bearer $NRTH_API_KEY"r = requests.get("https://nrth.solutions/api/v1/generate", headers=HEADERS, params={"proxy_user": "cus-7h2k9q", "secret": "aB3dE6fG9hJ2kL5m", "country": "TR", "session": "sticky", "lifetime": 30, "amount": 3}, timeout=15)
r.raise_for_status()
print(r.text)const r = await fetch('https://nrth.solutions/api/v1/generate?proxy_user=cus-7h2k9q&secret=aB3dE6fG9hJ2kL5m&country=TR&session=sticky&lifetime=30&amount=3', { headers: HEADERS });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.text());Sessions
Sticky session rotation.
Rotate a sticky session
POST /api/v1/sessions/{name}/rotate
Asks the network to drop the IP behind one of your session names so the next request gets a new one. Available only when the network supports rotation (supported: false otherwise — then simply use a new session name).
| Parameter | In | Type | Meaning |
|---|---|---|---|
name * | path | string | Your session name exactly as used in the password (_session-<name>). |
Body: JSON RotateInput.
| Field | Type | Meaning |
|---|---|---|
proxy_user * | string | Proxy user id or username that owns the session name. |
Response 200: RotateResult — Result.
Errors — 401: unauthorized · 404: not_found · 422: invalid_session_name, proxy_user_required · 502: rotate_failed
curl -s "https://nrth.solutions/api/v1/sessions/office1/rotate" \
-X POST \
-H "Authorization: Bearer $NRTH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"proxy_user":"cus-7h2k9q"}'r = requests.post("https://nrth.solutions/api/v1/sessions/office1/rotate", headers=HEADERS, json={"proxy_user": "cus-7h2k9q"}, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/sessions/office1/rotate', { method: 'POST', headers: HEADERS, body: JSON.stringify({"proxy_user":"cus-7h2k9q"}) });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Locations
Countries, regions and cities for targeting.
Locations
GET /api/v1/locations
All countries with localized names; region and city lists when available. With country= only that country’s lists. No key needed.
| Parameter | In | Type | Meaning |
|---|---|---|---|
lang | query | tr | en | Name language; default: your account language, else en. |
country | query | string | Return regions and cities of one country. |
Response 200: Locations / CountryLocations — Locations.
Errors — 401: invalid_api_key · 422: invalid_lang, invalid_country
curl -s "https://nrth.solutions/api/v1/locations?lang=en"r = requests.get("https://nrth.solutions/api/v1/locations", params={"lang": "en"}, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/locations?lang=en');
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Packages
What can be bought.
Packages
GET /api/v1/packages
Visible packages plus the payment methods accepted right now. No key needed.
Response 200: PackageList — Packages.
Errors — 401: invalid_api_key
curl -s "https://nrth.solutions/api/v1/packages"r = requests.get("https://nrth.solutions/api/v1/packages", timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/packages');
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Orders
Balance top-ups by bank transfer or crypto.
List orders
GET /api/v1/orders
Your orders, newest first.
| Parameter | In | Type | Meaning |
|---|---|---|---|
status | query | pending | paid | rejected | cancelled | Filter by status. |
limit | query | integer | Page size, 1–200. |
offset | query | integer | Skip this many rows. |
Response 200: OrderList — Orders.
Errors — 401: unauthorized · 422: invalid_status, invalid_limit, invalid_offset
curl -s "https://nrth.solutions/api/v1/orders?status=pending" \
-H "Authorization: Bearer $NRTH_API_KEY"r = requests.get("https://nrth.solutions/api/v1/orders", headers=HEADERS, params={"status": "pending"}, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/orders?status=pending', { headers: HEADERS });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Create an order
POST /api/v1/orders
Creates a pending order with a reference code and payment instructions. Write the reference in the transfer description; the balance is credited when support marks the order paid. At most 10 pending orders.
Body: JSON OrderInput.
| Field | Type | Meaning |
|---|---|---|
package_id * | string | A visible package id from GET /packages. |
method * | bank | crypto | One of payment_methods from GET /packages. |
note | string | Optional note to support (≤ 500). |
Response 201: Order — Pending order with instructions.
Errors — 401: unauthorized · 403: account_banned · 404: package_not_found · 422: invalid_method, method_not_offered, too_many_pending_orders, package_id_required
curl -s "https://nrth.solutions/api/v1/orders" \
-X POST \
-H "Authorization: Bearer $NRTH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"package_id":"pkg_01JAN3V7K2Q9X8M4D6W1R5T0YE","method":"bank"}'r = requests.post("https://nrth.solutions/api/v1/orders", headers=HEADERS, json={"package_id": "pkg_01JAN3V7K2Q9X8M4D6W1R5T0YE", "method": "bank"}, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/orders', { method: 'POST', headers: HEADERS, body: JSON.stringify({"package_id":"pkg_01JAN3V7K2Q9X8M4D6W1R5T0YE","method":"bank"}) });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Get an order
GET /api/v1/orders/{id}
One order with its instructions while pending.
| Parameter | In | Type | Meaning |
|---|---|---|---|
id * | path | string | Order id (ord_…). |
Response 200: Order — Order.
Errors — 401: unauthorized · 404: not_found
curl -s "https://nrth.solutions/api/v1/orders/ord_01JAN3V7K2Q9X8M4D6W1R5T0YC" \
-H "Authorization: Bearer $NRTH_API_KEY"r = requests.get("https://nrth.solutions/api/v1/orders/ord_01JAN3V7K2Q9X8M4D6W1R5T0YC", headers=HEADERS, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/orders/ord_01JAN3V7K2Q9X8M4D6W1R5T0YC', { headers: HEADERS });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Cancel a pending order
POST /api/v1/orders/{id}/cancel
Only pending orders can be cancelled.
| Parameter | In | Type | Meaning |
|---|---|---|---|
id * | path | string | Order id (ord_…). |
Response 200: Order — Cancelled order.
Errors — 401: unauthorized · 404: not_found · 409: order_not_pending
curl -s "https://nrth.solutions/api/v1/orders/ord_01JAN3V7K2Q9X8M4D6W1R5T0YC/cancel" \
-X POST \
-H "Authorization: Bearer $NRTH_API_KEY"r = requests.post("https://nrth.solutions/api/v1/orders/ord_01JAN3V7K2Q9X8M4D6W1R5T0YC/cancel", headers=HEADERS, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/orders/ord_01JAN3V7K2Q9X8M4D6W1R5T0YC/cancel', { method: 'POST', headers: HEADERS });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Support
Tickets and replies.
List tickets
GET /api/v1/tickets
Your tickets, latest activity first, without messages.
| Parameter | In | Type | Meaning |
|---|---|---|---|
status | query | open | answered | closed | Filter by status. |
limit | query | integer | Page size, 1–200. |
offset | query | integer | Skip this many rows. |
Response 200: TicketList — Tickets.
Errors — 401: unauthorized · 422: invalid_status, invalid_limit, invalid_offset
curl -s "https://nrth.solutions/api/v1/tickets?status=open" \
-H "Authorization: Bearer $NRTH_API_KEY"r = requests.get("https://nrth.solutions/api/v1/tickets", headers=HEADERS, params={"status": "open"}, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/tickets?status=open', { headers: HEADERS });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Open a ticket
POST /api/v1/tickets
Opens a support ticket with a first message. At most 20 open tickets.
Body: JSON TicketInput.
| Field | Type | Meaning |
|---|---|---|
subject * | string | ≤ 120 characters. |
body * | string | First message, plain text, ≤ 5000. |
priority | normal | high | Default normal. |
Response 201: Ticket — The ticket with its first message.
Errors — 401: unauthorized · 422: subject_required, subject_too_long, body_required, body_too_long, invalid_priority, too_many_open_tickets
curl -s "https://nrth.solutions/api/v1/tickets" \
-X POST \
-H "Authorization: Bearer $NRTH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"subject":"Session IPs change too often","body":"With session-office1 and lifetime 30 the IP changes every few minutes since yesterday.","priority":"normal"}'r = requests.post("https://nrth.solutions/api/v1/tickets", headers=HEADERS, json={"subject": "Session IPs change too often", "body": "With session-office1 and lifetime 30 the IP changes every few minutes since yesterday.", "priority": "normal"}, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/tickets', { method: 'POST', headers: HEADERS, body: JSON.stringify({"subject":"Session IPs change too often","body":"With session-office1 and lifetime 30 the IP changes every few minutes since yesterday.","priority":"normal"}) });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Get a ticket
GET /api/v1/tickets/{id}
One ticket with all its messages, oldest first.
| Parameter | In | Type | Meaning |
|---|---|---|---|
id * | path | string | Ticket id (tck_…). |
Response 200: Ticket — Ticket with messages.
Errors — 401: unauthorized · 404: not_found
curl -s "https://nrth.solutions/api/v1/tickets/tck_01JAN3V7K2Q9X8M4D6W1R5T0YD" \
-H "Authorization: Bearer $NRTH_API_KEY"r = requests.get("https://nrth.solutions/api/v1/tickets/tck_01JAN3V7K2Q9X8M4D6W1R5T0YD", headers=HEADERS, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/tickets/tck_01JAN3V7K2Q9X8M4D6W1R5T0YD', { headers: HEADERS });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Reply to a ticket
POST /api/v1/tickets/{id}/messages
Adds a message; the ticket goes back to open (a closed ticket reopens).
| Parameter | In | Type | Meaning |
|---|---|---|---|
id * | path | string | Ticket id (tck_…). |
Body: JSON TicketReplyInput.
| Field | Type | Meaning |
|---|---|---|
body * | string | Plain text, ≤ 5000. Replying to a closed ticket reopens it. |
Response 201: Ticket — Ticket with messages.
Errors — 401: unauthorized · 404: not_found · 422: body_required, body_too_long
curl -s "https://nrth.solutions/api/v1/tickets/tck_01JAN3V7K2Q9X8M4D6W1R5T0YD/messages" \
-X POST \
-H "Authorization: Bearer $NRTH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"body":"Still happening after switching to hardsession."}'r = requests.post("https://nrth.solutions/api/v1/tickets/tck_01JAN3V7K2Q9X8M4D6W1R5T0YD/messages", headers=HEADERS, json={"body": "Still happening after switching to hardsession."}, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/tickets/tck_01JAN3V7K2Q9X8M4D6W1R5T0YD/messages', { method: 'POST', headers: HEADERS, body: JSON.stringify({"body":"Still happening after switching to hardsession."}) });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Close a ticket
POST /api/v1/tickets/{id}/close
Marks the ticket closed; replying reopens it.
| Parameter | In | Type | Meaning |
|---|---|---|---|
id * | path | string | Ticket id (tck_…). |
Response 200: Ticket — Closed ticket.
Errors — 401: unauthorized · 404: not_found
curl -s "https://nrth.solutions/api/v1/tickets/tck_01JAN3V7K2Q9X8M4D6W1R5T0YD/close" \
-X POST \
-H "Authorization: Bearer $NRTH_API_KEY"r = requests.post("https://nrth.solutions/api/v1/tickets/tck_01JAN3V7K2Q9X8M4D6W1R5T0YD/close", headers=HEADERS, timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/tickets/tck_01JAN3V7K2Q9X8M4D6W1R5T0YD/close', { method: 'POST', headers: HEADERS });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Meta
Machine-readable description of this API.
OpenAPI document
GET /api/v1/openapi.json
This API as OpenAPI 3.1 — generate a client or import it into your HTTP tool. No key needed.
Response 200: JSON — OpenAPI.
curl -s "https://nrth.solutions/api/v1/openapi.json"r = requests.get("https://nrth.solutions/api/v1/openapi.json", timeout=15)
r.raise_for_status()
print(r.json())const r = await fetch('https://nrth.solutions/api/v1/openapi.json');
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());Objects
Fields marked * are always present.
Problem
RFC 9457 problem details. Content-Type: application/problem+json.
| Field | Type | Meaning |
|---|---|---|
type * | string | URI of the error section of the docs, with the code as fragment. |
title * | string | Short English title. |
status * | integer | HTTP status, repeated in the body. |
detail * | string | One sentence explaining what went wrong. |
instance | string | The request path. |
code * | string | Stable snake_case error code — branch on this, never on detail. |
details | object (map) | null | Extra data for some codes, e.g. { "key": "mode" } for invalid_params. |
Account
| Field | Type | Meaning |
|---|---|---|
id * | string | Account id. |
email * | string | Login e-mail. |
name | string | Display name. |
lang | tr | en | Panel language. |
status * | active | suspended | active or suspended; closed accounts cannot use the API. |
balance_bytes * | integer | Remaining balance in bytes (may be slightly negative after a long transfer). |
balance_gb * | number | Remaining balance in GB (1 GB = 1024³ bytes), 3 decimals. |
balance_expires_at * | timestamp (ISO 8601) | null | When the balance stops working; null = never. |
balance_state * | ok | empty | expired | ok, empty (≤ 0) or expired. |
concurrency_limit * | integer | null | Maximum simultaneous connections across all proxy users; null = unlimited. |
proxy_user_count | integer | Number of proxy users. |
active_connections | integer | Connections open right now. |
totp_enabled | boolean | Two-factor login enabled. |
email_verified | boolean | E-mail address verified. |
api_key_hint | string | null | Last 4 characters of the current API key. |
created_at | timestamp (ISO 8601) | Registration time. |
last_login_at | timestamp (ISO 8601) | null | Last panel login. |
last_seen_at | timestamp (ISO 8601) | null | Last proxy traffic. |
UsageRow
| Field | Type | Meaning |
|---|---|---|
day * | string | Istanbul calendar day, YYYY-MM-DD. |
proxy_user * | string | Proxy user id (the row of a deleted proxy user keeps its id). |
username | string | null | Proxy username, null when the proxy user was deleted. |
bytes_in * | integer | Bytes received from the targets. |
bytes_out * | integer | Bytes sent to the targets. |
bytes * | integer | bytes_in + bytes_out — what is charged. |
gb * | number | bytes / 1024³, 3 decimals. |
requests * | integer | Connections / requests counted. |
UsageTotals
| Field | Type | Meaning |
|---|---|---|
bytes_in * | integer | Sum. |
bytes_out * | integer | Sum. |
bytes * | integer | Sum. |
gb * | number | Sum in GB. |
requests * | integer | Sum. |
UsageResponse
| Field | Type | Meaning |
|---|---|---|
from * | string | First day of the range (inclusive). |
to * | string | Last day of the range (inclusive). |
rows * | array of UsageRow | One row per day and proxy user with traffic; days without traffic are omitted. |
totals * | UsageTotals |
ProxyUserUsage
| Field | Type | Meaning |
|---|---|---|
today * | integer | Bytes today (Istanbul day). |
d7 * | integer | Bytes in the last 7 days. |
d30 * | integer | Bytes in the last 30 days. |
ProxyUser
| Field | Type | Meaning |
|---|---|---|
id * | string | Proxy user id. |
username * | string | The proxy username (3–32, a-z 0-9 . _ -). |
label | string | Free text label. |
secret_hint | string | First 3 characters of the secret (the secret itself is shown only once). |
enabled * | boolean | Disabled proxy users are refused at the gateway. |
auth_mode * | password | ip | password: credentials or a whitelisted IP. ip: whitelisted IPs only, credentials refused. |
status * | active | disabled | expired | quota_exhausted | Computed state. |
blocked_by | user_disabled | customer_inactive | user_expired | balance_expired | quota_exhausted | balance_exhausted | null | Why the gateway would refuse a connection right now, or null. |
quota_bytes * | integer | null | Per-proxy-user cap in bytes; null = only the account balance limits. |
quota_gb | number | null | quota_bytes in GB. |
used_bytes * | integer | Bytes used since the last quota reset (including in-flight bytes). |
used_gb | number | used_bytes in GB. |
quota_ratio | number | null | used / quota, 0–1; null without a quota. |
expires_at | timestamp (ISO 8601) | null | Proxy user expiry; null = never. |
max_connections | integer | null | Simultaneous connection cap for this proxy user; null = unlimited. |
allow_targeting * | boolean | Whether _key-value parameters are accepted in the password. |
default_params | string | null | Parameters applied when the password carries no geo token, e.g. country-TR. |
default_country | string | null | The country code inside default_params, if any. |
active_connections | integer | Connections open right now. |
usage | ProxyUserUsage | |
created_at * | timestamp (ISO 8601) | Creation time. |
last_seen_at | timestamp (ISO 8601) | null | Last proxy traffic. |
ProxyUserInput
| Field | Type | Meaning |
|---|---|---|
username | string | Optional; a cus-xxxxxx name is generated when omitted. Usernames are global, 3–32 characters, a-z 0-9 . _ -. |
label | string | Free text label (≤ 64). |
note | string | Private note (≤ 500). |
quota_gb | number | null | Per-proxy-user cap in GB; null or omitted = unlimited (the account balance still limits). |
max_connections | integer | null | Simultaneous connection cap; null = unlimited. |
allow_targeting | boolean | Default true. When false every _key-value parameter is refused. |
default_country | string | ISO 3166-1 alpha-2; becomes default_params: country-XX. |
default_params | string | Full default parameter string (country-TR_city-istanbul); overrides default_country. No session keys. |
auth_mode | password | ip | password (default) or ip. |
enabled | boolean | Default true. |
ProxyUserPatch
| Field | Type | Meaning |
|---|---|---|
label | string | Label. |
note | string | Note. |
enabled | boolean | Enable / disable (open connections are cut on disable). |
quota_gb | number | null | New cap in GB; null = unlimited. |
max_connections | integer | null | null = unlimited. |
allow_targeting | boolean | Targeting permission. |
default_country | string | null | ISO code or null to clear. |
default_params | string | null | Parameter string or null to clear. |
auth_mode | password | ip | password or ip. |
expires_at | timestamp (ISO 8601) | null | Expiry, or null for never. |
ProxyUserCreated
| Field | Type | Meaning |
|---|---|---|
proxy_user * | ProxyUser | |
secret * | string | The proxy password. Shown only in this response — store it now. |
host * | string | Gateway host. |
port * | integer | Gateway port (HTTP and SOCKS5). |
url * | string | Ready-to-use http://user:secret@host:port. |
ProxyUserList
| Field | Type | Meaning |
|---|---|---|
items * | array of ProxyUser | All proxy users of the account, username order. |
total * | integer | Count. |
WhitelistEntry
| Field | Type | Meaning |
|---|---|---|
id * | integer | Entry id (use it to delete). |
proxy_user * | string | Owning proxy user. |
ip * | string | Normalized address or CIDR block. |
note | string | Note. |
created_at * | timestamp (ISO 8601) | Creation time. |
WhitelistInput
| Field | Type | Meaning |
|---|---|---|
ip * | string | IPv4/IPv6 address or CIDR block (at least /16 for IPv4, /48 for IPv6). |
note | string | Optional note (≤ 120). |
WhitelistList
| Field | Type | Meaning |
|---|---|---|
items * | array of WhitelistEntry | Entries of this proxy user. |
total * | integer | Count. |
RotateInput
| Field | Type | Meaning |
|---|---|---|
proxy_user * | string | Proxy user id or username that owns the session name. |
RotateResult
| Field | Type | Meaning |
|---|---|---|
supported * | boolean | false when rotation is not available on the network right now; just change the session name instead. |
ok * | boolean | true when the network dropped the session. |
session | string | null | Namespaced session id as seen by the network (never your name). |
Country
| Field | Type | Meaning |
|---|---|---|
code * | string | ISO 3166-1 alpha-2. |
name * | string | Name in the requested language. |
Locations
| Field | Type | Meaning |
|---|---|---|
count * | integer | Number of countries. |
countries * | array of Country | Every country, sorted by name. |
regions_available * | boolean | Whether region and city lists are available right now. |
regions * | object (map) | Region names per country code (values usable as region-…). |
cities * | object (map) | City names per country code (values usable as city-…). |
fetched_at | timestamp (ISO 8601) | null | When the region/city lists were last refreshed. |
CountryLocations
| Field | Type | Meaning |
|---|---|---|
country * | Country | |
regions_available * | boolean | As above. |
regions * | array of string | Regions of this country. |
cities * | array of string | Cities of this country. |
Package
| Field | Type | Meaning |
|---|---|---|
id * | string | Package id. |
name * | object | Name. |
description | object | Description. |
gb * | number | GB credited when paid. |
price * | number | Price. |
currency * | TRY | USD | TRY or USD. |
price_per_gb | number | null | price / gb. |
validity_days * | integer | Days added to the balance expiry when paid (0 = no change). |
concurrency_limit | integer | null | Concurrency limit applied to the account when paid; null = not changed. |
featured | boolean | Highlighted on the pricing page. |
PackageList
| Field | Type | Meaning |
|---|---|---|
items * | array of Package | Visible packages in display order. |
payment_methods * | array of bank | crypto | Methods currently accepted by POST /orders. |
currencies | array of string | Currencies in use. |
Order
| Field | Type | Meaning |
|---|---|---|
id * | string | Order id. |
reference * | string | Short code to write in the transfer description, e.g. NRTH-7K3Q2. |
status * | pending | paid | rejected | cancelled | Lifecycle state. |
method * | bank | crypto | manual | Payment method. |
package_id | string | null | Package, null for manual credits. |
package_name | object | null | Package name snapshot. |
gb * | number | GB credited when paid. |
price * | number | Price. |
currency * | string | Currency. |
validity_days | integer | Validity added when paid. |
customer_note | string | Your note. |
instructions | string | Payment instructions for the chosen method (pending orders only). |
created_at * | timestamp (ISO 8601) | Creation time. |
paid_at | timestamp (ISO 8601) | null | When it was marked paid. |
handled_at | timestamp (ISO 8601) | null | When it was paid, rejected or cancelled. |
OrderInput
| Field | Type | Meaning |
|---|---|---|
package_id * | string | A visible package id from GET /packages. |
method * | bank | crypto | One of payment_methods from GET /packages. |
note | string | Optional note to support (≤ 500). |
OrderList
| Field | Type | Meaning |
|---|---|---|
items * | array of Order | Newest first. |
total * | integer | Total matching orders. |
limit * | integer | Page size. |
offset * | integer | Offset. |
TicketMessage
| Field | Type | Meaning |
|---|---|---|
id * | integer | Message id. |
author_type * | customer | admin | customer or admin (support). |
body * | string | Plain text with line breaks. |
created_at * | timestamp (ISO 8601) | Time. |
Ticket
| Field | Type | Meaning |
|---|---|---|
id * | string | Ticket id. |
subject * | string | Subject. |
status * | open | answered | closed | open (waiting for support), answered, closed. |
priority * | normal | high | Priority. |
last_reply_by | customer | admin | Who wrote last. |
message_count | integer | Number of messages. |
messages | array of TicketMessage | Oldest first; only in single-ticket responses. |
created_at * | timestamp (ISO 8601) | Creation time. |
updated_at * | timestamp (ISO 8601) | Last activity. |
TicketInput
| Field | Type | Meaning |
|---|---|---|
subject * | string | ≤ 120 characters. |
body * | string | First message, plain text, ≤ 5000. |
priority | normal | high | Default normal. |
TicketReplyInput
| Field | Type | Meaning |
|---|---|---|
body * | string | Plain text, ≤ 5000. Replying to a closed ticket reopens it. |
TicketList
| Field | Type | Meaning |
|---|---|---|
items * | array of Ticket | Latest activity first (without messages). |
total * | integer | Total. |
limit * | integer | Page size. |
offset * | integer | Offset. |
The same information, machine-readable: https://nrth.solutions/api/v1/openapi.json (OpenAPI 3.1).
08Limits and fair use
There are no rate limits on the gateway or the API, no connection caps unless you set them yourself, and no expiry games: a package adds GB and days, both are visible on /me at all times. The hard limits that do exist:
| What | Limit |
|---|---|
| Proxy users per account | 200 |
| Whitelist entries per proxy user | 50 (blocks at least /16 IPv4, /48 IPv6) |
| Session name | 1–64 characters (A-Z a-z 0-9 . , -) |
lifetime | 1–1440 minutes |
| Generator lines per call | 1000 |
| Minimum per connection | 512 bytes counted, even for a tiny request |
| Usage history | daily rows kept 400 days |
| Pending orders | 10 |
| Open tickets | 20 (subject 120, message 5000 characters) |
| API request body | 64 KB |
| Idle connection | closed after 120 s without traffic |
Fair use
The exits are real residential connections. Use them for what they are good at — seeing the web the way a person in that country sees it — and nothing that would hurt the people behind them: no attacks, no credential stuffing, no spam, no child abuse material, no fraud, nothing illegal where you or the target are. We cut access without refund when we see it. Traffic that counts as abuse by the network’s own rules (for example port scanning or mail relaying) is blocked at the exit regardless.
Billing is bytes in both directions, after the proxy handshake, with a 512-byte floor per connection. Retries, redirects and large pages all count; a sticky session does not. Balances can dip slightly below zero on a long transfer; the next package tops them up from there.
Connection records
As required by law, the metadata of every proxy connection — time, client IP address and port, destination host and port (the requested address for unencrypted HTTP), bytes transferred and request count — is retained for the legally required period with integrity protection. Transferred content is not recorded. These records are disclosed only upon a lawful request from a competent authority and are not shown in the customer panel.