# Manage DNS records

> Read, add, update and delete record sets, the value format of every record type, and how to point a domain at our name servers.

## GET /api/v1/domains/zones

Reads every record set in the zone.

**Request**

```http
GET /api/v1/domains/zones?domainName=apiname.com
```

**Response — 200**

```json
[
  {
    "name": "apiname.com.",
    "type": "SOA",
    "ttl": 3600,
    "changeType": "",
    "records": [
      { "content": "tr.apiname.com. hostmaster.apiname.com. 2026103001 86400 7200 1209600 300",
        "disabled": false }
    ]
  },
  {
    "name": "apiname.com.",
    "type": "A",
    "ttl": 3600,
    "changeType": "",
    "records": [
      { "content": "203.0.113.10", "disabled": false }
    ]
  },
  {
    "name": "apiname.com.",
    "type": "MX",
    "ttl": 3600,
    "changeType": "",
    "records": [
      { "content": "10 mail1.apiname.com.", "disabled": false },
      { "content": "20 mail2.apiname.com.", "disabled": false }
    ]
  },
  {
    "name": "www.apiname.com.",
    "type": "CNAME",
    "ttl": 3600,
    "changeType": "",
    "records": [
      { "content": "apiname.com.", "disabled": false }
    ]
  }
]
```

:::note[An empty array means the zone does not exist yet]
`[]` is not an error. Create the first record with `POST` and the zone is created for you. Use this to decide whether to show "set up DNS" or the record editor.
:::

Names come back in full form with a trailing dot. `changeType` is empty on reads — ignore it.

## POST /api/v1/domains/zones

Adds one or more values to a record set. Existing values in the same set are **kept** — this adds, it does not replace.

**Request — `domainName` in the query string, body as below**

| Field | Required | Notes |
| --- | --- | --- |
| `zoneStruct.name` | varies | Subdomain label or full name. `""` or `"@"` for the apex. **Required for CNAME.** |
| `zoneStruct.type` | required | Record type in **uppercase**. `A` works, `a` does not. |
| `zoneStruct.ttl` | required | 1 to 86400 seconds. `3600` is a sensible default; `300` while making changes. |
| `zoneStruct.contents` | required | Array of values to add to the set. |

**Apex A record**

```http
POST /api/v1/domains/zones?domainName=apiname.com

{
  "zoneStruct": {
    "name": "@",
    "type": "A",
    "ttl": 3600,
    "contents": ["203.0.113.10"]
  }
}
```

**www as a CNAME to the apex**

```json
{
  "zoneStruct": {
    "name": "www",
    "type": "CNAME",
    "ttl": 3600,
    "contents": ["apiname.com"]
  }
}
```

**Two mail servers in one call**

```json
{
  "zoneStruct": {
    "name": "@",
    "type": "MX",
    "ttl": 3600,
    "contents": [
      "10 mail1.apiname.com",
      "20 mail2.apiname.com"
    ]
  }
}
```

**SPF**

```json
{
  "zoneStruct": {
    "name": "@",
    "type": "TXT",
    "ttl": 3600,
    "contents": ["v=spf1 include:_spf.google.com -all"]
  }
}
```

**DMARC**

```json
{
  "zoneStruct": {
    "name": "_dmarc",
    "type": "TXT",
    "ttl": 3600,
    "contents": ["v=DMARC1; p=quarantine; rua=mailto:dmarc@apiname.com"]
  }
}
```

**CAA — restrict which CA may issue certificates**

```json
{
  "zoneStruct": {
    "name": "@",
    "type": "CAA",
    "ttl": 3600,
    "contents": ["0 issue \"letsencrypt.org\""]
  }
}
```

**Response — 204, no body**

### Record types and their value format

