Building Gateway Proxy Strings with the API

Ask the API for ready-made rotating and residential proxy strings by country, state, city or sticky session, in the format your tool imports.

POST /gateways/endpoint returns finished proxy strings for a gateway plan, so your code never assembles the username grammar itself. Send the product and any targeting, and the answer comes back as a http:// URL, a socks5:// URL and a host:port:user:pass line, already checked against your plan and against the exits that exist:

curl -X POST https://node4.io/api/v1/gateways/endpoint \
  -H "Authorization: Bearer n4k_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"product": "rotating_datacenter", "country": "us"}'

This page walks through that call and the one before it. The field-by-field reference is in gateways and live connections, and the OpenAPI document at /openapi.json has every schema.

Before you start

Create API Key dialog in Account Settings, API & Webhooks tab: key name, expiration and the permission scopes to grant.

A key holding read:proxies can read proxy passwords, so keep it on your server. API key security covers storage and rotation.

Step 1: see what the account can use

GET /gateways lists one entry per gateway product the account holds, with the hostname, both ports, the logins that authenticate, and the targeting each product accepts:

curl -H "Authorization: Bearer n4k_YOUR_KEY" https://node4.io/api/v1/gateways
{
  "success": true,
  "data": {
    "gateways": [
      {
        "product": "rotating_shared",
        "name": "Rotating Datacenter",
        "host": "gw-rotating_datacenter.node4.io",
        "ports": { "http": 8083, "socks5": 1083 },
        "rotation": "per_connection",
        "targeting": { "country": true, "state": false, "city": false, "asn": false, "session": true },
        "usernameGrammar": "USERNAME[-country-<cc>][-session-<id>]",
        "stickySession": { "supported": true, "segment": "-session-<id>", "maxSeconds": 1800, "bestEffort": false },
        "usernames": [
          { "username": "node4_364512_k3Jd9xQa", "source": "account", "status": "active", "credentialId": 88, "label": null }
        ],
        "defaultUsername": "node4_364512_k3Jd9xQa"
      }
    ]
  }
}

(Trimmed to one product.) product is the internal key: Rotating Datacenter is rotating_shared, and requests accept rotating_datacenter as an alias for it. The other keys are rotating_unmetered and residential. A Rotating Unmetered entry also carries connectionLimit, the concurrent-connection ceiling of your plans added together.

Read targeting before you send options. On the residential entry, state, city and asn reflect what your residential plan includes, so the same request can be valid on one account and refused on another.

A login with "status": "over_limit" has used its own monthly bandwidth cap. The gateways refuse it until the cap is raised or the next UTC month starts, so the API never picks it as the default. Sub-user caps are covered in managing sub-users for clients.

Step 2: build the strings

The endpoint takes the same options as a JSON body on POST or as query parameters on GET:

OptionAccepted onWhat it does
productall (required)rotating_datacenter (or rotating_shared), rotating_unmetered, residential
protocolallhttp (default) or socks5; sets port, url and hostPortUserPass
countryallTwo-letter code, such as us
stateresidentialTwo or three characters, such as tx; needs country
cityresidentialLowercase city code with no separators, such as losangeles; needs country
asnresidentialThe bare network number, such as 7922
sessionallYour own sticky session id, letters and digits, 1 to 32 characters
stickyalltrue mints a session id for you
countall1 to 100; above 1, each string gets its own new session id
credentialId or usernameallWhich login to build with; default is the first active one

The GET form is handy in a shell:

curl -H "Authorization: Bearer n4k_YOUR_KEY" \
  "https://node4.io/api/v1/gateways/endpoint?product=residential&country=us&state=tx&city=austin&protocol=socks5"

Every response has the same shape. With count above 1 the endpoints array holds one entry per session, and the first entry's fields are repeated at the top level for the single-string case:

{
  "success": true,
  "data": {
    "product": "residential",
    "name": "Residential",
    "protocol": "http",
    "rotation": "per_connection",
    "targeting": { "country": "us", "state": "ny", "city": null, "asn": null },
    "sticky": { "enabled": true, "maxSeconds": 1800, "note": "..." },
    "host": "gw-residential.node4.io",
    "port": 8082,
    "username": "node4_364512_k3Jd9xQa-country-us-state-ny-session-q7m2x9ka4p",
    "password": "Vt8yKp2LwQ9sRb4n",
    "url": "http://node4_364512_k3Jd9xQa-country-us-state-ny-session-q7m2x9ka4p:Vt8yKp2LwQ9sRb4n@gw-residential.node4.io:8082",
    "http": "http://...@gw-residential.node4.io:8082",
    "socks5": "socks5://...@gw-residential.node4.io:1082",
    "hostPortUserPass": "gw-residential.node4.io:8082:node4_364512_k3Jd9xQa-country-us-state-ny-session-q7m2x9ka4p:Vt8yKp2LwQ9sRb4n",
    "endpoints": [
      { "sessionId": "q7m2x9ka4p", "host": "gw-residential.node4.io", "port": 8082, "username": "...", "password": "...", "url": "...", "http": "...", "socks5": "...", "hostPortUserPass": "..." }
    ]
  }
}

Pick the output format

All three forms carry the same login, so choose by what reads them:

FieldShapeFeed it to
httphttp://user:pass@host:portcurl -x, Python requests, most HTTP libraries
socks5socks5://user:pass@host:portSOCKS5 clients and libraries; see SOCKS5 setup
hostPortUserPasshost:port:user:passProxy testers, scraping tools and browser extensions that import a list

url is whichever of http or socks5 matches the protocol you asked for. The URL forms are percent-encoded, so they are safe to paste even if a password contains a reserved character; the bare username and password fields are not encoded.

Use them from code

A Node.js 18+ job that asks for 20 sticky residential sessions in the United States and spreads its work across them, using undici for proxy support:

import { ProxyAgent, fetch } from "undici";

const res = await fetch("https://node4.io/api/v1/gateways/endpoint", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.NODE4_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ product: "residential", country: "us", count: 20 }),
});
if (!res.ok) throw new Error(`${res.status}: ${(await res.json()).error}`);
const { endpoints } = (await res.json()).data;

// Each session keeps its own exit; one worker per session.
const agents = endpoints.map((e) => new ProxyAgent(e.http));
const r = await fetch("https://api.ipify.org?format=json", { dispatcher: agents[0] });
console.log(await r.json()); // { ip: "..." }

The same in Python with requests, for one rotating string with no session:

import os
import requests

API = "https://node4.io/api/v1"
headers = {"Authorization": f"Bearer {os.environ['NODE4_API_KEY']}"}

r = requests.post(f"{API}/gateways/endpoint", headers=headers,
                  json={"product": "rotating_datacenter", "country": "us"}, timeout=30)
r.raise_for_status()
proxy = r.json()["data"]["http"]

ip = requests.get("https://api.ipify.org", proxies={"http": proxy, "https": proxy}, timeout=30).text
print(ip)

Build strings once and reuse them. They stay valid for as long as the login does, so calling the endpoint before every request spends your 60-per-minute API budget for nothing.

Sticky or rotating

Without session, sticky or count, the string has no session segment and rotates: the gateway picks an exit for each connection, meaning each HTTPS tunnel or SOCKS5 connection, and each plain-HTTP request. Requests sent down one kept-alive tunnel share its exit, so a client that reuses connections rotates less often than one that opens a new one per request. The IP rotation guide shows how to control that from your client.

With a session, every connection carrying that session id leaves from one exit:

The Sticky session switch on the Proxies page's Connect card builds the same kind of string by hand, which is a quick way to check what the API returns.

Rotating gateway Connect card on the Proxies page with Sticky session on: the session id and the sticky connection string.

The lifetime is fixed. There is no ttl you can set: sending ttl with any value other than the ceiling (reported as maxSeconds) is refused with a 400 that says so. Which tasks need a session at all is covered in sticky versus rotating sessions.

