Automating IP Replacement and Proxy Health Checks

Watch static proxy health over the API, replace an address only when it has really failed, and update your config from the proxy.replaced webhook.

To keep a fleet of static proxies working without watching a dashboard, combine three pieces: GET /proxies/health to see what Node4 knows about each address, your own probe against the sites you actually use, and POST /proxies/{id}/replace when an address has really failed. A proxy.replaced webhook then tells the rest of your stack about the new address.

curl -H "Authorization: Bearer n4k_YOUR_KEY" https://node4.io/api/v1/proxies/health

Replacement applies to Premium Datacenter plans whose monthly allowance is above zero. Shared Datacenter plans include no replacements, and the replacementLimit in each replace response shows your plan's allowance. Gateway products have nothing to replace, because their exits change by design; rotation API or rotating gateway explains the difference. The endpoint reference is in refresh or replace a proxy's IP.

Before you start

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

Read proxy health

GET /proxies/health returns one row per active proxy, the same health state the dashboard's Proxies page shows in each proxy's row of its endpoint table, where a failing proxy with a replacement queued reads Failing with the replacement attempt beside it. It carries no usernames or passwords:

{
  "success": true,
  "data": {
    "total": 3,
    "healthy": 2,
    "proxies": [
      {
        "proxyId": 1201,
        "host": "203.0.113.18",
        "port": 4443,
        "protocol": "http",
        "proxyType": "premium_datacenter",
        "country": "US",
        "city": null,
        "isHealthy": true,
        "lastHealthCheck": "2026-10-17T09:40:12.000Z",
        "replacementPending": false,
        "retryInfo": null
      },
      {
        "proxyId": 1202,
        "host": "203.0.113.40",
        "port": 4443,
        "protocol": "http",
        "proxyType": "premium_datacenter",
        "country": "US",
        "city": null,
        "isHealthy": false,
        "lastHealthCheck": "2026-10-17T09:40:12.000Z",
        "replacementPending": true,
        "retryInfo": { "retryCount": 1, "maxRetries": 8, "nextRetryAt": "2026-10-17T09:55:00.000Z" }
      }
    ]
  }
}

What the fields tell you:

FieldMeaning
isHealthyWhether the address answered Node4's own reachability check
lastHealthCheckWhen that check last ran; null means it has not run for this address yet
replacementPendingNode4 has already queued a replacement for this proxy
retryInfoWhen a queued replacement is still being retried: attempts so far and the next try

Endpoint table on the Proxies page: one proxy failing its health check with a replacement queued, the rest healthy.

Two limits shape how to use it. The check confirms that the address answers; it cannot see whether a particular site blocks or slows it, so a proxy can be isHealthy: true and still fail on your target. And when replacementPending is true, leave that proxy alone: a replacement is already on its way, and replacing it yourself spends your allowance on work that is already happening.

Decide when to replace

Every replacement uses one from your monthly allowance and gives you a fresh address whose reputation is whatever it already was. Replace on evidence, not on one bad response. A policy that holds up:

  1. Probe through the proxy against your own targets, plus a neutral endpoint such as https://api.ipify.org, on a schedule.
  2. Count consecutive failures per proxy, across several runs. A single timeout or a 5xx from the target is usually the target, not the address.
  3. Replace when the neutral endpoint fails too, or when every one of your targets refuses the address over several runs. If only one target refuses it, the issue is that site's view of datacenter traffic, and why IPs get blocked is the better next step than a new address.
  4. Skip proxies with replacementPending: true and anything replaced in the last hour.
  5. Stop when the allowance is spent and alert a person instead.

A 407 from a proxy is an authentication problem, not a dead address: check the credentials first with fixing 407 errors.

Replace the address

curl -X POST -H "Authorization: Bearer n4k_YOUR_KEY" https://node4.io/api/v1/proxies/1202/replace
{
  "success": true,
  "data": {
    "proxyId": 1202,
    "oldIp": "203.0.113.40",
    "newIp": "198.51.100.112",
    "newPort": 4443,
    "newUsername": "node4_364512_p4Lx8sQe",
    "newPassword": "Rk3vW9nTq2LsYb6m",
    "replacementsUsed": 1,
    "replacementLimit": 1
  }
}

The proxy keeps its id and product and moves to the new address with the credentials in the response. Connections open on the old address stop working. The allowance is the same one the dashboard's Refresh IP button and the older POST /proxies/refresh draw on.

Every other answer means something specific:

StatusMeaningWhat to do
403 Your plan does not include IP replacements.Free plan, Shared Datacenter, or a plan with no allowance (replacementLimit: 0)Nothing to automate on this plan
404The id is not one of your proxiesRe-read GET /proxies
409 Proxy is not active.Expired or suspendedCheck the plan, not the address
409 already in progressAnother replacement of this proxy is runningWait, then read GET /proxies; it may already have moved
409 cannot be replacedThis proxy cannot be replaced automaticallyContact support if it is not working
429 with resetsAtThis month's replacements are used upStop; Retry-After and resetsAt say when the allowance resets
503Replacement is temporarily unavailableNothing was used; try again later

Two kinds of 429 exist. The API rate limit answers with retryAfterSeconds in the body and is safe to retry after the wait. The replacement allowance answers with resetsAt and replacementLimit, and its Retry-After can be weeks: do not sleep on it, stop the job.

Never retry a replace blindly

The replace call is not deduplicated. If a request times out or the connection drops, you cannot tell from your side whether the swap landed, and sending it again can use a second replacement and move the proxy twice. Check first:

def replaced_since(proxy_id, old_host):
    r = requests.get(f"{API}/proxies", headers=headers, timeout=30)
    for p in r.json()["data"]["proxies"]:
        if p["id"] == proxy_id:
            return p["host"] != old_host
    return False

