Docs pages

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: [email protected]

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.

Endpoints#

Method and path Does
GET /user/tokens/verify Check the key
GET /zones Your hosts, as zones. ?name= finds one by name.
GET /zones/{zone_id} One zone
GET /zones/{zone_id}/dns_records Records, filtered by type, name, content
POST /zones/{zone_id}/dns_records Create a record
PUT /zones/{zone_id}/dns_records/{id} Replace one record
PATCH /zones/{zone_id}/dns_records/{id} Change some fields of one record
DELETE /zones/{zone_id}/dns_records/{id} Delete one record

Check the key#

Shell
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:

Shell
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:

Shell
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.

Shell
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: 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.

Shell
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#

Shell
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, and it takes the same key:

Shell
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.