Docs pages

Spaceship-style API#

https://api.maildns.io/api/v1 takes the same paths, headers and JSON bodies as Spaceship's DNS records API. Code written for Spaceship works after you change the base URL, the key and the secret.

Requests and replies#

Every call sends three headers:

HTTP
X-API-Key: KEY
X-API-Secret: SECRET
Content-Type: application/json
  • Reads reply 200 with JSON. Writes reply 204 with no body once the change is saved at the authoritative nameservers.
  • Errors reply 4xx or 5xx with a JSON body in Spaceship's shape:
JSON
{"detail": "www.example.com is not one of your hosts", "data": []}
Status Meaning
400 The body is not valid JSON
401 Wrong key or secret, or a call from an IP address the key is not locked to
403 A name that is not at or under one of your hosts, or a nameserver change
404 A domain that is not yours, or an unknown path
422 An invalid record or request: type, value, TTL, CNAME clash, too many records
429 Too many requests, or an earlier change to the same root domain is still running. Wait Retry-After seconds (or a few seconds if there is none) and retry.
503 The change could not be made right now. Nothing was changed.

More in Errors.

{domain} in a path is one of your hosts (advisor.example.com) or the root domain they live in (example.com), except in /domains/{domain}, which takes a host. See Hosts.

Endpoints#

Method and path Does
GET /api/v1/domains Your hosts
GET /api/v1/domains/{domain} One host
GET /api/v1/dns/records/{domain} Records
PUT /api/v1/dns/records/{domain} Add records
DELETE /api/v1/dns/records/{domain} Remove records
GET /api/v1/dns/status/{domain} Whether a record is live on every nameserver (MailDNS only)

Record items#

Names are relative to {domain}; @ is {domain} itself. ttl is optional (300 to 86400, default 3600); it is used where the domain supports a TTL per record, otherwise the record gets the domain's TTL (more).

JSON
{"type": "TXT",   "name": "@",                 "value": "google-site-verification=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ", "ttl": 3600}
{"type": "MX",    "name": "@",                 "exchange": "smtp.google.com", "preference": 1, "ttl": 3600}
{"type": "CNAME", "name": "mail",              "cname": "ghs.googlehosted.com", "ttl": 3600}
{"type": "A",     "name": "@",                 "address": "203.0.113.10", "ttl": 3600}
{"type": "TXT",   "name": "google._domainkey", "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqh...", "ttl": 3600}
Type Value field Also
TXT value: the text, without quotes, up to 2,048 characters
MX exchange: the mail server preference: 0 to 65535
CNAME cname: the target name
A address: an IPv4 address
AAAA address: an IPv6 address

Replies also carry "group": {"type": "custom"} on each item, as Spaceship does. You don't need to send it.

List hosts#

GET /api/v1/domains?take=100&skip=0: one item per host on your key. take is 1 to 100.

Shell
curl 'https://api.maildns.io/api/v1/domains?take=100&skip=0' \
  -H 'X-API-Key: KEY' -H 'X-API-Secret: SECRET'
JSON
{
  "items": [
    {
      "name": "advisor.example.com",
      "unicodeName": "advisor.example.com",
      "isPremium": false,
      "autoRenew": true,
      "registrationDate": "2025-01-01T00:00:00.000Z",
      "expirationDate": "2027-01-01T00:00:00.000Z",
      "lifecycleStatus": "registered",
      "verificationStatus": null,
      "eppStatuses": ["clientTransferProhibited"],
      "suspensions": [],
      "privacyProtection": {"level": "high", "contactForm": true},
      "nameservers": {"provider": "basic", "hosts": ["ns1.example.net", "ns2.example.net"]}
    }
  ],
  "total": 1
}

The dates are the root domain's. Fields other than name, unicodeName, the dates, lifecycleStatus and nameservers are there so Spaceship parsers don't break; don't rely on them.

Get one host#

GET /api/v1/domains/{domain} with a host: the same item, on its own. Anything else is 404.

Shell
curl https://api.maildns.io/api/v1/domains/advisor.example.com \
  -H 'X-API-Key: KEY' -H 'X-API-Secret: SECRET'

