BackendSide DNS Manager

End-user Zone API

A focused reference for managing DNS zones and records over HTTP. Every UI action is also a JSON API call — with side-by-side cURL and PHP examples for every endpoint, ready to drop into your shell scripts, deploy pipelines, or dashboards.

Base URL   http://<your-host>:8084 Auth   X-API-Key: bsd_… Format   application/json

1.Generating an API token UI walkthrough

Every API call below carries an X-API-Key header. You generate that key once from the web UI and store it somewhere safe (a password manager, CI secret store, etc.) — the manager only displays it at creation time.

Step-by-step

  1. Open the web UI in your browser at http://<your-host>:8084/ and log in with your admin username and password.
  2. In the left sidebar, expand System and click API Tokens. The page header reads “API Tokens — Named tokens for programmatic access.”
  3. Click the orange + New token button in the top right.
  4. In the modal that appears, type a descriptive name in Token name (e.g. deploy-script, monitoring-bot, terraform). The name only identifies where the token is used — it has no effect on permissions.
  5. Click Create token. The modal flips to show the generated token (a string that begins with bsd_…) along with a Copy button.
  6. Copy the token immediately and paste it into your secret store. Once you close the dialog the plaintext value is gone forever — only its SHA-256 hash is kept on the server.
  7. The new token now appears in the table on the API Tokens page, showing its Created timestamp, Last used timestamp (updates on every authenticated request), and an Active status chip.
Revoking a token. If a token leaks or is no longer needed, click the trash icon at the end of its row on the API Tokens page and confirm. Any script using that token will lose access immediately. This action cannot be undone.
Fallback key. The static API_KEY value written into /etc/backendside-dns-manager/config.env at install time also works as an authentication header. Named tokens are preferred because they can be revoked individually and their last-used timestamp is tracked, but the static key remains as a bootstrap option.

Using your token

Send the token on every request as an X-API-Key HTTP header. That single header is the only authentication step for any of the endpoints documented below.

# Quick test: list zones
curl -H "X-API-Key: $KEY" \
  http://$HOST:8084/api/v1/zones
<?php
$host = 'http://your-host:8084';
$key  = 'bsd_your_token_here';

$ch = curl_init("$host/api/v1/zones");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        "X-API-Key: $key",
        "Accept: application/json",
    ],
]);
$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status !== 200) {
    throw new RuntimeException("API error $status: $body");
}
$zones = json_decode($body, true);
print_r($zones);

Conventions & errors

  • All request and response bodies are application/json.
  • Timestamps are ISO 8601 / RFC 3339, UTC.
  • Successful responses use 200 or 201. Validation failures use 400, missing items 404, conflicts 409, server errors 500.
  • Errors return a JSON body: { "error": "...", "detail": "...", "code": "..." } (the code field is machine-readable — see common values below).
  • BIND reload warnings. Any mutation that touches the DNS server will return its normal success body plus an extra "warning" string when the underlying rndc reload fails. The data write succeeded — DNS just hasn't picked it up. Surface the warning to your user; don't treat it as an error.
CodeMeaning
AUTH_REQUIREDNo X-API-Key header was sent.
INVALID_API_KEYThe token is wrong, revoked, or unknown.
RATE_LIMITEDPer-IP rate limit exceeded — slow down or whitelist your IP.
ZONE_NOT_FOUNDThe named zone is not managed by this server.
ZONE_ALREADY_EXISTSYou tried to create a zone that already exists.
ZONE_INVALID_NAMEThe zone name failed validation.
RECORD_NOT_FOUNDThe record (or deleted-record) ID doesn't exist in the zone.
BIND_RELOAD_FAILEDThe data write succeeded but the DNS reload didn't.

2.Zones

A zone is a managed DNS zone (e.g. example.com). The zone name is also its ID in URLs.

GET /api/v1/zones

List zones with optional pagination and substring search.

Query paramDefaultDescription
page11-based page number.
limit50Results per page (max 500).
qSubstring match on the zone name.

Response 200:

{
  "data": [
    { "id": "example.com", "name": "example.com",
      "type": "master", "status": "active", "dnssec": false }
  ],
  "meta": { "page": 1, "limit": 50, "total": 340 }
}
curl -H "X-API-Key: $KEY" \
  "http://$HOST:8084/api/v1/zones?page=1&limit=50&q=example"
