TTR Networks

Intermediate · PowerDNS on Debian: authoritative, secondary, DNSSEC and recursor · section 4 of 10

The HTTP API

✓ Lab-tested 21 October 2026 on Debian 13.7, PowerDNS Authoritative 4.9.17, PowerDNS Recursor 5.2.13, BIND dig 9.20.29

PowerDNS's HTTP API is how most real deployments manage zones: from automation, a web interface, or tools like Terraform and certbot's DNS plugins. It's a REST API on the built-in web server, authenticated with the X-API-Key header.

In the lab, a single PATCH request adds printer.internal.example.net. Three things are worth noticing in the output:

  • The change is live at once. No restart and no cache to wait for, unlike pdnsutil.
  • The serial went up by itself, from 2026102101 to 2026102102, because the zone has SOA-EDIT-API set to DEFAULT. Without that, API changes leave the serial alone, and secondaries never notice them.
  • A zone created through the API is served straight away, and gets the correct SOA from default-soa-content.

Zone kinds: Native, Primary and Secondary

Every zone has a kind, and pdnsutil creates zones as Native by default. Native means "replication is the database's job": PowerDNS sends no NOTIFY and expects every server to share or replicate the same database. That suits a MySQL or PostgreSQL cluster. For DNS-native replication to a secondary with its own database, which is what this lab does, the zones must be Primary on ns1. pdnsutil set-kind changes it.

Changing and creating zones through the API (real output)
root@ns1:~# curl -s -X PATCH -H "X-API-Key: lab-only-api-key" -H 'Content-Type: application/json' http://127.0.0.1:8081/api/v1/servers/localhost/zones/internal.example.net. -d '{"rrsets":[{"name":"printer.internal.example.net.","type":"A","ttl":3600,"changetype":"REPLACE","records":[{"content":"10.10.0.30","disabled":false}]}]}' -w 'HTTP %{http_code}\n'
HTTP 204

root@ns1:~# dig @203.0.113.53 printer.internal.example.net +short; dig @203.0.113.53 internal.example.net SOA +short
10.10.0.30
ns1.example.net. hostmaster.example.net. 2026102102 10800 3600 604800 3600

root@ns1:~# curl -s -X POST -H "X-API-Key: lab-only-api-key" -H 'Content-Type: application/json' http://127.0.0.1:8081/api/v1/servers/localhost/zones -d '{"name":"lab.example.net.","kind":"Native","nameservers":["ns1.example.net.","ns2.example.net."],"soa_edit_api":"DEFAULT"}' -o /dev/null -w 'HTTP %{http_code}\n'
HTTP 201

root@ns1:~# dig @203.0.113.53 lab.example.net SOA +short
ns1.example.net. hostmaster.lab.example.net. 2026102101 10800 3600 604800 3600

root@ns1:~# curl -s -H "X-API-Key: lab-only-api-key" http://127.0.0.1:8081/api/v1/servers/localhost/zones | grep -oE '"(name|kind)": *"[^"]+"|"serial": *[0-9]+' | paste - - - 
"kind": "Native"	"name": "example.net."	"serial": 2026102101
"kind": "Native"	"name": "internal.example.net."	"serial": 2026102102
"kind": "Native"	"name": "0.10.10.in-addr.arpa."	"serial": 2026102101
"kind": "Native"	"name": "lab.example.net."	"serial": 2026102101

root@ns1:~# curl -s -X DELETE -H "X-API-Key: lab-only-api-key" http://127.0.0.1:8081/api/v1/servers/localhost/zones/lab.example.net. -w 'HTTP %{http_code}\n'
HTTP 204

root@ns1:~# for z in example.net internal.example.net 0.10.10.in-addr.arpa; do pdnsutil set-kind $z primary; done; pdnsutil list-all-zones primary
example.net
internal.example.net
0.10.10.in-addr.arpa
  • Lines 1–2: Add a record: HTTP 204, done.
  • Lines 5–6: Live at once, and the serial went up by itself (SOA-EDIT-API).
  • Lines 8–12: A whole new zone, served immediately, with a correct SOA.
  • Lines 15–18: Every zone so far is kind Native.
  • Lines 20–21: The test zone removed again.
  • Lines 23–26: The real zones become Primary, so ns1 will send NOTIFY for them.