List records#

GET /api/v1/dns/records/{domain}?take=500&skip=0. take is 1 to 500.

Shell
curl 'https://api.maildns.io/api/v1/dns/records/advisor.example.com?take=500&skip=0' \
  -H 'X-API-Key: KEY' -H 'X-API-Secret: SECRET'
JSON
{
  "items": [
    {"type": "MX", "name": "@", "ttl": 3600, "exchange": "smtp.google.com", "preference": 1, "group": {"type": "custom"}},
    {"type": "TXT", "name": "@", "ttl": 3600, "value": "v=spf1 include:_spf.google.com ~all", "group": {"type": "custom"}},
    {"type": "TXT", "name": "_dmarc", "ttl": 3600, "value": "v=DMARC1; p=quarantine", "group": {"type": "custom"}}
  ],
  "total": 3
}

With the root domain in the path, you get every record of your hosts in that root, with names like advisor and _dmarc.advisor.

Add records#

PUT /api/v1/dns/records/{domain} with {"force": false, "items": [...]}. Replies 204.

Shell
curl -X PUT https://api.maildns.io/api/v1/dns/records/advisor.example.com \
  -H 'X-API-Key: KEY' -H 'X-API-Secret: SECRET' -H 'Content-Type: application/json' \
  -d '{
    "force": false,
    "items": [
      {"type": "MX", "name": "@", "exchange": "smtp.google.com", "preference": 1, "ttl": 3600},
      {"type": "TXT", "name": "@", "value": "v=spf1 include:_spf.google.com ~all", "ttl": 3600},
      {"type": "TXT", "name": "_dmarc", "value": "v=DMARC1; p=quarantine", "ttl": 3600}
    ]
  }'
  • The items are added to what is there. Items that are already there (same type, name and value) are skipped, so repeating a PUT is harmless.
  • PUT never removes anything, except with force (next point).
  • A CNAME can't share its name with other records. If an item would break that rule, the call is refused with 422, unless "force": true: then the records already at that name are replaced by the new ones.
  • An empty items list is 422.

Delete records#

DELETE /api/v1/dns/records/{domain} with a JSON array of items. Replies 204.

Shell
curl -X DELETE https://api.maildns.io/api/v1/dns/records/advisor.example.com \
  -H 'X-API-Key: KEY' -H 'X-API-Secret: SECRET' -H 'Content-Type: application/json' \
  -d '[{"type": "TXT", "name": "_dmarc", "value": "v=DMARC1; p=none"}]'
  • Removes exactly the records you list, matched on type, name and value (the exchange for MX, cname for CNAME, address for A and AAAA). ttl is ignored.
  • Items that don't exist are ignored, so repeating a DELETE is harmless.
  • To change a record, DELETE the old one, then PUT the new one.

Nameservers#

No key can change nameservers. A nameserver update, or an NS item in PUT, replies HTTP 403:

JSON
{"detail": "This API key doesn't work for nameservers.", "data": []}

nameservers in the domain items is there to read.

Check records#

GET /api/v1/dns/status/{domain}?name=&type=&value= is MailDNS's own endpoint: it asks every authoritative nameserver directly whether it serves a record. Use it after a write and before asking Google or Microsoft to verify.

  • With name, type and value, it checks exactly that record and always returns it, with "live": false until every nameserver serves it.
  • With fewer parameters, it checks every record of {domain} that matches the ones you gave.
Shell
curl -G https://api.maildns.io/api/v1/dns/status/advisor.example.com \
  -H 'X-API-Key: KEY' -H 'X-API-Secret: SECRET' \
  --data-urlencode name=@ \
  --data-urlencode type=TXT \
  --data-urlencode value=google-site-verification=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ
JSON
{
  "items": [
    {
      "name": "advisor.example.com",
      "type": "TXT",
      "value": "google-site-verification=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ",
      "live": true,
      "servers": {"ns1.example.net": true, "ns2.example.net": true}
    }
  ]
}

name in the reply is the record's full name. How to poll it: Checking propagation.