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.

  1. 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.

  2. 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"}'
  3. Connect

    Point your tool at tunnel.nrth.solutions:1337 with the username and the secret. HTTP and SOCKS5 share the port — the gateway detects the protocol. Append _country-XX to the secret to choose a country and _session-name to 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.

SettingValue
Hosttunnel.nrth.solutions
Port1337 (HTTP and SOCKS5)
Usernamethe proxy username, e.g. cus-7h2k9q
Passwordthe secret, optionally followed by parameters: aB3dE6fG9hJ2kL5m_country-TR_session-office1
Target DNSResolved 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:pass keeps 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).
  • -v prints the proxy handshake: a 407 means wrong credentials, a 403 carries the reason in its body (see Errors).
  • Use socks5h://, not socks5://: with socks5h the hostname is resolved by the network, so geo-restricted sites see a matching DNS resolver.
  • --connect-timeout 15 --max-time 60 are 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), then Timeout, then HTTPError from the target.
  • For retries use HTTPAdapter(max_retries=Retry(total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504])).
  • Each Session reuses 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; the auth object form needs no encoding.
  • The global fetch in Node ignores HTTP_PROXY; pass a dispatcher as above, or setGlobalDispatcher(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 each newContext() — 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. the proxy-chain package’s anonymizeProxy) or use the IP whitelist and drop the credentials.
  • headless: false is 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.ssl to 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 HttpProxyMiddleware turns the credentials in the URL into the Proxy-Authorization header; 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 in process_request.
  • SOCKS5 needs the scrapy-socks download handlers and a socks5h:// 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:

  1. 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, port 1337. Only Wi-Fi, and only apps that honour the system proxy (browsers do; many apps do not).
  2. 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, port 1337, 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.

TokenMeaningNotes
country-XXISO 3166-1 alpha-2, upper case: country-TR, country-US. The full list: GET /api/v1/locations.Any proxy user with targeting allowed
region-nameRegion / state, lower case, spaces as dots: region-california. Combine with country.Names from /locations when lists are available
city-nameCity, lower case, spaces as dots: city-istanbul, city-new.york. Combine with country; an unavailable city answers 400.Same
continent-nameafrica, asia, europe, north.america, oceania, south.america.
session-nameSticky 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-nameKeeps the IP as long as the device stays online — no lifetime, no automatic change.No lifetime
lockedsession-nameLocked to one IP, never substituted: when that IP goes offline the request fails instead of moving.No lifetime
lifetime-30Minutes a session keeps its IP: 1–1440, default 30. Out of range → 400.Only with session
isp-codeOperator / carrier shortcode, lower case (e.g. isp-turkcell). Codes vary per country; ask support for the current list.Expert
asn-AS15169Autonomous system, AS + digits.Expert
zip-90210Postal code, letters and digits without spaces (zip-SW1A1AA, zip-10115).Expert
geosource-maxmindWhich geolocation database decides what counts as "in TR": ipapi (default) or maxmind, lower case. Use the one your target site uses.Expert
latency-500Maximum round-trip to the exit, in ms. Below 300 the pool gets very small.Expert; not with extended
fraudscore-5Maximum reputation score of the exit: 0, 5 or 10 (0 = cleanest IPs only).Expert; not with extended
device-windowsExit device’s TCP fingerprint: windows, unix (Linux/Android) or apple (iOS/macOS).Expert; not with extended
activesince-60Exit must have been online for at least this many minutes (60–240 is the useful range; above 300 the pool shrinks sharply).Expert
extended-1Opens the extended pool (several times larger, more variance). Cannot be combined with other expert parameters.Expert
adblock-1Blocks 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, or country-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.

