Skip to content

DNS API: Zones and records

This page describes the endpoints for zones, records, and delegated subdomains. Basics such as the base URL, authentication, and error format are described in the API overview. All endpoints on this page require a token with access to the Records area; tokens restricted to selected zones only reach those zones.

The examples use the variable XCLOUD_DNS_TOKEN for your API token and the following base URL:

export XCLOUD_DNS_TOKEN='xcdns_...'
export XCLOUD_DNS_API='https://app.xcloud.eu/api/dns/v1'

Zones

Method Path Description
GET /zones List all zones, ordered by name
GET /zones/{id} Show a zone
POST /zones Create a zone
DELETE /zones/{id} Delete a zone including all its records
POST /zones/{id}/delegation_check Check the delegation again
POST /zones/{id}/dnssec Enable DNSSEC
DELETE /zones/{id}/dnssec Disable DNSSEC
GET /zones/{id}/export Export records as a BIND zone file
POST /zones/{id}/import Import records from a BIND zone file
POST /zones/{id}/retrieve Start a zone transfer (secondary zones only)

A zone cannot be changed after it has been created; there is no PATCH endpoint for zones.

Zone attributes

Attribute Description
id ID of the zone
name Zone name, e.g. example.com
state pending, active, error, or deleting
state_detail Additional information on the state, or null
nameservers The xCloud nameservers responsible for the zone
serial Current serial number of the zone, or null
zone_type master (records are managed in xCloud) or slave (secondary)
masters IP addresses of the primary nameservers of a secondary zone
dnssec_status DNSSEC signing status, or null
dnssec_ds DS records of the signed zone
dnssec_dnskey DNSKEY records of the signed zone
delegation_status ok, misconfigured, or unknown
delegation_checked_at Time of the last delegation check, or null
delegation_detail Nameservers observed at the parent zone, or null
records_count Number of records in the zone
registration_id ID of the linked domain registered via xCloud, or null
content_changed_at Time of the last change to the records, or null
created_at Creation time
updated_at Time of the last change

Show a zone

GET /zones/{id} returns a zone, GET /zones all zones in the same form:

curl "$XCLOUD_DNS_API/zones/$ZONE_ID" \
  -H "Authorization: Bearer $XCLOUD_DNS_TOKEN"
{
  "data": {
    "id": "0b7c4f0e-2a51-4c4e-9a7f-5d3c1e8b9f10",
    "name": "example.com",
    "state": "active",
    "state_detail": null,
    "nameservers": ["ns1.example.net", "ns2.example.net"],
    "serial": "2026102001",
    "zone_type": "master",
    "masters": [],
    "dnssec_status": null,
    "dnssec_ds": [],
    "dnssec_dnskey": [],
    "delegation_status": "ok",
    "delegation_checked_at": "2026-10-20T06:00:12+02:00",
    "delegation_detail": "ns1.example.net, ns2.example.net",
    "records_count": 12,
    "registration_id": "1d2e3f4a-5b6c-4d7e-8f9a-0b1c2d3e4f51",
    "created_at": "2026-09-14T10:21:37+02:00",
    "updated_at": "2026-10-20T06:00:12+02:00",
    "content_changed_at": "2026-10-19T17:42:05+02:00"
  }
}

Create a zone

POST /zones creates a zone. The zone starts in state pending and becomes active once it is set up on the nameservers.

Attribute Required Description
name yes Domain name of the zone, e.g. example.com
zone_type no master (default) or slave for a secondary zone
masters for slave IP addresses (IPv4 or IPv6) of your primary nameservers

A zone name can only exist once. NS and SOA records are set automatically. Once the zone is active, point the domain's nameservers to the nameservers listed in the zone's nameservers attribute.

Create a secondary zone that takes over its records from two primary nameservers via zone transfer (AXFR):

curl -X POST "$XCLOUD_DNS_API/zones" \
  -H "Authorization: Bearer $XCLOUD_DNS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "zone": {
      "name": "example.com",
      "zone_type": "slave",
      "masters": ["192.0.2.53", "2001:db8::53"]
    }
  }'

For a regular zone, only send the name: {"zone": {"name": "example.com"}}.