$ch = curl_init("$host/api/v1/zones?page=1&limit=50&q=example");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ["X-API-Key: $key"],
]);
$resp  = json_decode(curl_exec($ch), true);
curl_close($ch);

foreach ($resp['data'] as $z) {
    echo $z['name'] . PHP_EOL;
}

POST /api/v1/zones

Create a new zone. Generates a fresh zone file with an SOA record and a default NS placeholder if none is supplied.

Request body:

{
  "name":        "example.com",
  "type":        "master",
  "description": "Marketing site",
  "nameserver":  "ns1.example.com",
  "admin_email": "[email protected]",
  "default_ttl": 3600
}

Only name is required. type defaults to "master" and default_ttl defaults to 3600.

Validation rules (the server returns 400 on violation):

  • Lowercase letters, digits, -, _ only.
  • Each label is 1–63 characters; the full name must not exceed 253.
  • Must contain at least one dot.
  • No leading or trailing hyphen on any label.

Response 201: the created zone object (or with "warning" on a reload failure).

curl -X POST http://$HOST:8084/api/v1/zones \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{
    "name":"example.com",
    "nameserver":"ns1.example.com",
    "admin_email":"[email protected]",
    "default_ttl":3600
  }'
$payload = json_encode([
    'name'        => 'example.com',
    'nameserver'  => 'ns1.example.com',
    'admin_email' => '[email protected]',
    'default_ttl' => 3600,
]);

$ch = curl_init("$host/api/v1/zones");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_HTTPHEADER     => [
        "X-API-Key: $key",
        "Content-Type: application/json",
    ],
]);
$resp   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status !== 201) {
    throw new RuntimeException("Create failed: $resp");
}
$zone = json_decode($resp, true)['zone'];

GET /api/v1/zones/:id

Fetch a single zone with all its records.

Response 200:

{
  "id": "example.com", "name": "example.com",
  "type": "master", "status": "active",
  "nameserver": "ns1.example.com.",
  "admin_email": "hostmaster.example.com.",
  "default_ttl": 3600,
  "records": [
    { "id": "1714686702123456000", "zone_id": "example.com",
      "name": "@", "type": "A", "value": "192.0.2.1", "ttl": 300 }
  ]
}

Errors: 404 — zone not found.

curl -H "X-API-Key: $KEY" \
  http://$HOST:8084/api/v1/zones/example.com
$zone = 'example.com';
$ch   = curl_init("$host/api/v1/zones/" . urlencode($zone));
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ["X-API-Key: $key"],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);

foreach ($body['records'] as $r) {
    printf("%-20s %-6s %s\n", $r['name'], $r['type'], $r['value']);
}

PUT /api/v1/zones/:id

Update zone metadata (description, primary NS, admin email, default TTL). Records are preserved and the SOA serial bumps automatically.

Request body:

{
  "description": "Updated description",
  "nameserver":  "ns2.example.com",
  "admin_email": "[email protected]",
  "default_ttl": 7200
}

Response 200: { "message": "zone updated", "warning": "..." } (warning optional).

curl -X PUT http://$HOST:8084/api/v1/zones/example.com \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{"default_ttl":7200}'
$ch = curl_init("$host/api/v1/zones/example.com");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_POSTFIELDS     => json_encode(['default_ttl' => 7200]),
    CURLOPT_HTTPHEADER     => [
        "X-API-Key: $key",
        "Content-Type: application/json",
    ],
]);
$resp = json_decode(curl_exec($ch), true);
curl_close($ch);

DELETE /api/v1/zones/:id

Permanently delete a zone (file plus its zones.conf entry).

This is destructive. The zone file and all records are removed from disk. Records can be restored individually within 48 hours via the deleted-records endpoints, but the zone itself cannot. Take an export first if you might need to recover.
curl -X DELETE -H "X-API-Key: $KEY" \
  http://$HOST:8084/api/v1/zones/example.com
$ch = curl_init("$host/api/v1/zones/example.com");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ["X-API-Key: $key"],
]);
curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status !== 200) { /* handle error */ }

GET /api/v1/zones/:id/export

