Webhooks

Webhooks POST a signed JSON message to your URL when something happens on your account. Add them in the panel under Webhooks (up to 10 per login), pick the events, and copy the secret used to sign deliveries. The Test button sends a webhook.test event.

Events

EventWhendata fields
rotation.completeda rotation finished — successfully or not (API, link, panel or auto-rotate)proxy_id, label, port, status, ok, old_ip, new_ip, method, duration_ms, triggered_by, error
node.onlinea node (your box) came back onlinenode_id, hostname, online, last_seen_at
node.offlinea node stopped sending heartbeatsnode_id, hostname, online, last_seen_at
quota.exceededa proxy used up its traffic quota (once per crossing)proxy_id, label, port, quota_bytes, used_bytes, assigned_user_id
proxy.address_changingyour node is scheduled to move to another cloud router (sent when planned)node_id, from_host, to_host, switch_at, proxies (id, host, new_host, port, new_port)
proxy.address_changedthe move finished; the proxies answer on the new hostsame as above
webhook.testthe Test button in the panelmessage

A webhook with no events selected receives all of them. Reseller sub-users with their own webhooks receive rotation.completed and quota.exceeded for the proxies assigned to them.

Delivery

POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: ProxyKite-Webhooks/1
X-Hyp-Event: rotation.completed
X-Hyp-Delivery: 5f0c6a1e-8a1b-4c55-9d1e-2b7f1c3d9a10
X-Hyp-Timestamp: 1791914400
X-Hyp-Signature: sha256=3b6f...e91c
{"id": "5f0c6a1e-8a1b-4c55-9d1e-2b7f1c3d9a10", "event": "rotation.completed", "created_at": "2026-10-09T18:00:00+00:00",
 "data": {"proxy_id": 7, "label": "Shop A", "port": 30007, "status": "ok", "ok": true, "old_ip": "1.2.3.4",
          "new_ip": "5.6.7.8", "method": "netmode", "duration_ms": 9000, "triggered_by": "api", "error": null}}
  • One attempt, 5 second timeout, no retries, redirects not followed. Answer with any 2xx quickly and do the work afterwards. Anything else is logged as a failed delivery (visible in the panel).
  • id (= X-Hyp-Delivery) is unique per delivery; use it to ignore duplicates.
  • URLs must be public http(s) addresses — private, loopback and link-local targets are refused.

Verifying the signature

X-Hyp-Signature is sha256= + hex HMAC-SHA256 of "<X-Hyp-Timestamp>.<raw request body>", keyed with the webhook's secret. Always verify against the raw body (before JSON parsing), compare in constant time, and reject old timestamps (we suggest 5 minutes).

Python
Node.js
PHP
curl (test)
import hashlib, hmac, os, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["HYP_WEBHOOK_SECRET"].encode()

@app.post("/hooks/hyp")
def hyp_webhook():
    ts = request.headers.get("X-Hyp-Timestamp", "")
    sig = request.headers.get("X-Hyp-Signature", "")
    body = request.get_data()  # raw bytes
    expected = "sha256=" + hmac.new(SECRET, ts.encode() + b"." + body, hashlib.sha256).hexdigest()
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300 or not hmac.compare_digest(expected, sig):
        abort(401)
    event = request.get_json()
    print(event["event"], event["data"])
    return "", 204

Field notes

  • rotation.completed → status is ok, failed or not_implemented; ok is true only for ok; error is set when it failed. triggered_by is where the rotation came from: api, link, panel, auto.
  • quota.exceeded fires once when used_bytes reaches quota_bytes, and again only after the quota is reset or raised.