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_dsand 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) createdskipped: lines that are skipped, e.g. because an identical record already exists or the line is not supportederrors: 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
TLSArecord must start with_port._protocol, e.g._443._tcpor_25._tcp.mail. - A
CNAMErecord must be the only record with its name. - Enter
TXTandSPFvalues as plain text without surrounding quotes. Hostnames can be given with or without a trailing dot. - Below a delegated subdomain, only
NSrecords are allowed; below a virtual subdomain, all types exceptNS.
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, onlyNSrecords are allowed, that is, the delegation itself.virtual: the subdomain only groups records and remains part of the zone. Below it, all record types exceptNSare 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.