TypeTokenBehaviourEnds when
Rotating(none)New IP on every connection.—
Stickysession-name + lifetime-NSame 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.
Hardhardsession-nameSame IP as long as the exit device is online; no automatic change.When the exit goes offline, or you change the name.
Lockedlockedsession-nameOne 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 office1 of your proxy user to an internal id; another customer using office1 lands on a different IP, and the same name always maps to the same session for you.
  • To rotate, change the name. session-a1 → session-a2 is an instant new IP; no API call needed. Generating a list with session=sticky gives you one random name per line for exactly this.
  • Forced rotation of a live name: POST /api/v1/sessions/{name}/rotate asks the network to drop the IP behind the name. It answers supported: false while 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-Authorization header. A whitelisted address is treated as that proxy user; anything else gets 407.
  • SOCKS5: offer the "no authentication" method (0x00). It is accepted only for a whitelisted address; otherwise the gateway answers 0xFF (no acceptable method) and closes.
  • Entries are single addresses (203.0.113.5, 2001:db8::7) or blocks: at least /16 for IPv4 and /48 for 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-value tokens: set the proxy user’s default parameters (country-TR) for targeting. Sessions are not available this way.
  • auth_mode: ip makes 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

StatusBodyMeaning / 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.
403NRTH: bakiye bitti / balance exhaustedAccount balance is 0 — buy a package.
403NRTH: bakiye süresi doldu / balance expiredThe balance validity passed; a new package extends it.
403NRTH: kota doldu / quota exhaustedThis proxy user’s own GB cap is used up; raise it or reset it in the panel.
403NRTH: hesap kapalı / account disabledThe proxy user is disabled.
403NRTH: hesap süresi doldu / account expiredThe proxy user’s expiry date passed.
403NRTH: hesap askıda / account suspendedThe account is suspended; check your tickets.
403NRTH: çok fazla bağlantı / too many connectionsThe proxy user’s max_connections is reached.
403NRTH: eşzamanlı bağlantı sınırı / concurrency limit reachedThe account-wide concurrency limit is reached.
400NRTH: 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.
400NRTH: geçersiz proxy parametresi / invalid proxy parameterThe network could not satisfy the combination (e.g. an unknown city).
400NRTH: geçersiz istek / bad requestNot a proxy request (a bare path instead of CONNECT or an absolute URL).
502NRTH: üst bağlantı kurulamadı / upstream unavailableThe network could not connect to the target or timed out — retry, or try another exit (new session name).
502NRTH: gateway upstream auth errorA problem on our side between the gateway and the network. Retry in a minute; if it persists, open a ticket.

SOCKS5

StageCodeMeaning
Method selection0xFFNo acceptable method: you offered no username/password and your address is not whitelisted.
Authentication0x01Wrong username or secret (or the proxy user is in ip mode).
Reply0x00Connected.
Reply0x02Not allowed: any of the 403 reasons above, or a refused parameter (the 400 reasons).
Reply0x05Connection refused by the network or the target.
Reply0x01General failure (target unreachable, timeout).
Reply0x07Only CONNECT is supported — no BIND, no UDP ASSOCIATE.
Reply0x08Unsupported 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"
}
CodeHTTPMeaning
unauthorized401Send your API key as Authorization: Bearer <key>. Keys are created on the API page of the customer panel.
invalid_api_key401The API key is unknown, was regenerated, or belongs to a closed account.
forbidden403This action is not allowed for your account.
account_banned403This account is closed.
customer_inactive403The account is suspended; proxy users cannot be created while it is suspended.
not_found404No such resource, or it belongs to another account.
no_such_endpoint404The path or method does not exist in this API. See /api/v1/openapi.json.
invalid_json400The request body is not valid JSON.
payload_too_large413The request body exceeds 64 KB.
unsupported_media_type415Send request bodies as application/json.
invalid_query422A query parameter has an invalid value.
validation422A field has an invalid value.
internal500Something went wrong on our side. The incident is logged; try again or open a ticket.
username_taken409Pick another proxy username; usernames are global.
invalid_username422Usernames are 3–32 characters: a-z 0-9 . _ -.
too_many_proxy_users422An account can have at most 200 proxy users.
invalid_default_params422Default parameters use the credential grammar without session keys, e.g. country-TR.
invalid_ip422Give an IPv4/IPv6 address or a CIDR block.
cidr_too_wide422Blocks must be /16 or narrower for IPv4 and /48 or narrower for IPv6.
whitelist_full422A proxy user can have at most 50 whitelist entries.
ip_exists409This address is already on the whitelist of this proxy user.
ip_in_use409This address is registered to another proxy user; use credentials instead.
invalid_session_name422Session names are 1–64 characters: A-Z a-z 0-9 . , -.
rotate_failed502The network did not accept the rotation request; retry in a moment.
package_not_found404No visible package with that id.
invalid_method422method must be bank or crypto.
method_not_offered422This payment method is not currently offered.
too_many_pending_orders422At most 10 orders can be pending; cancel one or wait for it to be handled.
order_not_pending409Only pending orders can be cancelled.
subject_required422A ticket needs a subject.
subject_too_long422Subjects are at most 120 characters.
body_required422A message body is required.
body_too_long422Messages are at most 5000 characters.
invalid_priority422priority must be normal or high.
too_many_open_tickets422At most 20 tickets can be open at once.
invalid_day422Days are written YYYY-MM-DD.
targeting_not_allowed422This proxy user has targeting disabled; country, session and other parameters are refused.
invalid_secret422The proxy secret is 16 letters and digits with no underscore.
invalid_params422A 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.

