# Cloudflare-style API

`https://api.maildns.io/client/v4` takes the same paths, headers and JSON envelope as Cloudflare's API v4 for DNS records. Point a Cloudflare client at that base URL and give it your MailDNS key as the API token.

## Requests and replies

Send the key as a bearer token:

```http
Authorization: Bearer KEY
Content-Type: application/json
```

Clients that use Cloudflare's older key-and-email pair work too. Any email is accepted:

```http
X-Auth-Key: KEY
X-Auth-Email: you@example.com
```

Every reply is Cloudflare's envelope. Lists add `result_info`:

```json
{
  "success": true,
  "errors": [],
  "messages": [],
  "result": [],
  "result_info": {"page": 1, "per_page": 20, "count": 0, "total_count": 0, "total_pages": 0}
}
```

Errors have `"success": false` and a code in `errors`:

```json
{"success": false, "errors": [{"code": 1004, "message": "This API key doesn't work for nameservers."}], "messages": [], "result": null}
```

| HTTP | Code | Meaning |
|---|---|---|
| 403 | 10000 | Wrong key, or a call from an IP address the key is not locked to |
| 404 | 7003 | Unknown zone id, or a zone that is not yours |
| 404 | 81044 | Unknown record id |
| 400 | 1004 | Invalid record: type, content, TTL, CNAME clash, too many records. `message` says which. |
| 403 | 1004 | A name outside your hosts, an NS record, or a nameserver change |
| 429 | 10013 | Too many requests, or an earlier change to the same root domain is still running. Wait `Retry-After` seconds and retry. |
| 503 | 1000 | The change could not be made right now. Nothing was changed. |