count is the quickest way to get a pool of independent sticky exits for parallel workers: each of the up to 100 strings has a different session id. It cannot be combined with your own session, and sticky: false with count above 1 is a contradiction the API refuses.

Targeting rules

The rotating products accept country and nothing finer. State, city and ASN are residential, and sending them for a rotating product is a 400 naming the option, rather than a string that would quietly serve any city. Rotating exits are in the United States, Italy and Spain.

On residential, state and city each need country, and the API checks your request against the same coverage list the dashboard's targeting builder uses. Residential targeting explains the segments themselves; the API adds two checks worth knowing:

Errors and how to fix them

StatusUsual causeFix
400Unknown product, a malformed code, state/city/asn on a rotating product, count outside 1 to 100, count with session, a ttlRead the error string; it names the option and the accepted values
403The key lacks read:proxies, or your residential plan does not include that targetingAdd the scope to a new key, or drop the option the plan lacks
404No active plan for that product, or the credentialId/username is not an active login on the accountCheck GET /gateways for the products and logins you hold
409The account has no active login, or the login you named is over its monthly capCreate a login, raise the cap, or pick another login
422No exits in that country, an unknown or ambiguous city, a state with no coverageUse a location from the list in the error; add state to an ambiguous city
429The API rate limitWait for Retry-After

A string that the API built and the gateway still refuses is a connection problem rather than an API one: fixing 407 errors and connection issues take over there. Static proxies have no gateway string to rebuild; when one of those stops answering, automating IP replacement covers the health check and the replace call.

Rotating Unmetered: size the plan from connection data

Rotating Unmetered is priced by concurrent connections, so the useful number is how many your tools open at once. Two endpoints, on the same read:proxies scope, return the figures the dashboard's live connections panel draws.

Live connections panel on the Proxies page: open connections against the limit, the day's peak, refusals and a 24-hour chart.

GET /connections/live is a snapshot:

curl -H "Authorization: Bearer n4k_YOUR_KEY" https://node4.io/api/v1/connections/live
{
  "success": true,
  "data": {
    "hasPlan": true,
    "limit": 250,
    "open": 231,
    "at": 1792224000000,
    "category": "rotating_unmetered",
    "refusedThisMinute": 0,
    "refusedLast5Minutes": 14
  }
}

limit is your ceiling across every Rotating Unmetered plan on the account, open is what is open across the whole gateway fleet right now, and the two refused figures count connections turned away at the ceiling. open or the refusal counts can be null when the counter is briefly unreadable; treat that as "unknown", not zero.

GET /connections/history?range=24h returns the series, with 1h, 7d and 30d as the other ranges and 35 days of history kept:

import os, requests

r = requests.get("https://node4.io/api/v1/connections/history", params={"range": "7d"},
                 headers={"Authorization": f"Bearer {os.environ['NODE4_API_KEY']}"}, timeout=30)
d = r.json()["data"]
s = d["summary"]
print(f"limit {d['limit']}, peak {s['peak']}, refused {s['refused']} over {s['minutesAtLimit']} minutes")
if d["suggestion"]:
    print("suggested add-on:", d["suggestion"]["name"])

summary holds the peak, total refusals, minutesAtLimit (minutes that refused at least one connection) and activeMinutes. buckets is the chart data, one { t, peak, refused } per bucket of bucketMinutes. When the range had refusals, suggestion names the smallest plan that, added to what you hold, clears the demand: plans of one type stack, so nothing has to be canceled. redisOk: false means the most recent minutes could not be read and the series may be short at the end.

Read it this way: a peak well under limit with no refusals means the plan has room. Refusals during a few minutes a week usually mean a burst, which a smaller connection pool in your tool may absorb. Refusals across many minutes mean the work needs more concurrency. For an alert instead of a poll, subscribe to the connections.limit_reached webhook described in the API reference. Current plan sizes and prices are on /pricing/unmetered.