Webhooks
Webhooks let external systems react automatically to DNS changes – e.g. a ChatOps bot, an internal audit backend, or a backup pipeline that snapshots after every zone change.
Create a webhook
Per user under Settings → API & Security → Webhooks → New webhook:
- Name – display name.
- URL – HTTPS endpoint that accepts the POST. Localhost / private IPs are blocked by default (SSRF protection).
- Events – which events trigger. Default
["*"]= all.
On save the shared secret is shown once – use it to verify signatures on the receiver side.
Available events
| Event | When it fires |
|---|---|
zone.imported | A zone was created or replaced via import. |
record.created | New record (UI or API). |
record.updated | Record content or TTL changed. |
record.deleted | Record removed. |
record.bulk | Bulk operation (multiple create/delete in one call). |
Payload
JSON body. Example for record.updated:
{
"v": 1,
"event": "record.updated",
"timestamp": "2026-04-27T14:23:01.456Z",
"app": "PDNS Manager",
"actor_user_id": 17,
"data": {
"server": "master-fra1",
"zone": "example.com.",
"record": {
"name": "www.example.com.",
"type": "A",
"ttl": 60,
"old": [{"content": "203.0.113.10"}],
"new": [{"content": "203.0.113.20"}]
}
}
} Signature verification (mandatory!)
Each outgoing webhook request carries the header:
X-DNS-Manager-Signature: sha256=<hex> The value is an HMAC-SHA256 over the raw JSON body bytes, signed with the webhook shared secret. Always verify on the receiver side – otherwise any kid with a script can fake webhooks.
Verify in Python
import hmac, hashlib
SECRET = b"your-webhook-secret"
def verify(raw_body: bytes, header_value: str) -> bool:
if not header_value or not header_value.startswith("sha256="):
return False
sent = header_value.split("=", 1)[1].lower()
calc = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(sent, calc) Verify in Node.js
import crypto from "node:crypto";
const SECRET = "your-webhook-secret";
export function verify(rawBody, header) {
if (!header?.startsWith("sha256=")) return false;
const sent = header.slice("sha256=".length);
const calc = crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(sent, "hex"), Buffer.from(calc, "hex"));
} SSRF protection
Webhook URLs are validated before save and before each delivery. Blocked by default:
localhost,127.0.0.0/8::1- RFC1918 (
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16) - Link-local (
169.254.0.0/16,fe80::/10) - Multicast / broadcast
For internal setups (e.g. a webhook to an internal service in the same Compose net) there's an override:
# in .env
WEBHOOK_ALLOW_PRIVATE_URLS=true Retry
Webhooks run as background tasks. Non-2xx responses are retried with backoff a few times. Permanent failures end up in the backend log – the panel shows the last status in the webhook list.
Rotate the secret
In the webhook list → Rotate secret. The old one stops working immediately, the new one is shown once.