API Endpoints¶
This page is a task-shaped map of the API, not the complete list. The API Reference is generated from the source and documents every operation with its full request and response schemas - use that when you need the authoritative answer, and this page when you want to find the right endpoint for a job.
A running instance can also serve the same specification interactively.
Interactive API documentation¶
When api.docs_enabled = true in config/settings.php, every Poweradmin
instance hosts its own Swagger UI:
From there you can:
- Browse every endpoint grouped by tag (Zones, Records, Users, ...)
- See the exact JSON request and response schemas
- Paste in an API key via the Authorize button
- Issue real requests against the running instance and inspect the response
For production deployments, leave docs_enabled off and use a staging
instance for exploration.
High-level endpoint map (API v2)¶
API v2 is the recommended version. All paths are prefixed with /api/v2.
Zones¶
| Method | Path | Purpose |
|---|---|---|
GET |
/zones |
List zones; id is the identifier the other zone endpoints take (canonical_id carries the same value) |
POST |
/zones |
Create zone |
GET |
/zones/{id} |
Get zone |
PUT |
/zones/{id} |
Update zone |
DELETE |
/zones/{id} |
Delete zone |
GET |
/zones/{id}/owners |
List zone owners (v4.2.0+) |
POST |
/zones/{id}/owners |
Add owner(s), supports batch (v4.2.0+) |
DELETE |
/zones/{id}/owners/{user_id} |
Remove owner (v4.2.0+) |
From v4.2.6, v4.3.5, v4.4.1 and v4.5.0, changing a zone's name, type or master through
PUT /zones/{id} requires the zone metadata permission (zone_meta_edit_own or
zone_meta_edit_others), the same gate as the web edit form; record content permissions alone
return 403. A value that matches what is stored is not treated as a change, so clients that
resend every field on update keep working.
In API backend mode, POST /zones/{id}/owners answers 409 with "Owners cannot be added to this
zone: its ID is shared with another zone" when the id belongs to two zones. See
Zone IDs Shared by Two Zones.
Setting the serial policy on create (v4.5.0+)¶
POST /zones accepts an optional soa_edit_api string that sets the zone's
SOA-EDIT-API serial policy:
Omit the field, or send an empty string, to apply the dns.soa_edit_api
configuration default. Send OFF to disable the policy for this zone
specifically; in API backend mode that also clears the default PowerDNS applies.
The accepted values are DEFAULT, INCREASE, EPOCH, SOA-EDIT,
SOA-EDIT-INCREASE and OFF, narrowed to whatever the dns.soa_edit_api_options
configuration list allows. A value outside that set returns 400, and so does a
non-string value. This differs deliberately from the add-zone form, which silently
drops an unoffered value and falls back to the default.
Two limits worth knowing before scripting against it. The field is create-only:
PUT /zones/{id} does not accept it, and an existing zone's policy is changed
through PUT /zones/{id}/metadata/SOA-EDIT-API instead. And a SLAVE zone takes
its serial from the primary, so a value accepted there is validated but never
applied.
Zone metadata and DNSSEC¶
| Method | Path | Purpose |
|---|---|---|
GET |
/zones/{id}/metadata |
List all metadata kinds for a zone (v4.3.0+) |
GET |
/zones/{id}/metadata/{kind} |
Get one metadata kind (v4.3.0+) |
PUT |
/zones/{id}/metadata/{kind} |
Set one metadata kind (v4.3.0+) |
DELETE |
/zones/{id}/metadata/{kind} |
Remove one metadata kind (v4.3.0+) |
GET |
/zones/{id}/dnssec |
Get DNSSEC status and keys (v4.5.0+) |
POST |
/zones/{id}/dnssec |
Sign or unsign the zone (v4.5.0+) |
GET |
/zones/{id}/dnssec/keys |
List the zone's keys with their DNSKEY and DS (v4.5.0+) |
POST |
/zones/{id}/dnssec/keys |
Add a key (type, algorithm, bits, optional active) (v4.5.0+) |
POST |
/zones/{id}/dnssec/keys/import |
Import a key from a BIND-format private key (type, privatekey, optional active) (v4.6.0+) |
GET |
/zones/{id}/dnssec/keys/{key_id} |
Get one key (v4.5.0+) |
PATCH |
/zones/{id}/dnssec/keys/{key_id} |
Activate or deactivate a key (v4.5.0+) |
DELETE |
/zones/{id}/dnssec/keys/{key_id} |
Delete a key (v4.5.0+) |
POST |
/zones/{id}/dnssec/rectify |
Rectify a signed primary zone (v4.5.0+) |
Reading keys needs view access to the zone; the other key endpoints need zone_dnssec_manage_own
on the zone (or administrator rights) and the PowerDNS API. From 4.6.0, adding, importing,
updating and deleting keys also needs view access to the zone, as on the web DNSSEC pages, and
answers 403 without it. For API key scopes, adding a key is a
create operation and rectifying is an update. New keys are created inactive unless active is
true, as in the web UI. A key's type is the role PowerDNS derives on every read (ksk or
zsk only while an active SEP and an active non-SEP key share an algorithm, otherwise csk); from
4.6.0 the sep field reports the stored secure-entry-point flag. Rectify is refused for Secondary
and Consumer zones. When PowerDNS
cannot be reached, the status, sign/unsign, key and rectify endpoints answer 502 rather than
reporting the zone as unsigned.
An imported key must use an algorithm offered for new keys on the connected PowerDNS version.
Any other algorithm answers 400 with "The private key uses an unsupported algorithm (one of:
...)", listing the accepted ones.
Server status (v4.5.0+)¶
| Method | Path | Purpose |
|---|---|---|
GET |
/server/status |
PowerDNS version, uptime and statistics, for monitoring |
Requires the server_status_view permission (administrators have it implicitly) and the
PowerDNS API to be configured. It works with read-only keys; keys restricted to specific zones
get 403 because the status is not tied to a zone. The endpoint does not depend on
interface.show_pdns_status.
?metrics=uptime,udp-queriesreturns only the named metrics?include=slavesalso probes the configured autoprimary servers, which is slower. It needssupermaster_viewas well, since it lists their addresses501means the PowerDNS API is not configured,503means PowerDNS is not reachable, so a monitoring check can rely on the status code alone
{
"success": true,
"message": "Server status retrieved successfully",
"data": {
"running": true,
"server_id": "localhost",
"daemon_type": "authoritative",
"version": "4.9.4",
"uptime_seconds": 86400,
"metrics": { "udp-queries": "1234", "uptime": "86400" }
}
}
Dynamic DNS¶
| Method | Path | Purpose |
|---|---|---|
POST |
/dynamic-dns |
Update a record from a dynamic DNS client (v4.5.0+) |
This is the API v2 equivalent of the standalone dynamic_update.php script
described in Dynamic DNS. Both remain
supported; the standalone script keeps the dyndns2-compatible query-string
interface that routers expect.
Change requests (v4.6.0+)¶
| Method | Path | Purpose |
|---|---|---|
POST |
/zones/{id}/change-requests |
File a change request for a zone |
GET |
/change-requests |
List change requests in the caller's review scope plus the caller's own |
GET |
/change-requests/{id} |
Get a change request with its stale actions |
POST |
/change-requests/{id}/approve |
Approve and apply a change request |
POST |
/change-requests/{id}/reject |
Reject a change request |
DELETE |
/change-requests/{id} |
Cancel the caller's own pending change request |
All of them answer 404 while approval.enabled is off. When the caller's changes to a
zone are routed through review, the record write endpoints below answer 403 with
"Changes to this zone require approval; create a change request instead". See
Change Requests.
Records¶
| Method | Path | Purpose |
|---|---|---|
GET |
/zones/{id}/records |
List records in a zone |
POST |
/zones/{id}/records |
Create record |
GET |
/zones/{id}/records/{recordId} |
Fetch a single record |
PUT |
/zones/{id}/records/{recordId} |
Update record |
DELETE |
/zones/{id}/records/{recordId} |
Delete record |
POST |
/zones/{id}/records/bulk |
Bulk create records |
GET |
/zones/{id}/rrsets |
List RRsets |
GET |
/zones/{id}/rrsets/{name}/{type} |
Get a specific RRset |
When ttl is omitted on a record create call, Poweradmin applies the
configured default (dns.ttl_reverse for PTR records in reverse zones when
set, dns.ttl otherwise). See
DNS settings for details. A ttl of 0
is accepted (do not cache); negative values and fractional strings such as
"0.5" are refused with 400.
Records in Secondary and Consumer zones are read-only - they replicate
from a primary. Create, update, delete, and bulk-write calls against such a
zone return 403 with a message stating the zone is read-only (rather than a
permission error). Edit the records on the primary instead.
Users¶
| Method | Path | Purpose |
|---|---|---|
GET |
/users |
List users |
POST |
/users |
Create user |
GET |
/users/{id} |
Get user |
PUT |
/users/{id} |
Update user |
DELETE |
/users/{id} |
Delete user |
Writing perm_templ is gated separately from the write itself, and a rejected
assignment returns 403:
- Callers holding
user_is_ueberusermay assign any template. - Callers holding
user_edit_templ_permmay assign any template that does not grantuser_is_ueberuser, and may not change their own account's template unless they also holduser_edit_others. - Everyone else may not send
perm_templat all.
Omitting perm_templ on create assigns the least-privileged non-superuser template,
whoever the caller is; it no longer falls back to template id 1. If no such template
exists, the request fails rather than granting administrator rights. This applies from
v4.2.6, v4.3.5, v4.4.1 and v4.5.0 - on earlier releases, omitting the field created an
administrator, so check any client that relies on the old default.
Permission template and groups on reads (v4.5.0+)¶
User reads carry the assigned permission template by ID and by name, plus the groups the user belongs to:
{
"user_id": 5,
"username": "svc-acme",
"perm_templ": 2,
"perm_templ_name": "Zone Manager",
"groups": [{ "id": 3, "name": "dns-operators" }]
}
perm_templ_name is null when the stored template no longer exists or is a
group template rather than a user template.
Assigning groups on create (v4.5.0+)¶
POST /users accepts an optional groups array holding integer group IDs,
exact group names, or a mix of both:
The response lists the memberships that were actually created, so a caller does not have to assume the request took full effect:
Rules worth knowing before you script against it:
- Sending
groupsrequiresuser_is_ueberuser, because a group carries its own permission template. Callers without it get403, even when they may otherwise create users. - Names are matched exactly, including case and accents.
"Viewers"resolves,"viewers"does not. This is deliberate: MySQL's default collation would otherwise accept variants that PostgreSQL and SQLite reject. - Numeric strings are treated as names, not IDs. Send
3to reference group 3;"3"is looked up as a group named3and will normally fail. - If any entry does not resolve to a group, the whole request returns
400and no user is created. - Naming the same group more than once, whether by ID and by name or by repeating one value, still creates a single membership.
- The array may hold up to 50 groups. A longer list returns
400and creates nothing.
Membership changes after creation go through
POST/DELETE /groups/{id}/members; PUT /users/{id} does not accept groups.
Groups (v4.2.0+)¶
| Method | Path | Purpose |
|---|---|---|
GET |
/groups |
List groups |
POST |
/groups |
Create group |
GET |
/groups/{id} |
Get group |
PUT |
/groups/{id} |
Update group |
DELETE |
/groups/{id} |
Delete group |
GET |
/groups/{id}/members |
List group members |
POST |
/groups/{id}/members |
Add member |
DELETE |
/groups/{id}/members/{user_id} |
Remove member |
GET |
/groups/{id}/zones |
List zones assigned to group |
POST |
/groups/{id}/zones |
Assign zone to group |
DELETE |
/groups/{id}/zones/{zone_id} |
Remove zone from group |
Permission templates¶
| Method | Path | Purpose |
|---|---|---|
GET |
/permission-templates |
List templates |
POST |
/permission-templates |
Create template |
GET |
/permission-templates/{id} |
Get template |
PUT |
/permission-templates/{id} |
Update template |
DELETE |
/permission-templates/{id} |
Delete template |
GET |
/permissions |
List available permission flags |
GET |
/permissions/{id} |
Get permission flag details |
Zone templates (v4.2.0+)¶
| Method | Path | Purpose |
|---|---|---|
GET |
/zone-templates |
List zone templates |
POST |
/zone-templates |
Create zone template |
GET |
/zone-templates/{id} |
Get zone template |
PUT |
/zone-templates/{id} |
Update zone template |
DELETE |
/zone-templates/{id} |
Delete zone template |
GET |
/zone-templates/{id}/records |
List template records |
POST |
/zone-templates/{id}/records |
Add template record |
PUT |
/zone-templates/{template_id}/records/{id} |
Update template record |
DELETE |
/zone-templates/{template_id}/records/{id} |
Delete template record |
From v4.2.6, v4.3.5, v4.4.1 and v4.5.0, adding or updating a template record validates the
content against the record type and returns 400 with the validator message when it fails.
See Record validation for the rules,
including where MX and SRV priority belongs.
From the same releases, GET /zone-templates and GET /zone-templates/{id}/records are also
readable with zone_master_add or zone_slave_add, matching the add-zone form, which offers
templates to anyone who may add a zone. Writing a template still needs the template permissions.
Health endpoints (v4.5.0+)¶
These sit outside the versioned API and outside the API key model. They take no
credentials, are disabled by default, and return 404 while disabled. Paths are
absolute, not relative to /api/v2.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/health |
Readiness: database and PowerDNS API reachability. 200 healthy, 503 otherwise |
GET |
/ping |
Liveness: returns ok and checks nothing else |
They are not part of the OpenAPI specification, and they answer regardless of whether
api.enabled is set. See Health Checks.
API v1 endpoints¶
Removed in 4.5.0. On 4.5.0 and later, every /api/v1 path answers
410 Gone and points at /api/v2.
On 4.2.x-4.4.x, v1 is still available under /api/v1. The endpoint surface is
similar but the response envelope is less consistent and several v2 features
(RRsets, bulk records, zone owners, groups, zone templates) are not available.
See API Configuration for
the v1 endpoint list, and migrate to v2 before upgrading to 4.5.0.
Paging, sorting and filtering¶
From 4.6.0, the v2 list endpoints in the table below accept page and per_page, and most
also take sort and a few filters. Before 4.6.0 only /zones and /users paged, and none
sorted or filtered. Short lists such as /zones/{id}/owners always come back whole.
Paging¶
Without per_page (or with per_page=0) the whole list comes back and there is no
pagination object. With it, page starts at 1 (a smaller value is read as 1), per_page
is capped at 10000, and the response carries a pagination object at the top level, next to
data:
{
"success": true,
"data": { "zones": [ ... ] },
"pagination": { "current_page": 2, "per_page": 50, "total": 150, "last_page": 3 }
}
A page past the end returns an empty list together with the pagination object, so a client
can stop when current_page reaches last_page.
Warning: The parameter is
per_page, notlimit. An unrecognised parameter is ignored rather than rejected, so?limit=50silently returns every row.
From 4.6.0, a query parameter sent as an array, such as ?type[]=A, answers 400 with
"Query parameter 'type' must be a single value". This applies to every v2 endpoint and to the
internal API.
Sorting¶
sort takes comma-separated field names, each with an optional :asc or :desc suffix
(ascending by default), up to five fields:
A field the endpoint does not offer, a field given twice, or a direction other than asc or
desc answers 400 with a message that lists the allowed fields.
Without sort, most lists come in the database order of their name column; the exceptions are
noted in the table below. Lists that are sorted in memory (records, RRsets, groups, templates,
permissions) compare text fields case-insensitively in natural order, so host2 sorts before
host10. /zones applies that natural order only for sort=name, where numeric prefixes are
compared as numbers. /users sorts case-insensitively but not naturally.
Filters¶
Every filter is a query parameter and combines with sort, page and per_page. The
pagination.total counts the filtered list. q is a case-insensitive substring match.
| Endpoint | Sort fields | Filters |
|---|---|---|
GET /zones |
name, type, id |
name (exact zone name), q (zone name) |
GET /zones/{id}/records |
name, type, content, ttl, priority |
type (record type), name, content, q (name or content). Default order: type, then name (as PowerDNS returns them in API backend mode) |
GET /zones/{id}/rrsets |
name, type, ttl |
type (record type), name, content, q (name or content); content and q keep an RRset when any of its records matches. Default order: type, then name (as PowerDNS returns them in API backend mode) |
GET /users |
id, username, fullname, email |
q (username, full name, email or description), username and email (exact lookups, see below). Default order: id |
GET /groups |
id, name, member_count, zone_count, created_at |
q (name or description) |
GET /groups/{id}/members |
user_id, username, fullname, email, joined_at |
q (username, full name or email). Default order: newest member first |
GET /groups/{id}/zones |
zone_id, zone_name, zone_type, created_at |
q (zone name). Default order: newest assignment first |
GET /zone-templates |
id, name, zones_linked |
q (name or description) |
GET /zone-templates/{id}/records |
name, type, content, ttl, priority |
type (record type, case-insensitive), name, content, q (name or content) |
GET /permissions |
id, name |
q (name or description) |
GET /permission-templates |
id, name, template_type |
q (name or description), type (user or group; anything else answers 400) |
GET /change-requests |
none | status (pending by default; approved, rejected, cancelled, failed or all; anything else answers 400), zone_id. Default order: newest first |
name and content on the record lists are case-insensitive substring matches like q;
q searches both columns at once.
GET /users?username=alice and GET /users?email=alice@example.com look up one user. The
list then holds that user or is empty, page, per_page and q are ignored, sort is
checked but not applied, no pagination object is returned, and username wins when both are
given.
GET /change-requests ignores sort; it always lists newest first. Without per_page it
returns at most 10000 requests, and when more match, the response carries a pagination
object (per_page 10000, last_page above 1) so the client knows to page on with
per_page and page.
Examples¶
The second page of 50 zones, newest id first:
curl -H "X-API-Key: your-api-key-here" \
"https://poweradmin.example.com/api/v2/zones?sort=id:desc&page=2&per_page=50"
The A records of a zone that mention mail, by name:
curl -H "X-API-Key: your-api-key-here" \
"https://poweradmin.example.com/api/v2/zones/42/records?type=A&q=mail&sort=name"
Group permission templates, ordered by name:
curl -H "X-API-Key: your-api-key-here" \
"https://poweradmin.example.com/api/v2/permission-templates?type=group&sort=name"
Related documentation¶
- Overview - what the API can do, response envelope
- Authentication - API keys and Basic Auth
- API Configuration - enabling the API, web server requirements, security, request/response examples
- Dynamic DNS with cURL - the separate,
simpler
dynamic_update.phpendpoint for IP-update scripts