Stream the raw .zone file as text/plain. Useful for backups, manual edits in an external editor, or moving the zone to another nameserver. The response sets Content-Disposition: attachment; filename="<zone>.zone".

curl -H "X-API-Key: $KEY" -O -J \
  http://$HOST:8084/api/v1/zones/example.com/export
$ch = curl_init("$host/api/v1/zones/example.com/export");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ["X-API-Key: $key"],
]);
$zoneText = curl_exec($ch);
curl_close($ch);

file_put_contents('example.com.zone', $zoneText);

3.Records

A record belongs to a zone. The server assigns each record an opaque string ID at creation time. Supported types: A, AAAA, CNAME, MX, TXT, NS, SRV, CAA, PTR.

GET /api/v1/records

List records. With no query string, returns every record in every zone. Filters compose with logical AND and are case-insensitive.

Query paramDescription
zone=<name>Limit to a single zone.
type=<TYPE>Filter by record type, e.g. A, MX, TXT.
name=<substr>Substring match on the record name.
q=<substr>Substring match across both name and value.
# All A records in one zone
curl -H "X-API-Key: $KEY" \
  "http://$HOST:8084/api/v1/records?zone=example.com&type=A"
$qs = http_build_query(['zone' => 'example.com', 'type' => 'A']);
$ch = curl_init("$host/api/v1/records?$qs");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ["X-API-Key: $key"],
]);
$records = json_decode(curl_exec($ch), true);
curl_close($ch);

POST /api/v1/records

Create a record in an existing zone.

FieldRequiredNotes
zone_idyesThe owning zone (must already exist).
nameyes@ for the apex, * or *.foo for wildcards, otherwise standard label rules.
typeyesOne of A, AAAA, CNAME, MX, NS, TXT, SRV, CAA, PTR.
valueyesValidated per type — see notes below.
ttlnoDefaults to 3600.
priorityMX/SRV onlyRequired for MX and SRV.
weight, portSRV onlyRequired for SRV.

Per-type value validation:

  • A — must parse as IPv4.
  • AAAA — must parse as IPv6.
  • CNAME, NS, PTR, MX, SRV — DNS hostname; trailing dot optional.
  • TXT — anything non-empty without NUL bytes.
  • CAA — length ≤ 1024 (no further parsing).
curl -X POST http://$HOST:8084/api/v1/records \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{
    "zone_id":"example.com",
    "name":"www",
    "type":"A",
    "value":"192.0.2.10",
    "ttl":300
  }'
function createRecord(string $host, string $key, array $rec): array {
    $ch = curl_init("$host/api/v1/records");
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($rec),
        CURLOPT_HTTPHEADER     => [
            "X-API-Key: $key",
            "Content-Type: application/json",
        ],
    ]);
    $body   = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($status !== 201) {
        throw new RuntimeException("Create failed ($status): $body");
    }
    return json_decode($body, true);
}

$row = createRecord($host, $key, [
    'zone_id' => 'example.com',
    'name'    => 'www',
    'type'    => 'A',
    'value'   => '192.0.2.10',
    'ttl'     => 300,
]);
echo "Created record id: {$row['id']}\n";

PUT /api/v1/records/:id?zone=<name>

Replace a record. The body shape matches POST minus the zone_id (it goes in the query string instead). All fields are required — this is a full replacement, not a patch.

curl -X PUT \
  "http://$HOST:8084/api/v1/records/1714686702123456000?zone=example.com" \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{"name":"www","type":"A","value":"192.0.2.99","ttl":600}'
$id   = '1714686702123456000';
$zone = 'example.com';
$url  = "$host/api/v1/records/$id?" . http_build_query(['zone' => $zone]);

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_POSTFIELDS     => json_encode([
        'name'  => 'www',
        'type'  => 'A',
        'value' => '192.0.2.99',
        'ttl'   => 600,
    ]),
    CURLOPT_HTTPHEADER => [
        "X-API-Key: $key",
        "Content-Type: application/json",
    ],
]);
$resp = json_decode(curl_exec($ch), true);
curl_close($ch);

DELETE /api/v1/records/:id?zone=<name>

Delete a single record. The zone query parameter is required. The deleted entry is preserved in the deleted-records sidecar for 48 hours so you can restore it.