ParameterInTypeMeaning
fromquerystringFirst day, YYYY-MM-DD.
toquerystringLast day (inclusive), YYYY-MM-DD.
proxy_userquerystringProxy 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).

FieldTypeMeaning
usernamestringOptional; a cus-xxxxxx name is generated when omitted. Usernames are global, 3–32 characters, a-z 0-9 . _ -.
labelstringFree text label (≤ 64).
notestringPrivate note (≤ 500).
quota_gbnumber | nullPer-proxy-user cap in GB; null or omitted = unlimited (the account balance still limits).
max_connectionsinteger | nullSimultaneous connection cap; null = unlimited.
allow_targetingbooleanDefault true. When false every _key-value parameter is refused.
default_countrystringISO 3166-1 alpha-2; becomes default_params: country-XX.
default_paramsstringFull default parameter string (country-TR_city-istanbul); overrides default_country. No session keys.
auth_modepassword | ippassword (default) or ip.
enabledbooleanDefault 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.

ParameterInTypeMeaning
id *pathstringProxy 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.

ParameterInTypeMeaning
id *pathstringProxy user id (usr_…).

Body: JSON ProxyUserPatch.

FieldTypeMeaning
labelstringLabel.
notestringNote.
enabledbooleanEnable / disable (open connections are cut on disable).
quota_gbnumber | nullNew cap in GB; null = unlimited.
max_connectionsinteger | nullnull = unlimited.
allow_targetingbooleanTargeting permission.
default_countrystring | nullISO code or null to clear.
default_paramsstring | nullParameter string or null to clear.
auth_modepassword | ippassword or ip.
expires_attimestamp (ISO 8601) | nullExpiry, 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.

ParameterInTypeMeaning
id *pathstringProxy 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)  # 204
const 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); // 204

Rotate the secret

POST /api/v1/proxy-users/{id}/rotate-secret

Generates a new secret and returns it once; the old one stops working immediately.

ParameterInTypeMeaning
id *pathstringProxy 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.

ParameterInTypeMeaning
id *pathstringProxy 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.

ParameterInTypeMeaning
id *pathstringProxy user id (usr_…).

Body: JSON WhitelistInput.

FieldTypeMeaning
ip *stringIPv4/IPv6 address or CIDR block (at least /16 for IPv4, /48 for IPv6).
notestringOptional 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.

ParameterInTypeMeaning
id *pathstringProxy user id (usr_…).
ip_id *pathintegerWhitelist 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)  # 204
const 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); // 204

Generator

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.

ParameterInTypeMeaning
proxy_user *querystringProxy user id or username.
secretquerystringThe proxy user’s secret. Omit to get **** placeholders.
countryquerystringISO 3166-1 alpha-2 country, e.g. TR.
regionquerystringRegion name (spaces become dots).
cityquerystringCity name (spaces become dots).
sessionqueryrotating | sticky | hard | lockedrotating (new IP per request), sticky (_session- + lifetime), hard (_hardsession-), locked (_lockedsession-).
session_namequerystringUse this session name on every line instead of random ones.
lifetimequeryintegerSticky session lifetime in minutes (sticky only).
protocolqueryhttp | socks5Scheme for the url format; the port is the same for both.
formatqueryurl | userpass_hostport | hostport_userpass | userpass_hostport_colon | user:pass@host:port | host:port:user:pass | user:pass:host:portLine 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.
amountqueryintegerNumber of lines.
paramsquerystringExtra key-value tokens joined with _, e.g. isp-turkcell_device-windows (no session or lifetime keys).
downloadquery11 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).