The `message` texts are the ones listed in [Errors](https://maildns.io/docs/errors.md).

## Endpoints

| Method and path | Does |
|---|---|
| [`GET /user/tokens/verify`](#check-the-key) | Check the key |
| [`GET /zones`](#zones) | Your hosts, as zones. `?name=` finds one by name. |
| [`GET /zones/{zone_id}`](#zones) | One zone |
| [`GET /zones/{zone_id}/dns_records`](#list-records) | Records, filtered by `type`, `name`, `content` |
| [`POST /zones/{zone_id}/dns_records`](#create-a-record) | Create a record |
| [`PUT /zones/{zone_id}/dns_records/{id}`](#replace-or-change-a-record) | Replace one record |
| [`PATCH /zones/{zone_id}/dns_records/{id}`](#replace-or-change-a-record) | Change some fields of one record |
| [`DELETE /zones/{zone_id}/dns_records/{id}`](#delete-a-record) | Delete one record |

## Check the key

```bash
curl https://api.maildns.io/client/v4/user/tokens/verify \
  -H 'Authorization: Bearer KEY'
```

```json
{"success": true, "errors": [], "messages": [], "result": {"id": "c0a8f3e1d2b4a6978e5f4d3c2b1a0f9e", "status": "active"}}
```

## Zones

Each of your hosts is a zone, and the zone's `name` is the host. Find a zone's id by name:

```bash
curl 'https://api.maildns.io/client/v4/zones?name=advisor.example.com' \
  -H 'Authorization: Bearer KEY'
```

```json
{
  "success": true,
  "errors": [],
  "messages": [],
  "result": [
    {
      "id": "3f1a6c2e9b7d4e08a5c1f2b3d4e5f607",
      "name": "advisor.example.com",
      "status": "active",
      "paused": false,
      "type": "full",
      "name_servers": ["ns1.example.net", "ns2.example.net"],
      "created_on": "2025-01-01T00:00:00Z",
      "modified_on": "2026-10-18T09:30:00Z"
    }
  ],
  "result_info": {"page": 1, "per_page": 20, "count": 1, "total_count": 1, "total_pages": 1}
}
```

- Without `name`, you get every host on your key, `per_page` at a time (up to 50, default 20; `page` from 1).
- Zone ids never change, so you can store them.
- The root domain holding your hosts can be looked up by name too (`?name=example.com`). Its zone shows only your hosts' records.
- `name_servers` is there to read. It can't be changed.
- `GET /zones/{zone_id}` returns one zone, as the `result` object.

## List records

`GET /zones/{zone_id}/dns_records` with optional filters `type`, `name` (the full name), `content`, `page` and `per_page`:

```bash
curl 'https://api.maildns.io/client/v4/zones/3f1a6c2e9b7d4e08a5c1f2b3d4e5f607/dns_records?type=TXT' \
  -H 'Authorization: Bearer KEY'
```

```json
{
  "success": true,
  "errors": [],
  "messages": [],
  "result": [
    {
      "id": "8b2e5d1f0a9c4b7e6d3f2a1c0b9e8d7f",
      "zone_id": "3f1a6c2e9b7d4e08a5c1f2b3d4e5f607",
      "zone_name": "advisor.example.com",
      "name": "advisor.example.com",
      "type": "TXT",
      "content": "google-site-verification=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ",
      "proxiable": false,
      "proxied": false,
      "ttl": 3600,
      "locked": false,
      "created_on": "2026-10-18T09:30:00Z",
      "modified_on": "2026-10-18T09:30:00Z"
    },
    {
      "id": "51c0e4a7d2b98f3e6a1d0c5b4e7f2a93",
      "zone_id": "3f1a6c2e9b7d4e08a5c1f2b3d4e5f607",
      "zone_name": "advisor.example.com",
      "name": "_dmarc.advisor.example.com",
      "type": "TXT",
      "content": "v=DMARC1; p=quarantine",
      "proxiable": false,
      "proxied": false,
      "ttl": 3600,
      "locked": false,
      "created_on": "2026-10-18T09:31:00Z",
      "modified_on": "2026-10-18T09:31:00Z"
    }
  ],
  "result_info": {"page": 1, "per_page": 100, "count": 2, "total_count": 2, "total_pages": 1}
}
```

`name` in replies is always the full name. MX records also carry `priority`. `proxied` is always `false`: MailDNS serves DNS records only.

## Create a record

`POST /zones/{zone_id}/dns_records` with `type`, `name`, `content`, `ttl`, and `priority` for MX. `name` can be the full name, the part in front of the zone (`_dmarc`), or `@` for the zone itself.

```bash
curl -X POST https://api.maildns.io/client/v4/zones/3f1a6c2e9b7d4e08a5c1f2b3d4e5f607/dns_records \
  -H 'Authorization: Bearer KEY' -H 'Content-Type: application/json' \
  -d '{"type": "TXT", "name": "@", "ttl": 3600,
       "content": "google-site-verification=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ"}'
```

The reply's `result` is the new record, with its `id`. More shapes:

```json
{"type": "MX", "name": "@", "content": "smtp.google.com", "priority": 1, "ttl": 3600}
{"type": "TXT", "name": "google._domainkey", "content": "v=DKIM1; k=rsa; p=MIIBIjANBgkqh...", "ttl": 3600}
{"type": "CNAME", "name": "mail", "content": "ghs.googlehosted.com", "ttl": 3600}
{"type": "A", "name": "@", "content": "203.0.113.10", "ttl": 3600}
```

- If the same record (type, name, content) is already there, you get that record back instead of an error, so retrying a create is harmless.
- TXT `content` goes without surrounding quotes, in one piece, even past 255 characters.
- `ttl` follows [the TTL rule](https://maildns.io/docs/keys.md#record-rules): `1` (automatic) and any value are accepted; the record is served with the domain's TTL unless the domain supports one per record.

## Replace or change a record

`PUT` replaces the whole record; `PATCH` changes only the fields you send.

```bash
curl -X PATCH https://api.maildns.io/client/v4/zones/3f1a6c2e9b7d4e08a5c1f2b3d4e5f607/dns_records/51c0e4a7d2b98f3e6a1d0c5b4e7f2a93 \
  -H 'Authorization: Bearer KEY' -H 'Content-Type: application/json' \
  -d '{"content": "v=DMARC1; p=reject"}'
```

Record ids come from the record itself, so a changed record comes back with a **new id**. Use the `id` in the reply from then on.

## Delete a record

```bash
curl -X DELETE https://api.maildns.io/client/v4/zones/3f1a6c2e9b7d4e08a5c1f2b3d4e5f607/dns_records/51c0e4a7d2b98f3e6a1d0c5b4e7f2a93 \
  -H 'Authorization: Bearer KEY'
```

```json
{"success": true, "errors": [], "messages": [], "result": {"id": "51c0e4a7d2b98f3e6a1d0c5b4e7f2a93"}}
```

## Nameservers

NS records and anything that would change a zone's nameservers are refused with HTTP 403, code 1004, `This API key doesn't work for nameservers.` Zones can't be created or deleted either. Reading `name_servers` is fine.

## Check that a record is live

The live check is a [Dynadot-style call](https://maildns.io/docs/propagation.md), and it takes the same key:

```bash
curl https://api.maildns.io/api3.json \
  --data-urlencode key=KEY --data-urlencode command=dns_status \
  --data-urlencode domain=advisor.example.com --data-urlencode subdomain=@ \
  --data-urlencode record_type=txt \
  --data-urlencode value=google-site-verification=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ
```

Wait for `"Live": "yes"` before asking Google or Microsoft to verify.