curl -X DELETE -H "X-API-Key: $KEY" \
  "http://$HOST:8084/api/v1/records/1714686702123456000?zone=example.com"
$id   = '1714686702123456000';
$zone = 'example.com';
$url  = "$host/api/v1/records/$id?" . http_build_query(['zone' => $zone]);

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ["X-API-Key: $key"],
]);
curl_exec($ch);
curl_close($ch);

POST /api/v1/records/bulk

Create many records in one zone with a single BIND reload at the end. Invalid individual records are skipped (non-fatal) — if every record is invalid the request returns 400.

Response 201:

{
  "created": 3,
  "records": [ { /* DNSRecord */ }, ... ],
  "skipped": [],
  "warning": "..."
}
curl -X POST http://$HOST:8084/api/v1/records/bulk \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{
    "zone_id":"example.com",
    "records":[
      {"name":"@",   "type":"A",     "value":"192.0.2.1", "ttl":3600},
      {"name":"www", "type":"CNAME", "value":"@",         "ttl":3600},
      {"name":"@",   "type":"MX",    "value":"mail",      "ttl":3600, "priority":10}
    ]
  }'
$payload = json_encode([
    'zone_id' => 'example.com',
    'records' => [
        ['name' => '@',   'type' => 'A',     'value' => '192.0.2.1', 'ttl' => 3600],
        ['name' => 'www', 'type' => 'CNAME', 'value' => '@',         'ttl' => 3600],
        ['name' => '@',   'type' => 'MX',    'value' => 'mail',      'ttl' => 3600, 'priority' => 10],
    ],
]);

$ch = curl_init("$host/api/v1/records/bulk");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_HTTPHEADER     => [
        "X-API-Key: $key",
        "Content-Type: application/json",
    ],
]);
$resp = json_decode(curl_exec($ch), true);
curl_close($ch);

echo "Created: {$resp['created']}\n";
if (!empty($resp['skipped'])) {
    foreach ($resp['skipped'] as $reason) {
        echo "Skipped: $reason\n";
    }
}

POST /api/v1/records/bulk-delete

Delete many records in one zone with a single BIND reload at the end.

curl -X POST http://$HOST:8084/api/v1/records/bulk-delete \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{
    "zone":"example.com",
    "ids":["1714686702123456000","1714686703987654321"]
  }'
$payload = json_encode([
    'zone' => 'example.com',
    'ids'  => ['1714686702123456000', '1714686703987654321'],
]);

$ch = curl_init("$host/api/v1/records/bulk-delete");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_HTTPHEADER     => [
        "X-API-Key: $key",
        "Content-Type: application/json",
    ],
]);
$resp = json_decode(curl_exec($ch), true);
curl_close($ch);
echo "Deleted: {$resp['deleted']}\n";

4.Restore & import

GET /api/v1/zones/:id/deleted-records

List recoverable records for a zone. Records remain available for 48 hours after deletion, capped at the 50 most recent entries. Older or excess entries are pruned automatically.

Response 200:

[
  {
    "id":         "1714686702123456000",
    "name":       "www",
    "type":       "A",
    "value":      "192.0.2.10",
    "ttl":        3600,
    "deleted_at": "2026-05-02T03:11:42Z",
    "deleted_by": "admin"
  }
]
curl -H "X-API-Key: $KEY" \
  http://$HOST:8084/api/v1/zones/example.com/deleted-records
$ch = curl_init("$host/api/v1/zones/example.com/deleted-records");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ["X-API-Key: $key"],
]);
$deleted = json_decode(curl_exec($ch), true);
curl_close($ch);

foreach ($deleted as $d) {
    echo "{$d['deleted_at']}  {$d['name']} {$d['type']} {$d['value']}\n";
}

POST /api/v1/zones/:id/deleted-records/:rid/restore

Re-insert a deleted record back into the zone file and reload BIND. The restored record gets a fresh created_at / updated_at and is removed from the deleted-records list on success.

curl -X POST -H "X-API-Key: $KEY" \
  http://$HOST:8084/api/v1/zones/example.com/deleted-records/1714686702123456000/restore