ParameterInTypeMeaning
name *pathstringYour session name exactly as used in the password (_session-<name>).

Body: JSON RotateInput.

FieldTypeMeaning
proxy_user *stringProxy 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.

ParameterInTypeMeaning
langquerytr | enName language; default: your account language, else en.
countryquerystringReturn 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.

ParameterInTypeMeaning
statusquerypending | paid | rejected | cancelledFilter by status.
limitqueryintegerPage size, 1–200.
offsetqueryintegerSkip 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.

FieldTypeMeaning
package_id *stringA visible package id from GET /packages.
method *bank | cryptoOne of payment_methods from GET /packages.
notestringOptional 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.

ParameterInTypeMeaning
id *pathstringOrder 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.

ParameterInTypeMeaning
id *pathstringOrder 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.

ParameterInTypeMeaning
statusqueryopen | answered | closedFilter by status.
limitqueryintegerPage size, 1–200.
offsetqueryintegerSkip 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.

FieldTypeMeaning
subject *string≤ 120 characters.
body *stringFirst message, plain text, ≤ 5000.
prioritynormal | highDefault 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.

ParameterInTypeMeaning
id *pathstringTicket 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).

ParameterInTypeMeaning
id *pathstringTicket id (tck_…).

Body: JSON TicketReplyInput.

FieldTypeMeaning
body *stringPlain 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.

ParameterInTypeMeaning
id *pathstringTicket 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.

FieldTypeMeaning
type *stringURI of the error section of the docs, with the code as fragment.
title *stringShort English title.
status *integerHTTP status, repeated in the body.
detail *stringOne sentence explaining what went wrong.
instancestringThe request path.
code *stringStable snake_case error code — branch on this, never on detail.
detailsobject (map) | nullExtra data for some codes, e.g. { "key": "mode" } for invalid_params.

Account

FieldTypeMeaning
id *stringAccount id.
email *stringLogin e-mail.
namestringDisplay name.
langtr | enPanel language.
status *active | suspendedactive or suspended; closed accounts cannot use the API.
balance_bytes *integerRemaining balance in bytes (may be slightly negative after a long transfer).
balance_gb *numberRemaining balance in GB (1 GB = 1024³ bytes), 3 decimals.
balance_expires_at *timestamp (ISO 8601) | nullWhen the balance stops working; null = never.
balance_state *ok | empty | expiredok, empty (≤ 0) or expired.
concurrency_limit *integer | nullMaximum simultaneous connections across all proxy users; null = unlimited.
proxy_user_countintegerNumber of proxy users.
active_connectionsintegerConnections open right now.
totp_enabledbooleanTwo-factor login enabled.
email_verifiedbooleanE-mail address verified.
api_key_hintstring | nullLast 4 characters of the current API key.
created_attimestamp (ISO 8601)Registration time.
last_login_attimestamp (ISO 8601) | nullLast panel login.
last_seen_attimestamp (ISO 8601) | nullLast proxy traffic.

UsageRow

FieldTypeMeaning
day *stringIstanbul calendar day, YYYY-MM-DD.
proxy_user *stringProxy user id (the row of a deleted proxy user keeps its id).
usernamestring | nullProxy username, null when the proxy user was deleted.
bytes_in *integerBytes received from the targets.
bytes_out *integerBytes sent to the targets.
bytes *integerbytes_in + bytes_out — what is charged.
gb *numberbytes / 1024³, 3 decimals.
requests *integerConnections / requests counted.

UsageTotals

FieldTypeMeaning
bytes_in *integerSum.
bytes_out *integerSum.
bytes *integerSum.
gb *numberSum in GB.
requests *integerSum.

UsageResponse

FieldTypeMeaning
from *stringFirst day of the range (inclusive).
to *stringLast day of the range (inclusive).
rows *array of UsageRowOne row per day and proxy user with traffic; days without traffic are omitted.
totals *UsageTotals

ProxyUserUsage

