Skip to content

Managing DNS with the API

This page covers the zone and record endpoints with curl examples. All requests need a bearer token; see the API overview. Writes require the editor or admin role.

Terminal window
curl -sk https://dtm.internal:8443/api/v1/zones \
-H "Authorization: Bearer $DTM_TOKEN"
Terminal window
curl -sk -X POST https://dtm.internal:8443/api/v1/zones \
-H "Authorization: Bearer $DTM_TOKEN" \
-H "X-DTM-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{
"name": "example.internal.",
"default_ttl": 300,
"soa": {"mname": "ns1.example.internal.", "rname": "hostmaster.example.internal."}
}'

soa is optional; DTM fills sensible defaults. An optional deletion_lock (bool) sets the zone’s deletion lock explicitly; omit it and the zone inherits the cluster default (see delete guardrails).

Terminal window
curl -sk https://dtm.internal:8443/api/v1/zones/<zone-id> \
-H "Authorization: Bearer $DTM_TOKEN"
curl -sk -X PUT https://dtm.internal:8443/api/v1/zones/<zone-id> \
-H "Authorization: Bearer $DTM_TOKEN" \
-H "X-DTM-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{"default_ttl": 600}'
curl -sk -X DELETE https://dtm.internal:8443/api/v1/zones/<zone-id> \
-H "Authorization: Bearer $DTM_TOKEN" -H "X-DTM-CSRF: 1"

Deleting a zone removes all of its records. If the zone carries a deletion lock, the DELETE returns 409 and nothing is deleted; clear the lock first, then delete:

Terminal window
curl -sk -X PUT https://dtm.internal:8443/api/v1/zones/<zone-id> \
-H "Authorization: Bearer $DTM_TOKEN" \
-H "X-DTM-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{"deletion_lock": false}'
Terminal window
curl -sk https://dtm.internal:8443/api/v1/zones/<zone-id>/records \
-H "Authorization: Bearer $DTM_TOKEN"

A load-balanced, health-checked A record:

Terminal window
curl -sk -X POST https://dtm.internal:8443/api/v1/zones/<zone-id>/records \
-H "Authorization: Bearer $DTM_TOKEN" \
-H "X-DTM-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{
"name": "web",
"type": "A",
"ttl": 60,
"values": [{"value": "10.0.1.10"}, {"value": "10.0.1.11"}],
"lb_policy": {"method": "round-robin"},
"health_check_template": {
"protocol": "http", "port": 80, "path": "/healthz",
"interval_seconds": 15, "timeout_seconds": 5
}
}'

Supported types: A, AAAA, CNAME, MX, NS, PTR, SRV, TXT, CAA, ALIAS. For CAA, set tag (issue, issuewild, or iodef) and carry the CAA flag (0 to 255) in priority; an empty value is valid and means deny all issuance. For A, values must be valid IPv4; for AAAA, valid IPv6. CNAME cannot coexist with other types at the same name. The lb_policy object takes method (round-robin, failover, latency, region, geo-failover, weighted), client_affinity, affinity_ttl_seconds, failover_order, region_preference, and, for weighted, a value_weights map of record value to percentage (an unknown method, a weight outside 0 to 100, listed weights summing past 100, or a key matching no value is rejected); see load-balancing methods for what each does, and authenticated health probes for probing secured backends. ttl accepts 0 (inherit the zone default) up to 2147483647 seconds, the RFC 2181 ceiling; a larger value is rejected with 400 on create and update.

Records are addressed by their own ID (not the zone) for read, update, delete:

Terminal window
curl -sk https://dtm.internal:8443/api/v1/records/<record-id> \
-H "Authorization: Bearer $DTM_TOKEN"
curl -sk -X PUT https://dtm.internal:8443/api/v1/records/<record-id> \
-H "Authorization: Bearer $DTM_TOKEN" \
-H "X-DTM-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{"ttl": 30, "values": [{"value": "10.0.1.10"}]}'
curl -sk -X DELETE https://dtm.internal:8443/api/v1/records/<record-id> \
-H "Authorization: Bearer $DTM_TOKEN" -H "X-DTM-CSRF: 1"

Delete guardrails: protected records and bulk delete

Section titled “Delete guardrails: protected records and bulk delete”