$zone = 'example.com';
$rid  = '1714686702123456000';
$url  = "$host/api/v1/zones/$zone/deleted-records/$rid/restore";

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ["X-API-Key: $key"],
]);
$resp = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($resp['record']);

POST /api/v1/admin/import-text

Import an existing BIND zone file by submitting it as raw text. The server validates it with named-checkzone before writing to disk and reloading BIND. Use this when migrating a zone in from another nameserver.

Request body:

{
  "name":    "example.com",
  "content": "$TTL 3600\n@ IN SOA ns1.example.com. hostmaster.example.com. (\n  2026050201 3600 600 1209600 3600 )\n  IN NS ns1.example.com.\nwww IN A 192.0.2.10\n"
}

Response 200: { "zone": "example.com", "message": "zone imported", "warning": "..." } (warning optional).

Errors:

  • 400invalid zone file with the named-checkzone output in detail.
  • 500 — write or reload failure.
ZONE_TEXT=$(cat example.com.zone)
curl -X POST http://$HOST:8084/api/v1/admin/import-text \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d "$(jq -n --arg n example.com --arg c "$ZONE_TEXT" \
        '{name:$n, content:$c}')"
$content = file_get_contents('example.com.zone');
$payload = json_encode([
    'name'    => 'example.com',
    'content' => $content,
]);

$ch = curl_init("$host/api/v1/admin/import-text");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_HTTPHEADER     => [
        "X-API-Key: $key",
        "Content-Type: application/json",
    ],
]);
$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status === 400) {
    $err = json_decode($body, true);
    throw new RuntimeException("Invalid zone: " . $err['detail']);
}

5.Zone templates

Reusable record sets to apply when you create a zone. Four templates ship built-in (Basic Web, Email, Minimal, Full Stack); custom templates can be added from the UI.

GET /api/v1/templates

List all templates. Combine with the bulk-create endpoint to apply a template to a freshly-created zone — see the Apply a template pattern below.

Response 200:

[
  {
    "id":          "basic-web",
    "name":        "Basic Web",
    "description": "A record at apex + www CNAME",
    "builtin":     true,
    "records": [
      { "name": "@",   "type": "A",     "value": "0.0.0.0", "ttl": 3600 },
      { "name": "www", "type": "CNAME", "value": "@",       "ttl": 3600 }
    ]
  }
]
Apply a template. The API has no dedicated “apply template” endpoint — instead, fetch the template, then bulk-create its records into a new zone:
# 1. Create the zone
curl -X POST http://$HOST:8084/api/v1/zones \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{"name":"example.com"}'

# 2. Pull the template's records
RECORDS=$(curl -s -H "X-API-Key: $KEY" \
  http://$HOST:8084/api/v1/templates \
  | jq '[.[] | select(.id=="basic-web") | .records[]]')

# 3. Bulk-create them in the zone
curl -X POST http://$HOST:8084/api/v1/records/bulk \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d "{\"zone_id\":\"example.com\",\"records\":$RECORDS}"
// 1. Create the zone
createZone($host, $key, ['name' => 'example.com']);

// 2. Fetch templates and pick one
$ch = curl_init("$host/api/v1/templates");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ["X-API-Key: $key"],
]);
$templates = json_decode(curl_exec($ch), true);
curl_close($ch);

$basicWeb = null;
foreach ($templates as $t) {
    if ($t['id'] === 'basic-web') { $basicWeb = $t; break; }
}

// 3. Bulk-apply its records
$payload = json_encode([
    'zone_id' => 'example.com',
    'records' => $basicWeb['records'],
]);
$ch = curl_init("$host/api/v1/records/bulk");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_HTTPHEADER     => [
        "X-API-Key: $key",
        "Content-Type: application/json",
    ],
]);
$resp = json_decode(curl_exec($ch), true);
curl_close($ch);

6.DNSSEC

Three endpoints wrap BIND's built-in dnssec-policy machinery. Enabling signing flips a flag and lets BIND auto-generate the KSK and ZSK a few seconds later.

GET /api/v1/dnssec/:zone

Current signing state. The endpoint always returns 200 when the zone exists — check the enabled field rather than the status code.

Response 200 — signed and ready:

{
  "zone":     "example.com",
  "enabled":  true,
  "policy":   "default",
  "key_dir":  "/var/cache/bind",
  "keys":       [ /* DNSKEYs */ ],
  "ds_records": [
    {
      "tag": 41284, "algorithm": 13, "digest_algorithm": 2,
      "digest":  "AB12CD34...",
      "display": "41284 13 2 AB12CD34..."
    }
  ]
}
curl -H "X-API-Key: $KEY" \
  http://$HOST:8084/api/v1/dnssec/example.com
$ch = curl_init("$host/api/v1/dnssec/example.com");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ["X-API-Key: $key"],
]);
$state = json_decode(curl_exec($ch), true);
curl_close($ch);

if ($state['enabled'] && !empty($state['ds_records'])) {
    echo "DS to publish at registrar:\n";
    foreach ($state['ds_records'] as $ds) {
        echo "  example.com. IN DS {$ds['display']}\n";
    }
}

POST /api/v1/dnssec/:zone

Enable signing for the zone. No body required. The flag is saved immediately; BIND generates keys within a few seconds and starts serving signed responses.

Publish the DS records at your registrar. After signing, poll GET /api/v1/dnssec/:zone until ds_records is populated, then copy each entry's display string into your domain registrar's DNSSEC settings. Without that step, validating resolvers will not see the chain of trust.
curl -X POST -H "X-API-Key: $KEY" \
  http://$HOST:8084/api/v1/dnssec/example.com
$ch = curl_init("$host/api/v1/dnssec/example.com");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ["X-API-Key: $key"],
]);
$resp = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $resp['message'] . PHP_EOL;

DELETE /api/v1/dnssec/:zone

Disable signing. BIND stops signing on reload but the existing key files remain in /var/cache/bind — clean them up by hand if desired.

Remove the DS at your registrar first. A DS at the parent that no longer matches a published DNSKEY causes SERVFAIL responses for validating resolvers. Coordinate the order: pull the DS first, wait for the parent's TTL, then disable signing.
curl -X DELETE -H "X-API-Key: $KEY" \
  http://$HOST:8084/api/v1/dnssec/example.com
$ch = curl_init("$host/api/v1/dnssec/example.com");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ["X-API-Key: $key"],
]);
curl_exec($ch);
curl_close($ch);

7.Tools

POST /api/v1/admin/lookup

Run a dig query from the manager host so you can verify your changes are visible from the same machine BIND is running on. Useful after creating or updating a record.

Request body:

{
  "name":   "www.example.com",
  "type":   "A",
  "server": "127.0.0.1"
}
FieldDefaultNotes
namerequiredThe DNS name to query.
typeAOne of A, AAAA, MX, TXT, NS, CNAME, SOA, PTR, CAA, SRV, ANY (case-insensitive).
server127.0.0.1Resolver to query, passed as @<server>.

Response is always 200 with { "output": "...", "error": false }. NXDOMAIN, SERVFAIL, etc. come back with error: true and the dig output in output — display it to the user as-is. Queries are hard-capped at 10 seconds.

curl -X POST http://$HOST:8084/api/v1/admin/lookup \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{"name":"www.example.com","type":"A"}'
$payload = json_encode([
    'name' => 'www.example.com',
    'type' => 'A',
]);

$ch = curl_init("$host/api/v1/admin/lookup");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_HTTPHEADER     => [
        "X-API-Key: $key",
        "Content-Type: application/json",
    ],
]);
$resp = json_decode(curl_exec($ch), true);
curl_close($ch);

echo $resp['output'];

POST /api/v1/bind/reload

Force a BIND reload. Every record/zone mutation already triggers an automatic reload, but call this if a previous request returned a warning field (the data write succeeded but the reload failed) and you've corrected the underlying issue.

The endpoint runs named-checkconf first and refuses to reload if the BIND configuration is invalid — so a bad edit returns 500 with the error in detail while BIND keeps serving the previous (working) config.

curl -X POST -H "X-API-Key: $KEY" \
  http://$HOST:8084/api/v1/bind/reload
$ch = curl_init("$host/api/v1/bind/reload");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ["X-API-Key: $key"],
]);
$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status !== 200) {
    $err = json_decode($body, true);
    throw new RuntimeException("Reload failed: " . ($err['detail'] ?? $body));
}