FieldTypeMeaning
today *integerBytes today (Istanbul day).
d7 *integerBytes in the last 7 days.
d30 *integerBytes in the last 30 days.

ProxyUser

FieldTypeMeaning
id *stringProxy user id.
username *stringThe proxy username (3–32, a-z 0-9 . _ -).
labelstringFree text label.
secret_hintstringFirst 3 characters of the secret (the secret itself is shown only once).
enabled *booleanDisabled proxy users are refused at the gateway.
auth_mode *password | ippassword: credentials or a whitelisted IP. ip: whitelisted IPs only, credentials refused.
status *active | disabled | expired | quota_exhaustedComputed state.
blocked_byuser_disabled | customer_inactive | user_expired | balance_expired | quota_exhausted | balance_exhausted | nullWhy the gateway would refuse a connection right now, or null.
quota_bytes *integer | nullPer-proxy-user cap in bytes; null = only the account balance limits.
quota_gbnumber | nullquota_bytes in GB.
used_bytes *integerBytes used since the last quota reset (including in-flight bytes).
used_gbnumberused_bytes in GB.
quota_rationumber | nullused / quota, 0–1; null without a quota.
expires_attimestamp (ISO 8601) | nullProxy user expiry; null = never.
max_connectionsinteger | nullSimultaneous connection cap for this proxy user; null = unlimited.
allow_targeting *booleanWhether _key-value parameters are accepted in the password.
default_paramsstring | nullParameters applied when the password carries no geo token, e.g. country-TR.
default_countrystring | nullThe country code inside default_params, if any.
active_connectionsintegerConnections open right now.
usageProxyUserUsage
created_at *timestamp (ISO 8601)Creation time.
last_seen_attimestamp (ISO 8601) | nullLast proxy traffic.

ProxyUserInput

FieldTypeMeaning
usernamestringOptional; a cus-xxxxxx name is generated when omitted. Usernames are global, 3–32 characters, a-z 0-9 . _ -.
labelstringFree text label (≤ 64).
notestringPrivate note (≤ 500).
quota_gbnumber | nullPer-proxy-user cap in GB; null or omitted = unlimited (the account balance still limits).
max_connectionsinteger | nullSimultaneous connection cap; null = unlimited.
allow_targetingbooleanDefault true. When false every _key-value parameter is refused.
default_countrystringISO 3166-1 alpha-2; becomes default_params: country-XX.
default_paramsstringFull default parameter string (country-TR_city-istanbul); overrides default_country. No session keys.
auth_modepassword | ippassword (default) or ip.
enabledbooleanDefault true.

ProxyUserPatch

FieldTypeMeaning
labelstringLabel.
notestringNote.
enabledbooleanEnable / disable (open connections are cut on disable).
quota_gbnumber | nullNew cap in GB; null = unlimited.
max_connectionsinteger | nullnull = unlimited.
allow_targetingbooleanTargeting permission.
default_countrystring | nullISO code or null to clear.
default_paramsstring | nullParameter string or null to clear.
auth_modepassword | ippassword or ip.
expires_attimestamp (ISO 8601) | nullExpiry, or null for never.

ProxyUserCreated

FieldTypeMeaning
proxy_user *ProxyUser
secret *stringThe proxy password. Shown only in this response — store it now.
host *stringGateway host.
port *integerGateway port (HTTP and SOCKS5).
url *stringReady-to-use http://user:secret@host:port.

ProxyUserList

FieldTypeMeaning
items *array of ProxyUserAll proxy users of the account, username order.
total *integerCount.

WhitelistEntry

FieldTypeMeaning
id *integerEntry id (use it to delete).
proxy_user *stringOwning proxy user.
ip *stringNormalized address or CIDR block.
notestringNote.
created_at *timestamp (ISO 8601)Creation time.

WhitelistInput

FieldTypeMeaning
ip *stringIPv4/IPv6 address or CIDR block (at least /16 for IPv4, /48 for IPv6).
notestringOptional note (≤ 120).

WhitelistList

FieldTypeMeaning
items *array of WhitelistEntryEntries of this proxy user.
total *integerCount.

RotateInput

FieldTypeMeaning
proxy_user *stringProxy user id or username that owns the session name.