If the host has changed, the replacement happened: take the new details from GET /proxies and carry on. Only if it has not changed is a second call a new attempt. The Node.js and Python client libraries from support follow the same rule and never resend a replacement on their own.

Listen for proxy.replaced

The proxy.replaced webhook fires for every replacement: from the API, from the dashboard, by our support team, or automatically after downtime. That makes it the one place to update your own config, whoever triggered the change. Register it once:

curl -X POST https://node4.io/api/v1/webhooks \
  -H "Authorization: Bearer n4k_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "proxy-swaps", "url": "https://ops.example.com/hooks/node4", "events": ["proxy.replaced"]}'

The same webhook can be created without code in Account Settings → API & Webhooks with Create Webhook, ticking Proxy Replaced.

Create Webhook dialog in Account Settings, API & Webhooks tab: name, endpoint URL and events, with Proxy Replaced ticked.

Keep the secret from the response: the API returns it only once, though the dashboard's Copy secret button shows it again. Each delivery looks like this:

{
  "id": "evt_3f9a1c0b7d2e4a6f8b1c2d3e",
  "event": "proxy.replaced",
  "data": {
    "proxyId": 1202,
    "oldIp": "203.0.113.40",
    "newIp": "198.51.100.112",
    "newPort": 4443,
    "reason": "manual",
    "timestamp": "2026-10-17T09:47:31.000Z"
  },
  "timestamp": 1792230451000
}

The outer timestamp is the send time in epoch milliseconds; the one inside data is ISO 8601. reason is manual when you replaced it, support when our team did, and auto when Node4 replaced it after downtime, with downtimeMinutes added. The payload carries no credentials, so fetch them from GET /proxies after you receive it. A small Node.js receiver that verifies the signature and skips repeats:

import express from "express";
import crypto from "node:crypto";

const app = express();
const seen = new Set(); // use a database in production

app.post("/hooks/node4", express.raw({ type: "application/json" }), async (req, res) => {
  const expected = crypto.createHmac("sha256", process.env.NODE4_WEBHOOK_SECRET).update(req.body).digest("hex");
  const given = req.get("X-Node4-Signature") ?? "";
  if (given.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
    return res.status(401).end();
  }
  const evt = JSON.parse(req.body.toString("utf8"));
  if (seen.has(evt.id)) return res.status(200).end();
  seen.add(evt.id);

  if (evt.event === "proxy.replaced") {
    const { proxyId, oldIp, newIp, reason } = evt.data;
    console.log(`proxy ${proxyId}: ${oldIp} -> ${newIp} (${reason})`);
    // re-read GET /proxies and push the new host and credentials to your workers
  }
  res.status(200).end();
});

app.listen(3000);

Answer with a 2xx quickly and do the slow work afterwards; a timeout or 5xx is retried with the same event id. The full delivery rules are in webhooks, and receiving Node4 webhooks covers signature checks and retries in depth.

A small monitoring script

This Python job runs from cron every 10 minutes. It probes each healthy-looking proxy through itself, keeps a failure count on disk, and replaces only after three failed runs in a row, never more than one call per proxy per run. Allowances are small, so it stops and alerts a person as soon as the allowance is used:

import json, os, pathlib, requests

API = "https://node4.io/api/v1"
headers = {"Authorization": f"Bearer {os.environ['NODE4_API_KEY']}"}
STATE = pathlib.Path("proxy-failures.json")
THRESHOLD = 3

failures = json.loads(STATE.read_text()) if STATE.exists() else {}
proxies = {p["id"]: p for p in requests.get(f"{API}/proxies", headers=headers, timeout=30).json()["data"]["proxies"]}
health = requests.get(f"{API}/proxies/health", headers=headers, timeout=30).json()["data"]["proxies"]

def probe(p):
    url = f"http://{p['username']}:{p['password']}@{p['host']}:{p['port']}"
    try:
        r = requests.get("https://api.ipify.org", proxies={"http": url, "https": url}, timeout=15)
        return r.status_code == 200
    except requests.RequestException:
        return False

for h in health:
    pid, key = h["proxyId"], str(h["proxyId"])
    if h["replacementPending"] or pid not in proxies:
        continue
    if probe(proxies[pid]):
        failures.pop(key, None)
        continue
    failures[key] = failures.get(key, 0) + 1
    if failures[key] < THRESHOLD:
        continue
    try:
        r = requests.post(f"{API}/proxies/{pid}/replace", headers=headers, timeout=60)
    except requests.RequestException:
        print(f"{pid}: outcome unknown, check GET /proxies before trying again")
        continue
    if r.ok:
        d = r.json()["data"]
        print(f"{pid}: {d['oldIp']} -> {d['newIp']} ({d['replacementsUsed']}/{d['replacementLimit']})")
        failures.pop(key, None)
        if d["replacementsUsed"] is not None and d["replacementsUsed"] >= d["replacementLimit"]:
            print("allowance used for this month; alerting a person")
            break
    elif r.status_code == 403:
        print("this plan includes no replacements; alerting a person")
        break
    elif r.status_code == 429 and "resetsAt" in r.json():
        print(f"allowance used up until {r.json()['resetsAt']}; alerting a person")
        break
    else:
        print(f"{pid}: {r.status_code} {r.json().get('error')}")

STATE.write_text(json.dumps(failures))

It makes two API reads and at most a few writes per run, far inside the 60-per-minute limit. Add your own targets to probe for a signal that matches your work, and keep the threshold high enough that a target's bad minute does not spend your allowance. When a fleet needs more fresh addresses than the allowance covers, more proxies is usually the cheaper fix than more replacements: plan sizes are on /pricing/dedicated and /pricing/shared.