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:
Authorization: Bearer KEY
Content-Type: application/jsonClients that use Cloudflare's older key-and-email pair work too. Any email is accepted:
X-Auth-Key: KEY
X-Auth-Email: [email protected]Every reply is Cloudflare's envelope. Lists add result_info:
{
"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:
{"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#
curl https://api.maildns.io/client/v4/user/tokens/verify \
-H 'Authorization: Bearer KEY'{"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:
curl 'https://api.maildns.io/client/v4/zones?name=advisor.example.com' \
-H 'Authorization: Bearer KEY'{
"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_pageat a time (up to 50, default 20;pagefrom 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_serversis there to read. It can't be changed.GET /zones/{zone_id}returns one zone, as theresultobject.
List records#
GET /zones/{zone_id}/dns_records with optional filters type, name (the full name), content, page and per_page:
curl 'https://api.maildns.io/client/v4/zones/3f1a6c2e9b7d4e08a5c1f2b3d4e5f607/dns_records?type=TXT' \
-H 'Authorization: Bearer KEY'{
"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.
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:
{"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
contentgoes without surrounding quotes, in one piece, even past 255 characters. ttlfollows 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.
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#
curl -X DELETE https://api.maildns.io/client/v4/zones/3f1a6c2e9b7d4e08a5c1f2b3d4e5f607/dns_records/51c0e4a7d2b98f3e6a1d0c5b4e7f2a93 \
-H 'Authorization: Bearer KEY'{"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:
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=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQWait for "Live": "yes" before asking Google or Microsoft to verify.