| Type | Value format | Example | Notes |
| --- | --- | --- | --- |
| A | IPv4 address | `203.0.113.10` | An IPv6 value here is rejected. |
| AAAA | IPv6 address | `2001:db8::1` | An IPv4 value here is rejected. |
| CNAME | Host name | `apiname.com` | **`name` must not be empty or `@`** — a CNAME at the apex is invalid DNS. Use `ALIAS` instead. An IP value is rejected. |
| ALIAS | Host name | `apiname.com` | **Use this for a CNAME-like record at the apex.** It resolves to the target's address without breaking the other apex records. |
| MX | `priority host` | `10 mail.apiname.com` | Two fields, space-separated. Priority must be a number; lower wins. The host must not be an IP. |
| TXT | Free text | `v=spf1 -all` | Send it unquoted — quotes are added for you. |
| CAA | `flag tag "value"` | `0 issue "letsencrypt.org"` | **Exactly three** space-separated fields, or it is rejected. |
| SRV | `priority weight port host` | `10 5 443 sip.apiname.com` | Four fields. The name is conventionally `_service._proto`. |
| PTR | Host name | `host.apiname.com` | Reverse lookups. |
| TLSA, NAPTR, CERT, DNAME, LOC, SSHFP and others | Per the relevant standard | — | Accepted and validated by the DNS server. A syntax problem surfaces as `ZoneClient:10008`. |
| SOA | Full SOA string | — | Created with the zone. Only change it if you know exactly why. |
| DS, DNSKEY, RRSIG, NSEC | — | — | **Do not manage DNSSEC here.** Use [the DNSSEC endpoints](https://www.domainnameapi.com/docs/name-servers/dnssec/), which publish at the registry. |

:::caution[Name servers: use the delegation endpoint, not this one]
To change which name servers answer for the domain, use `PUT /api/v1/domains/dns/name-server`. That is the registry-level operation and the only one that has any effect on delegation. The zones endpoint is for zone *contents* — A, MX, TXT, CNAME and so on.
:::

:::caution[You cannot have a CNAME alongside other records at the same name]
This is a DNS rule, not ours. If `www` has a CNAME, it cannot also have an A record. And nothing can have a CNAME at the apex — that is what `ALIAS` is for. A mail-enabled domain must have an A or ALIAS at the apex, never a CNAME, or mail delivery breaks.
:::

**Errors**

| Code | Meaning and fix |
| --- | --- |
| `ZoneClientValidationError` | A CNAME was sent without a `name`. CNAMEs can only exist on subdomains. |
| `ZoneClient:10002` | An `A` record's value is not a valid IPv4 address. |
| `ZoneClient:10003` | An `AAAA` record's value is not a valid IPv6 address. |
| `ZoneClient:10004` | The record name is required and cannot start with `@`. |
| `ZoneClient:10005` | A `CAA` value is not three space-separated fields. `details` shows the correct format. |
| `ZoneClient:10008` | The DNS server rejected the value's syntax. Check it against the record type's specification. |
| `ZoneClient:10009` | An `MX` priority is not a number. |
| `ZoneClient:10010` | A `CNAME` or `MX` value is an IP address. These require host names. |
| `Dna.DomainService:Zone:10001` | That exact value already exists in the set, or you sent it twice. Nothing was changed. |
| `ZoneClient:10001` | The DNS service is temporarily unreachable. Retry shortly. |
| `Dna.DomainService:Domain:10007` | Domain not found or not yours. |

## PUT /api/v1/domains/zones

Replaces a record set. **Every existing value in the set is discarded** and replaced with what you send.

**Request — query string carries both the domain and the current record name**

```http
PUT /api/v1/domains/zones?domainName=apiname.com&recordName=www

{
  "zoneStruct": {
    "name": "www",
    "type": "A",
    "ttl": 300,
    "contents": ["203.0.113.50"]
  }
}
```

This replaces every A value at `www` with the single new address and drops the TTL to 300.

| Parameter | Required | Notes |
| --- | --- | --- |
| `domainName` | required | Query string. |
| `recordName` | required | Query string. The **current** name of the set. |
| `zoneStruct` | required | Body. The desired end state. |

**Renaming a record set**

Send a different `zoneStruct.name` from `recordName`. The old set is removed and the values appear under the new name. Values already present at the destination are preserved rather than silently overwritten.

```http
PUT /api/v1/domains/zones?domainName=apiname.com&recordName=blog

{
  "zoneStruct": {
    "name": "news",
    "type": "CNAME",
    "ttl": 3600,
    "contents": ["apiname.com"]
  }
}
```

**Response — 204, no body**

:::caution[You cannot change a record's type here]
The set is located using `recordName` plus the `type` in your body, so the type is used for both lookup and result. To change a record from, say, CNAME to A: `DELETE` the old value, then `POST` the new one.
:::

:::note[Adding versus replacing]
To **add** a second IP to `www`, use `POST` — it merges. Using `PUT` with one value removes the other IP. This is the most common accident in this section.
:::

**Errors**

| Code | Meaning and fix |
| --- | --- |
| `ZoneClient:10006` | The zone does not exist. Create a record with `POST` first. |
| `ZoneClient:10007` | No record set matches `recordName` plus that type. |
| `Dna.DomainService:Zone:10001` | A value repeats within `contents`, or already exists at the destination name. |

## DELETE /api/v1/domains/zones

Deletes **one value** from a record set. When it was the last value, the whole set is removed.

**Request**

```http
DELETE /api/v1/domains/zones?domainName=apiname.com

{
  "name": "www.apiname.com",
  "recordType": "A",
  "record": "203.0.113.11"
}
```

| Field | Required | Notes |
| --- | --- | --- |
| `name` | required | The record name. **Must belong to this zone** — the zone itself or a subdomain of it. |
| `recordType` | required | Uppercase. |
| `record` | required | The exact value to remove. Whitespace and trailing dots are ignored when matching. |

**Response — 204, no body**

:::note[Removing one of several values]
If `www` has two A values and you delete one, the other stays. Delete both and the set disappears entirely. There is no "delete the whole set" call — delete each value, or use `PUT` to replace the set with what you want.
:::

**Errors**

| Code | Meaning and fix |
| --- | --- |
| `ZoneClient:10011` | `name` does not belong to this zone. `details` shows the expected format. Note that a name merely *containing* the zone — `apiname.com.example.net` — is correctly rejected. |
| `ZoneClient:10006` | The zone does not exist. |
| `ZoneClient:10007` | No such record set, or no value matching `record`. Read the zone with `GET` and copy the value exactly. |

## POST /api/v1/domains/zones/apply

Publishes the zone by delegating the domain to our name servers. This is the step that makes your records actually resolve.

**Request**

```http
POST /api/v1/domains/zones/apply?domainName=apiname.com
```

**Response — delegation was changed**

```json
{
  "nameServersChanged": true,
  "previousNameServers": ["ns1.otherhost.com", "ns2.otherhost.com"],
  "currentNameServers": ["tr.apiname.com", "eu.apiname.com"],
  "systemNameServers": ["tr.apiname.com", "eu.apiname.com"]
}
```

**Response — already delegated, nothing to do**

```json
{
  "nameServersChanged": false,
  "previousNameServers": ["tr.apiname.com", "eu.apiname.com"],
  "currentNameServers": ["tr.apiname.com", "eu.apiname.com"],
  "systemNameServers": ["tr.apiname.com", "eu.apiname.com"]
}
```

| Field | How to use it |
| --- | --- |
| `nameServersChanged` | `false` means the domain was already on our name servers. `true` means you just changed delegation and **propagation has started** — warn the customer about the wait. |
| `previousNameServers` | What the domain used before. **Log this** — it is your only record of the old setup if you need to revert. |
| `currentNameServers` | What it uses now. |

:::tip[Safe to call repeatedly]
Calling it when the domain is already delegated does nothing and returns `nameServersChanged: false`. You can safely call it after every zone edit as a belt-and-braces measure.
:::

:::danger[This replaces the delegation entirely]
The comparison is exact. A domain using our two name servers *plus* a third is rewritten down to just our two. And everything the previous name servers were serving — mail included — stops being authoritative once resolvers catch up.

**Recreate your records in the zone first, then call this.**
:::

**Errors**

| Code | Meaning and fix |
| --- | --- |
| `Dna.DomainService:Domain:10007` | Domain unknown, not yours, or deleted. |
| `Dna.DomainService:Dns:10003` | The domain has no name servers at all. Set them with `PUT /domains/dns/name-server` first. |
| `Dna.DomainService:Domain:10070` | Waiting for registry documents. |
| `Dna.DomainService:Dnssec:10010` | DNSSEC is active. Disable it first, or use `PUT /domains/dns/name-server` with `dnssecMode`. |
| `Dna.DomainService:DomainSale:10007` | The domain is listed for sale and uses parking records. |
| `Dna.DomainService:Epp:10001` | The registry rejected the delegation change. |