The records of a secondary zone are read-only: creating, changing, or deleting records, DNSSEC, and import are rejected with 422. POST /zones/{id}/retrieve starts an immediate zone transfer from the primary nameservers; for other zones, it returns 422.

Delegation check

xCloud regularly checks whether the domain's delegation at the registry points to the zone's nameservers; the result is in delegation_status. POST /zones/{id}/delegation_check queues an additional check. A zone can be checked again at most once per minute; more frequent requests are rejected with 422.

DNSSEC

POST /zones/{id}/dnssec enables DNSSEC signing for the zone, DELETE /zones/{id}/dnssec disables it. Both run in the background; the response shows the zone at the time of the request.

  • For domains registered via xCloud, the DS records are published at the registry automatically (and removed when you disable DNSSEC).
  • For externally registered domains, take the DS records from dnssec_ds and store them at your registrar.

Export a zone file

GET /zones/{id}/export returns all records of the zone as a BIND zone file with content type text/plain. SOA and apex NS records are managed by the nameservers and are not included.

Import a zone file

POST /zones/{id}/import creates records from a BIND zone file.

Attribute Required Description
zonefile yes Content of the BIND zone file as a string
dry_run no true only shows what would be imported; default false

Unlike other endpoints, the attributes are not wrapped in an outer object. The following example reads a local zone file, packs it into the request with jq, and first runs a dry run:

jq -n --rawfile zonefile example.com.zone '{zonefile: $zonefile, dry_run: true}' |
  curl -X POST "$XCLOUD_DNS_API/zones/$ZONE_ID/import" \
    -H "Authorization: Bearer $XCLOUD_DNS_TOKEN" \
    -H "Content-Type: application/json" \
    -d @-

The response is an import report:

{
  "data": {
    "dry_run": true,
    "creates": [
      { "record_type": "A", "subdomain": "www", "content": "192.0.2.10", "ttl": 3600,
        "prio": null }
    ],
    "skipped": [
      { "line": 7, "reason": "an identical record already exists" }
    ],
    "errors": []
  }
}
  • creates: records that are (or, in a dry run, would be) created
  • skipped: lines that are skipped, e.g. because an identical record already exists or the line is not supported
  • errors: lines that fail validation, with line number and message

Repeat the request with "dry_run": false to apply the import. The records in creates are only created if errors is empty; they are then created all together.

Delete a zone

DELETE /zones/{id} permanently deletes the zone including all its records.

Records

Method Path Description
GET /zones/{zone_id}/records List the records of a zone
GET /records List the records of all zones
POST /zones/{zone_id}/records Create a record
PATCH /records/{id} Change a record
DELETE /records/{id} Delete a record
PATCH /zones/{zone_id}/records/bulk_ttl Set the TTL of several records
POST /zones/{zone_id}/records/bulk_destroy Delete several records
POST /records/{id}/dyndns Enable DynDNS for a record
DELETE /records/{id}/dyndns Disable DynDNS for a record

GET /zones/{zone_id}/records returns the records of a zone, ordered by subdomain and type. GET /records returns the records of all zones, ordered by zone name, subdomain, and type; the optional query parameter zone_id limits the list to one zone. NS and SOA records that are set automatically are not included.

Record attributes

Attribute Description
id ID of the record
zone_id ID of the zone
zone_name Name of the zone
record_type A, AAAA, CNAME, MX, NS, PTR, SPF, SRV, TXT, or TLSA
subdomain Name relative to the zone, e.g. www; empty for the zone itself
content Value of the record
ttl TTL in seconds
prio Priority for MX and SRV, otherwise null
comment Free-text note, or null
dyndns_enabled Whether DynDNS is enabled
dyndns_url DynDNS update URL, or null
created_at Creation time
updated_at Time of the last change

List records

Fetch the records of a zone:

