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:
- A URL — must be HTTPS. We send a HEAD probe when you save; your server must respond within 5 seconds or registration fails.
- An event filter — select
proxy.ip_changedfor this use case. The complete event catalogue is at /documents. - An optional proxy filter — by default we fire for every proxy in your account. Scope it to one or more proxy IDs if you only care about a specific pool.
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 stableidfield (e.g.evt_9k2x7m). On retries the ID is unchanged, so your handler must deduplicate on it. The simplest approach: insert theidinto 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:
ngrok http 3000— generates a public HTTPS URL that tunnels to your local port. See our Nodetonet vs ngrok comparison for context on when you might prefer the two tools for different tasks.- Nodetonet's own HTTP tunnels — pair a Windows
.exeagent or a mobile device token and get a persistent subdomain without a separate tool.
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:
- Log every IP transition — keep an audit table of
(proxyId, oldIp, newIp, reason, timestamp)for compliance or debugging. Run our free What is my IP lookup against eachnewIpto capture carrier and geo metadata at rotation time. - Re-authenticate sessions — if your scraper holds a logged-in session tied to the old IP, use the event as the signal to log in again before the next request rather than discovering a broken session mid-crawl.
- Trigger a proxy health check — call our proxy checker after a
device_reconnectto confirm the new IP is clean before committing it to your rotation pool. - Route traffic intelligently — update a routing table so new requests go to the backconnect endpoint only after the carrier IP is confirmed fresh.
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
- Your first REST API call — the authentication and request basics this post builds on.
- Sticky sessions explained — understand exactly when and why
reason: "sticky_ttl"fires, and how to tune TTL for your workload. - When to use rotating mobile proxies — decide whether each job needs a fresh IP per request or a pinned session.
- Token groups and device pools — managing the fleets that generate the IP-change events your webhooks receive.
- Create a free account and register your first webhook in minutes.