← Back to blog
WEBHOOKS nodetonet.com

Webhooks for IP change events — get notified the moment a tunnel rotates

N Nodetonet Team
April 4, 2026 7 min read

Polling the API with "has my proxy's IP changed?" works, but it wastes cycles on both ends and adds latency to every downstream workflow. Webhooks invert the model entirely: you tell Nodetonet one HTTPS endpoint, and the moment any of your tunnels switches exit IP we deliver a small, signed JSON payload to it — no polling, no delay, no missed events. This guide walks through registration, the three triggers that fire the event, the exact payload shape, HMAC signature verification, retry behaviour, and how to test the whole pipeline locally before you go live.

TL;DR: register a URL at /webhooks, listen for proxy.ip_changed, verify the X-Nodetonet-Signature header, and handle deduplication with the stable id field. Everything below shows you exactly how.

Why webhooks instead of polling

A polling loop must guess how often the IP changes. Set the interval too short and you burn request quota; set it too long and your downstream logic reacts to stale data. The problems compound if you manage dozens of token groups where different pools rotate on different schedules. Webhooks solve this cleanly — you get an event within seconds of the IP change, regardless of whether that change happened because of a sticky session TTL, a manual request, or a real-world carrier event on the device.

This matters most in automation pipelines: an account-management workflow that must log every new exit IP, a monitoring dashboard that tracks carrier NAT reassignments, or a load-balancer that needs to know when a pool member's address has shifted.

Registering a webhook

Open /webhooks in the panel and click + New webhook. You provide three things:

After you save, the panel returns a signing secret. Treat it like a password — store it in an environment variable, never in source code. You will use it to verify every incoming request.

The three triggers

proxy.ip_changed fires under three distinct circumstances. The reason field in the payload tells you which one occurred:

Trigger reason value When it happens
Sticky TTL expiry sticky_ttl The tunnel is in sticky mode; the time-to-live elapsed and the next request landed on a different device or upstream session, rolling the exit IP.
Manual rotation manual Your client sent an X-Rotate: 1 header, or you clicked Rotate in the panel.
Device reconnect device_reconnect A paired phone switched cell towers, toggled airplane mode, or lost and regained signal; the carrier assigned it a fresh public IP through CGNAT.

The device_reconnect trigger is unique to mobile proxies — datacenter and upstream residential pools never fire it. It is especially useful for monitoring the natural IP churn of a real cellular device and deciding whether your downstream session should re-authenticate.

The payload

Every proxy.ip_changed event uses the same JSON envelope:

{
  "event": "proxy.ip_changed",
  "id": "evt_9k2x7m",
  "timestamp": "2026-04-04T11:08:42.331Z",
  "data": {
    "proxyId": "prx_4d7a91",
    "oldIp": "188.114.96.7",
    "newIp": "92.184.117.42",
    "reason": "device_reconnect",
    "tokenId": "tok_3kzl8w9"
  }
}

Standard headers sent on every delivery:

Content-Type: application/json
User-Agent: Nodetonet-Webhooks/1.0
X-Nodetonet-Event: proxy.ip_changed
X-Nodetonet-Event-Id: evt_9k2x7m
X-Nodetonet-Signature: t=1712228922,v1=4f8c...e3a1

tokenId cross-references the device pool that served the request — useful if a single proxy ID maps to multiple token groups. Log both proxyId and tokenId together for complete audit trails.

HMAC signature verification

The X-Nodetonet-Signature header protects you against spoofed deliveries. It contains a Unix timestamp and an HMAC-SHA256 signature computed over the string "timestamp.rawBody" using your signing secret.

Verification in Node.js — under 15 lines:

const crypto = require('crypto');

function verifyWebhook(rawBody, signatureHeader, secret) {
  const [tPart, vPart] = signatureHeader.split(',');
  const t = tPart.split('=')[1];
  const sig = vPart.split('=')[1];

  // Reject events older than 5 minutes to block replay attacks
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(t + '.' + rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(sig, 'hex'),
  );
}

Two common mistakes to avoid. First, pass the raw request body string — not a re-serialised JSON object. Whitespace differences will break the signature. In Express, use express.raw({ type: 'application/json' }) before your JSON middleware on the webhook route. Second, the 5-minute window in line 7 is not optional — it prevents replay attacks where an adversary captures a valid signed delivery and re-sends it to your endpoint later.

Retry schedule on failure

If your endpoint returns a 5xx status code — or does not respond within 10 seconds — we retry with exponential backoff:

Attempt 1: immediately
Attempt 2: +30 s
Attempt 3: +2 min
Attempt 4: +15 min
Attempt 5: +1 h
Attempt 6: +6 h
Abandoned after 24 h total

A 4xx response is treated as a deliberate rejection — bad signature, wrong content type, unrecognised event — and we do not retry it. If your endpoint is temporarily broken (a deploy window, a crash), return 500 or 503 and we will automatically come back.

Idempotency key: every event carries a stable id field (e.g. evt_9k2x7m). On retries the ID is unchanged, so your handler must deduplicate on it. The simplest approach: insert the id into a database column with a unique constraint and silently ignore insert conflicts.

Testing locally before production

The panel includes a Send test event button next to every registered webhook. Click it and we dispatch a synthetic proxy.ip_changed event with realistic-looking data and a real HMAC signature — good enough to exercise your full verification and handler logic.

To receive that test against a local development server, expose a local port to the public internet. Common options:

Confirm your handler logs the parsed payload and the signature verification result before you register the endpoint in production. Catching a raw-body parsing mistake in development is far cheaper than diagnosing it from production logs.

Putting it together — a minimal Express handler

Below is a complete, production-ready webhook receiver. It verifies the signature, deduplicates by event ID, and logs the transition for downstream use:

const express = require('express');
const crypto  = require('crypto');
const app     = express();

const SECRET = process.env.NODETONET_WEBHOOK_SECRET;
const seen   = new Set(); // replace with DB unique constraint in production

app.post('/hooks/ip-changed',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.headers['x-nodetonet-signature'];
    if (!verifyWebhook(req.body.toString(), sig, SECRET)) {
      return res.sendStatus(401);
    }
    const event = JSON.parse(req.body);
    if (seen.has(event.id)) return res.sendStatus(200); // dedupe
    seen.add(event.id);
    const { proxyId, oldIp, newIp, reason } = event.data;
    console.log('IP rotated:', proxyId, oldIp, '->', newIp, '(' + reason + ')');
    // your downstream logic here
    res.sendStatus(200);
  },
);

app.listen(3000);

Notice express.raw is applied only to this one route — your other routes can still parse JSON normally. Respond with 200 as soon as you have accepted the event; do any slow work asynchronously so you never breach the 10-second delivery timeout.

What to do with the IP change event

The event becomes most powerful when combined with other Nodetonet features:

For fully automated pipeline construction, see programmatic tunnel creation in Python — the webhook complements that guide by closing the feedback loop between IP rotation and your orchestration code.

What's next

Frequently asked questions

What triggers a proxy.ip_changed webhook event?
Three things: a sticky session TTL expiring and the next request landing on a new device or upstream session; a manual rotation via the X-Rotate header or panel button; or a paired mobile phone reconnecting to the carrier with a new public IP after a cell-tower switch or signal drop.
How do I verify that a webhook delivery is really from Nodetonet?
Check the X-Nodetonet-Signature header. It contains a timestamp and an HMAC-SHA256 signature over the string "timestamp.rawBody" using the signing secret you received at registration. Always pass the raw request body — not re-serialised JSON — and reject events older than 5 minutes to prevent replay attacks.
What happens if my webhook endpoint is down when an event fires?
Nodetonet retries with exponential backoff — immediately, then +30 s, +2 min, +15 min, +1 h, and +6 h — abandoning after 24 hours total. Return 500 or 503 if your endpoint is temporarily broken; we will keep retrying. A 4xx response is treated as a permanent rejection and is not retried.
How do I prevent processing the same event twice if Nodetonet retries?
Every event carries a stable id field that is unchanged across retries. Store each id in a database column with a unique constraint and silently ignore insert conflicts. This is the standard idempotency pattern for webhook receivers.
Can I filter webhook events to only some of my proxies?
Yes. When you register the webhook at /webhooks, the optional proxy filter lets you scope delivery to a specific list of proxy IDs. By default every proxy in your account fires the event. Scoping is useful when you run separate pipelines for different pools, such as a dedicated handler for your rotating mobile proxy group.
How can I test the webhook locally before pointing it at production?
Click Send test event in the panel next to your registered webhook — we dispatch a synthetic proxy.ip_changed event with a real HMAC signature. To receive it on your local machine, expose a port with a tunnelling tool such as ngrok, or use Nodetonet's own HTTP tunnels to get a persistent public subdomain without a third-party tool.
N

Nodetonet Team

Building Nodetonet — a prepaid proxy + tunneling platform that replaces ngrok, Cloudflared and a residential proxy provider with a single panel.

Related posts