curl "$XCLOUD_DNS_API/zones/$ZONE_ID/records" \
  -H "Authorization: Bearer $XCLOUD_DNS_TOKEN"
{
  "data": [
    {
      "id": "5f0c2d6e-8b1a-4f3e-9c2d-1a7b8e9f0c11",
      "zone_id": "0b7c4f0e-2a51-4c4e-9a7f-5d3c1e8b9f10",
      "zone_name": "example.com",
      "record_type": "MX",
      "subdomain": "",
      "content": "mail.example.com",
      "ttl": 3600,
      "prio": 10,
      "comment": "Primary mail server",
      "dyndns_enabled": false,
      "dyndns_url": null,
      "created_at": "2026-09-14T10:25:03+02:00",
      "updated_at": "2026-09-14T10:25:03+02:00"
    },
    {
      "id": "9a3e7b21-4c5d-4e6f-8a9b-0c1d2e3f4a52",
      "zone_id": "0b7c4f0e-2a51-4c4e-9a7f-5d3c1e8b9f10",
      "zone_name": "example.com",
      "record_type": "A",
      "subdomain": "www",
      "content": "192.0.2.10",
      "ttl": 3600,
      "prio": null,
      "comment": null,
      "dyndns_enabled": false,
      "dyndns_url": null,
      "created_at": "2026-09-14T10:24:51+02:00",
      "updated_at": "2026-10-19T17:42:05+02:00"
    }
  ]
}

Create a record

POST /zones/{zone_id}/records creates a record in the zone.

Attribute Required Description
record_type yes Record type, see table below
content yes Value of the record, at most 20,000 characters
subdomain no Name relative to the zone; omit for the zone itself
ttl no TTL in seconds, at least 60; default 3600
prio for MX, SRV Priority (integer, 0 or greater); only for MX and SRV
comment no Free-text note, at most 255 characters

The value format depends on the record type:

Type content Notes
A IPv4 address, e.g. 192.0.2.10 DynDNS possible
AAAA IPv6 address, e.g. 2001:db8::10 DynDNS possible
CNAME Target hostname, e.g. shop.example.net Not on the zone itself
MX Mail server hostname, e.g. mail.example.com prio required
NS Nameserver hostname, e.g. ns1.example.net Delegates the subdomain
PTR Target hostname
SPF SPF policy as text
SRV weight port target, e.g. 10 5060 sip.example.com prio required
TXT Free text, e.g. v=DMARC1; p=reject
TLSA usage selector matching-type data, e.g. 3 1 1 2b4f… Name _port._protocol

Further rules:

  • The name of a TLSA record must start with _port._protocol, e.g. _443._tcp or _25._tcp.mail.
  • A CNAME record must be the only record with its name.
  • Enter TXT and SPF values as plain text without surrounding quotes. Hostnames can be given with or without a trailing dot.
  • Below a delegated subdomain, only NS records are allowed; below a virtual subdomain, all types except NS.

An MX record for the zone itself:

curl -X POST "$XCLOUD_DNS_API/zones/$ZONE_ID/records" \
  -H "Authorization: Bearer $XCLOUD_DNS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "record": {
      "record_type": "MX",
      "content": "mail.example.com",
      "prio": 10,
      "ttl": 3600,
      "comment": "Primary mail server"
    }
  }'

An SRV record for SIP over TCP. The priority goes into prio, the other three values into content:

curl -X POST "$XCLOUD_DNS_API/zones/$ZONE_ID/records" \
  -H "Authorization: Bearer $XCLOUD_DNS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "record": {
      "record_type": "SRV",
      "subdomain": "_sip._tcp",
      "content": "5 5060 sip.example.com",
      "prio": 10,
      "ttl": 3600
    }
  }'

A TXT record with a DMARC policy:

curl -X POST "$XCLOUD_DNS_API/zones/$ZONE_ID/records" \
  -H "Authorization: Bearer $XCLOUD_DNS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "record": {
      "record_type": "TXT",
      "subdomain": "_dmarc",
      "content": "v=DMARC1; p=reject; rua=mailto:dmarc@example.com",
      "ttl": 3600
    }
  }'

A TLSA record for a mail server on port 25:

curl -X POST "$XCLOUD_DNS_API/zones/$ZONE_ID/records" \
  -H "Authorization: Bearer $XCLOUD_DNS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "record": {
      "record_type": "TLSA",
      "subdomain": "_25._tcp.mail",
      "content": "3 1 1 0c72ac70b745ac19998811b131d662c9ac69dbdbe7cb23e5b514b56664c5d3d6",
      "ttl": 3600
    }
  }'

The response contains the created record in data.

Change a record

PATCH /records/{id} changes the attributes subdomain, content, ttl, prio, and comment. Attributes you omit remain unchanged. The record type cannot be changed; to change it, delete the record and create a new one. To remove a comment, send null:

curl -X PATCH "$XCLOUD_DNS_API/records/$RECORD_ID" \
  -H "Authorization: Bearer $XCLOUD_DNS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "record": {
      "content": "198.51.100.20",
      "ttl": 300,
      "comment": null
    }
  }'

Change or delete several records

PATCH /zones/{zone_id}/records/bulk_ttl sets a common TTL for several records of a zone, and POST /zones/{zone_id}/records/bulk_destroy deletes several records of a zone. Both take at most 200 record IDs per request and are atomic: either all records are changed, or none.

Lower the TTL of two records before a migration:

curl -X PATCH "$XCLOUD_DNS_API/zones/$ZONE_ID/records/bulk_ttl" \
  -H "Authorization: Bearer $XCLOUD_DNS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "record": {
      "ids": [
        "5f0c2d6e-8b1a-4f3e-9c2d-1a7b8e9f0c11",
        "9a3e7b21-4c5d-4e6f-8a9b-0c1d2e3f4a52"
      ],
      "ttl": 300
    }
  }'

bulk_destroy takes the same {"record": {"ids": [...]}} structure without ttl.

DynDNS

DynDNS is available for A and AAAA records. POST /records/{id}/dyndns enables DynDNS and generates a new update URL, which the response returns in dyndns_url. Calling the endpoint again generates a new URL and invalidates the previous one. DELETE /records/{id}/dyndns disables DynDNS and invalidates the URL.

To update the record, call the update URL with a simple GET request from the device whose IP address the record should point to. No token is needed, the URL itself authorizes the request:

curl "$DYNDNS_URL"

The record is set to the IP address from which the request comes. The address family must match the record type: an A record requires an IPv4 request (e.g. curl -4), an AAAA record an IPv6 request (curl -6); otherwise the request is rejected with 422. Keep the URL secret: anyone who knows it can change the record.

Delegated subdomains

Method Path Description
GET /zones/{zone_id}/sub_zones List the delegated and virtual subdomains
POST /zones/{zone_id}/sub_zones Create a subdomain
DELETE /zones/{zone_id}/sub_zones/{id} Delete a subdomain

A subdomain (sub_zone) defines how a part of the zone is managed:

  • delegating: the subdomain is delegated to other nameservers. Below it, only NS records are allowed, that is, the delegation itself.
  • virtual: the subdomain only groups records and remains part of the zone. Below it, all record types except NS are allowed.

Subdomain attributes

Attribute Description
id ID of the subdomain
zone_id ID of the zone
name Name relative to the zone, e.g. shop
function delegating or virtual
records_count Number of records below the subdomain
created_at Creation time

Fetch the subdomains of a zone:

curl "$XCLOUD_DNS_API/zones/$ZONE_ID/sub_zones" \
  -H "Authorization: Bearer $XCLOUD_DNS_TOKEN"
{
  "data": [
    {
      "id": "c4d5e6f7-0a1b-4c2d-8e3f-4a5b6c7d8e93",
      "zone_id": "0b7c4f0e-2a51-4c4e-9a7f-5d3c1e8b9f10",
      "name": "shop",
      "function": "delegating",
      "records_count": 2,
      "created_at": "2026-10-02T14:08:44+02:00"
    }
  ]
}

Create and delete a subdomain

Create a delegated subdomain shop.example.com and then the NS records of the delegation:

curl -X POST "$XCLOUD_DNS_API/zones/$ZONE_ID/sub_zones" \
  -H "Authorization: Bearer $XCLOUD_DNS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sub_zone": {"name": "shop", "function": "delegating"}}'

for ns in ns1.shop-hosting.example ns2.shop-hosting.example; do
  curl -X POST "$XCLOUD_DNS_API/zones/$ZONE_ID/records" \
    -H "Authorization: Bearer $XCLOUD_DNS_TOKEN" \
    -H "Content-Type: application/json" \
    -d "{\"record\": {\"record_type\": \"NS\", \"subdomain\": \"shop\", \"content\": \"$ns\"}}"
done

The name is relative to the zone and must not be the zone itself. Existing records below the new subdomain are assigned to it. When you delete a subdomain, its records remain in the zone. A delegated subdomain can only be deleted once it no longer contains NS records.