RotateResult

FieldTypeMeaning
supported *booleanfalse when rotation is not available on the network right now; just change the session name instead.
ok *booleantrue when the network dropped the session.
sessionstring | nullNamespaced session id as seen by the network (never your name).

Country

FieldTypeMeaning
code *stringISO 3166-1 alpha-2.
name *stringName in the requested language.

Locations

FieldTypeMeaning
count *integerNumber of countries.
countries *array of CountryEvery country, sorted by name.
regions_available *booleanWhether 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_attimestamp (ISO 8601) | nullWhen the region/city lists were last refreshed.

CountryLocations

FieldTypeMeaning
country *Country
regions_available *booleanAs above.
regions *array of stringRegions of this country.
cities *array of stringCities of this country.

Package

FieldTypeMeaning
id *stringPackage id.
name *objectName.
descriptionobjectDescription.
gb *numberGB credited when paid.
price *numberPrice.
currency *TRY | USDTRY or USD.
price_per_gbnumber | nullprice / gb.
validity_days *integerDays added to the balance expiry when paid (0 = no change).
concurrency_limitinteger | nullConcurrency limit applied to the account when paid; null = not changed.
featuredbooleanHighlighted on the pricing page.

PackageList

FieldTypeMeaning
items *array of PackageVisible packages in display order.
payment_methods *array of bank | cryptoMethods currently accepted by POST /orders.
currenciesarray of stringCurrencies in use.

Order

FieldTypeMeaning
id *stringOrder id.
reference *stringShort code to write in the transfer description, e.g. NRTH-7K3Q2.
status *pending | paid | rejected | cancelledLifecycle state.
method *bank | crypto | manualPayment method.
package_idstring | nullPackage, null for manual credits.
package_nameobject | nullPackage name snapshot.
gb *numberGB credited when paid.
price *numberPrice.
currency *stringCurrency.
validity_daysintegerValidity added when paid.
customer_notestringYour note.
instructionsstringPayment instructions for the chosen method (pending orders only).
created_at *timestamp (ISO 8601)Creation time.
paid_attimestamp (ISO 8601) | nullWhen it was marked paid.
handled_attimestamp (ISO 8601) | nullWhen it was paid, rejected or cancelled.

OrderInput

FieldTypeMeaning
package_id *stringA visible package id from GET /packages.
method *bank | cryptoOne of payment_methods from GET /packages.
notestringOptional note to support (≤ 500).

OrderList

FieldTypeMeaning
items *array of OrderNewest first.
total *integerTotal matching orders.
limit *integerPage size.
offset *integerOffset.

TicketMessage

FieldTypeMeaning
id *integerMessage id.
author_type *customer | admincustomer or admin (support).
body *stringPlain text with line breaks.
created_at *timestamp (ISO 8601)Time.

Ticket

FieldTypeMeaning
id *stringTicket id.
subject *stringSubject.
status *open | answered | closedopen (waiting for support), answered, closed.
priority *normal | highPriority.
last_reply_bycustomer | adminWho wrote last.
message_countintegerNumber of messages.
messagesarray of TicketMessageOldest first; only in single-ticket responses.
created_at *timestamp (ISO 8601)Creation time.
updated_at *timestamp (ISO 8601)Last activity.

TicketInput

FieldTypeMeaning
subject *string≤ 120 characters.
body *stringFirst message, plain text, ≤ 5000.
prioritynormal | highDefault normal.

TicketReplyInput

FieldTypeMeaning
body *stringPlain text, ≤ 5000. Replying to a closed ticket reopens it.

TicketList

FieldTypeMeaning
items *array of TicketLatest activity first (without messages).
total *integerTotal.
limit *integerPage size.
offset *integerOffset.

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:

WhatLimit
Proxy users per account200
Whitelist entries per proxy user50 (blocks at least /16 IPv4, /48 IPv6)
Session name1–64 characters (A-Z a-z 0-9 . , -)
lifetime1–1440 minutes
Generator lines per call1000
Minimum per connection512 bytes counted, even for a tiny request
Usage historydaily rows kept 400 days
Pending orders10
Open tickets20 (subject 120, message 5000 characters)
API request body64 KB
Idle connectionclosed 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.