Mark a record "protected": true (on create or update) and a plain DELETE returns 409; retry with ?force=true to delete it anyway (audited distinctly). Use it on records a sync tool must never remove.

Protection covers replacement as well as deletion, since replacing a record deletes the incumbent. Creating a colliding record at a protected name, or renaming another record onto it with PUT, answers 409 naming the protected record. The bulk zone import and the Azure Private DNS import skip a protected incumbent and report it in the response instead of overwriting.

Bulk deletion has its own endpoint and cap:

Terminal window
curl -sk -X POST https://dtm.internal:8443/api/v1/zones/<zone-id>/records/bulk-delete \
-H "Authorization: Bearer $DTM_TOKEN" \
-H "X-DTM-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{"ids": ["rec-...", "rec-..."], "force": false}'

The response reports deleted, protected_skipped, and not_found. A batch larger than the cap (default 100) is refused whole with 422 and nothing is deleted; the cap is adjustable by an admin via PUT /api/v1/config/delete-guardrails (bulk_delete_cap). Note the two force spellings: bulk delete takes "force" in the JSON body, single delete takes ?force=true as a query parameter.

The same delete-guardrails setting carries default_zone_deletion_lock: when true, newly created zones start life locked unless the create says otherwise. It applies at creation only, never retroactively; see deletion locks.

DTM can bulk import and export zones, which is how you migrate existing DNS in.

Terminal window
# One zone as an RFC 1035 zone file
curl -sk https://dtm.internal:8443/api/v1/zones/<zone-id>/export \
-H "Authorization: Bearer $DTM_TOKEN" -H "X-DTM-CSRF: 1"
# Every zone as a JSON bundle (preserves LB policy, health-check templates,
# region tags)
curl -sk https://dtm.internal:8443/api/v1/zones/export \
-H "Authorization: Bearer $DTM_TOKEN" -H "X-DTM-CSRF: 1" -o zones.json

The Content-Type selects the parser: application/json for a DTM JSON bundle (preserves everything except health-check template secrets, see below), text/csv for a spreadsheet (lossy, onboarding only), multipart/form-data or text/dns for BIND zone files.

Terminal window
# Re-import the JSON bundle
curl -sk -X POST https://dtm.internal:8443/api/v1/zones/import \
-H "Authorization: Bearer $DTM_TOKEN" \
-H "X-DTM-CSRF: 1" \
-H "Content-Type: application/json" \
--data-binary @zones.json
# Preview without writing anything
curl -sk -X POST "https://dtm.internal:8443/api/v1/zones/import?dry_run=true" \
-H "Authorization: Bearer $DTM_TOKEN" \
-H "X-DTM-CSRF: 1" \
-H "Content-Type: application/json" \
--data-binary @zones.json

Health-check template secrets (headers values, oauth2.client_secret, mtls.key_pem) are masked as *** in the export and are not restored by the import. Re-apply them after importing, or authenticated probes will fail and their backends will drop out of answers.

Limits: 20 MB per request, 50 zones per request, 50,000 records per zone (the per-zone endpoint /api/v1/zones/<zone-id>/import takes up to 10 MB). An oversized body always fails with 413 and nothing is written. For JSON and CSV bundles, a request over the zone or record cap fails whole with 400 before any write. For BIND zone files, an over-cap or malformed file is instead reported per file in the response’s parse_errors (HTTP 200) while the other files in the same request still import, so scripts importing multiple BIND files should check parse_errors rather than relying on the status code. The per-zone endpoint truncates a BIND import at the record cap and notes it in errors[].

Imports are validated like individual creates. Zone and record names must be syntactically valid DNS names, every record must carry at least one value, and a BIND zone file must carry a complete, fully qualified SOA (a primary nameserver and a responsible mailbox). SOA timers and serial you omit from a JSON bundle are filled with the same defaults as a zone create rather than imported as zeros. Malformed input is rejected with 400 and an error naming the offending zone, row, or record. The dry-run plan runs the same validation as the commit, including CNAME exclusivity at a name, so a clean plan means a clean import. See migrating to Hayami DTM for source-by-source instructions.

ALIAS records are created like any record with "type": "ALIAS" and hostname values; see ALIAS records. Override records use a separate /api/v1/overrides endpoint; see override records.