Docs pages

Dynadot-style API#

https://api.maildns.io/api3.json takes the same commands, parameters and JSON replies as Dynadot's api3 for DNS records. Code written for Dynadot works after you change the base URL and the key. This style also has MailDNS's live check, dns_status, which every other style uses with the same key.

Requests#

  • GET with a query string, or POST with the same parameters as a form-encoded body (application/x-www-form-urlencoded). Prefer POST for writes: long DKIM values and your key stay out of URLs.
  • Every call has key and command. Command names are lowercase.
  • URL-encode every value. A DKIM key contains +, / and =; an unencoded + arrives as a space and the record is wrong. With curl use --data-urlencode; with Python requests, pass a dict as data= or params=.
  • Only JSON. /api3.xml replies XML is not supported: call /api3.json.

Replies#

Every reply is HTTP 200 with a JSON object, exactly like Dynadot. The object has one key, named after the command (SetDnsResponse, GetDnsResponse, ...):

JSON
{"SetDnsResponse": {"ResponseCode": 0, "Status": "success"}}

Errors have "ResponseCode": "-1", "Status": "error" and an Error text:

JSON
{"SetDnsResponse": {"ResponseCode": "-1", "Status": "error", "Error": "record type SRV is not allowed (allowed: A, AAAA, CNAME, MX, TXT)"}}

A wrong key, an unknown command and the rate limit use a plain Response object instead:

JSON
{"Response": {"ResponseCode": "-1", "Error": "invalid key"}}

Test for success with str(ResponseCode) == "0": as on Dynadot, success is the number 0 and errors are the string "-1". Every error text is listed in Errors.

Commands#

Command Parameters Does
account_info none The key's name and how many hosts it reaches
list_domain none Your hosts
domain_info domain One host, with its records
get_dns domain The records of a host, or of all your hosts in a root domain
set_dns2 domain, records, ttl, add_dns_to_current_setting Add or replace records
get_ns domain The authoritative nameservers
dns_status domain, subdomain, record_type, value Whether a record is live on every nameserver (MailDNS only)

domain is always either one of your hosts (advisor.example.com) or the root domain they live in (example.com). See Hosts.

account_info#

Shell
curl 'https://api.maildns.io/api3.json?key=KEY&command=account_info'
JSON
{
  "AccountInfoResponse": {
    "ResponseCode": 0,
    "Status": "success",
    "AccountInfo": {"Username": "acme-inboxes", "Hosts": "3"}
  }
}

list_domain#

One entry per host on your key.

Shell
curl 'https://api.maildns.io/api3.json?key=KEY&command=list_domain'
JSON
{
  "ListDomainInfoResponse": {
    "ResponseCode": 0,
    "Status": "success",
    "MainDomains": [
      {
        "Name": "advisor.example.com",
        "RootDomain": "example.com",
        "Expiration": "1798761600000",
        "Registration": "1735689600000",
        "NameServerSettings": {"Type": "Dynadot DNS"},
        "Locked": "yes",
        "Disabled": "no",
        "Hold": "no",
        "Privacy": "full",
        "RenewOption": "auto renew",
        "Status": "active"
      },
      {
        "Name": "billing.example.com",
        "RootDomain": "example.com",
        "Expiration": "1798761600000",
        "Registration": "1735689600000",
        "NameServerSettings": {"Type": "Dynadot DNS"},
        "Locked": "yes",
        "Disabled": "no",
        "Hold": "no",
        "Privacy": "full",
        "RenewOption": "auto renew",
        "Status": "active"
      }
    ]
  }
}
  • Name: the host. RootDomain: the registered domain it lives in.
  • Expiration and Registration are the root domain's dates, as milliseconds since 1970 in a string.
  • NameServerSettings.Type is always "Dynadot DNS" and Status is always "active". The other fields are there so Dynadot parsers don't break; don't rely on them.

domain_info#

The list_domain entry for one host, with its records in NameServerSettings (same shape as get_dns).

Shell
curl 'https://api.maildns.io/api3.json?key=KEY&command=domain_info&domain=advisor.example.com'
JSON
{
  "DomainInfoResponse": {
    "ResponseCode": 0,
    "Status": "success",
    "DomainInfo": {
      "Name": "advisor.example.com",
      "RootDomain": "example.com",
      "Expiration": "1798761600000",
      "Registration": "1735689600000",
      "NameServerSettings": {
        "Type": "Dynadot DNS",
        "MainDomains": [
          {"RecordType": "MX", "Value": "smtp.google.com", "Value2": "1"},
          {"RecordType": "TXT", "Value": "v=spf1 include:_spf.google.com ~all"}
        ],
        "SubDomains": [
          {"Subhost": "_dmarc", "RecordType": "TXT", "Value": "v=DMARC1; p=quarantine"}
        ],
        "TTL": "3600"
      },
      "Locked": "yes",
      "Disabled": "no",
      "Hold": "no",
      "Privacy": "full",
      "RenewOption": "auto renew",
      "Status": "active"
    }
  }
}

get_dns#

The records of a host (host form) or of all your hosts in a root domain (root form).

Shell
curl 'https://api.maildns.io/api3.json?key=KEY&command=get_dns&domain=advisor.example.com'
JSON
{
  "GetDnsResponse": {
    "ResponseCode": 0,
    "Status": "success",
    "GetDns": {
      "NameServerSettings": {
        "Type": "Dynadot DNS",
        "MainDomains": [
          {"RecordType": "MX", "Value": "smtp.google.com", "Value2": "1"},
          {"RecordType": "TXT", "Value": "v=spf1 include:_spf.google.com ~all"},
          {"RecordType": "TXT", "Value": "google-site-verification=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ"}
        ],
        "SubDomains": [
          {"Subhost": "_dmarc", "RecordType": "TXT", "Value": "v=DMARC1; p=quarantine"},
          {"Subhost": "google._domainkey", "RecordType": "TXT", "Value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...IDAQAB"}
        ],
        "TTL": "3600"
      }
    }
  }
}
Field Meaning
MainDomains[] Records at domain itself. Empty in root form.
SubDomains[] Records under domain. Subhost is the name in front of domain.
RecordType A, AAAA, CNAME, MX or TXT (uppercase in replies)
Value The record's value. TXT values come back whole, without quotes.
Value2 MX priority. Only on MX records.
TTL The TTL in seconds, as a string

The same host in root form (domain=example.com) comes back with every record under SubDomains:

JSON
{
  "GetDnsResponse": {
    "ResponseCode": 0,
    "Status": "success",
    "GetDns": {
      "NameServerSettings": {
        "Type": "Dynadot DNS",
        "MainDomains": [],
        "SubDomains": [
          {"Subhost": "advisor", "RecordType": "MX", "Value": "smtp.google.com", "Value2": "1"},
          {"Subhost": "advisor", "RecordType": "TXT", "Value": "v=spf1 include:_spf.google.com ~all"},
          {"Subhost": "_dmarc.advisor", "RecordType": "TXT", "Value": "v=DMARC1; p=quarantine"}
        ],
        "TTL": "3600"
      }
    }
  }
}

set_dns2#

Adds or replaces records. Returns once the change is saved at the authoritative nameservers.

Parameter Value
domain A host (advisor.example.com) or the root of your hosts (example.com)
main_record_type{n} a, aaaa, cname, mx or txt: a record at domain itself. Host form only.
main_record{n} Its value
main_recordx{n} MX priority, 0 to 65535. MX only.
subdomain{n} A name under domain: _dmarc, google._domainkey (host form) or _dmarc.advisor (root form)
sub_record_type{n} a, aaaa, cname, mx or txt
sub_record{n} Its value
sub_recordx{n} MX priority. MX only.
ttl Accepted for compatibility. Records are served with the domain's TTL, which MailDNS never changes (why).
add_dns_to_current_setting 1 adds the records to what is there. Leave it out to replace (below).

n counts from 0, separately for main and subdomain records, up to 100 of each per call. Record types are lowercase like Dynadot's (uppercase works too).

Add mode (add_dns_to_current_setting=1): the records are added to what is there. A record that is already there (same name, type and value) is skipped, so repeating a call is harmless.

Replace mode (no add flag): every host the call mentions ends up with exactly the records in the call, including the records under it. Hosts the call does not mention keep their records. On Dynadot itself a replace wipes the whole domain; MailDNS limits it to the hosts you name. See Replace calls work per host.

The success reply is always the same:

JSON
{"SetDnsResponse": {"ResponseCode": 0, "Status": "success"}}

Add a verification TXT at the host#

As a URL (values encoded by hand):

https://api.maildns.io/api3.json?key=KEY&command=set_dns2&domain=advisor.example.com&add_dns_to_current_setting=1&main_record_type0=txt&main_record0=google-site-verification%3D3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ

The same call as a POST:

Shell
curl https://api.maildns.io/api3.json \
  --data-urlencode key=KEY \
  --data-urlencode command=set_dns2 \
  --data-urlencode domain=advisor.example.com \
  --data-urlencode add_dns_to_current_setting=1 \
  --data-urlencode main_record_type0=txt \
  --data-urlencode main_record0=google-site-verification=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ

Add DKIM in root form#

https://api.maildns.io/api3.json?key=KEY&command=set_dns2&domain=example.com&add_dns_to_current_setting=1&subdomain0=google._domainkey.advisor&sub_record_type0=txt&sub_record0=v%3DDKIM1%3B%20k%3Drsa%3B%20p%3DMIIBIjANBgkqh...

Mail records in one call#

MX, SPF and DMARC for one host:

Shell
curl https://api.maildns.io/api3.json \
  --data-urlencode key=KEY \
  --data-urlencode command=set_dns2 \
  --data-urlencode domain=advisor.example.com \
  --data-urlencode add_dns_to_current_setting=1 \
  --data-urlencode main_record_type0=mx \
  --data-urlencode main_record0=smtp.google.com \
  --data-urlencode main_recordx0=1 \
  --data-urlencode main_record_type1=txt \
  --data-urlencode 'main_record1=v=spf1 include:_spf.google.com ~all' \
  --data-urlencode subdomain0=_dmarc \
  --data-urlencode sub_record_type0=txt \
  --data-urlencode 'sub_record0=v=DMARC1; p=quarantine'

Two hosts in one call (root form)#

Shell
curl https://api.maildns.io/api3.json \
  --data-urlencode key=KEY \
  --data-urlencode command=set_dns2 \
  --data-urlencode domain=example.com \
  --data-urlencode add_dns_to_current_setting=1 \
  --data-urlencode subdomain0=advisor \
  --data-urlencode sub_record_type0=mx \
  --data-urlencode sub_record0=smtp.google.com \
  --data-urlencode sub_recordx0=1 \
  --data-urlencode subdomain1=billing \
  --data-urlencode sub_record_type1=mx \
  --data-urlencode sub_record1=smtp.google.com \
  --data-urlencode sub_recordx1=1

Delete one record#

There is no delete command, as on Dynadot. Read the host with get_dns, drop the record, and send the rest back in replace mode (no add flag). Every record of the host you leave out is removed, so send all the others:

Python
import os
import requests

API = "https://api.maildns.io/api3.json"
KEY = os.environ["MAILDNS_KEY"]


def call(**params):
    reply = requests.post(API, data={"key": KEY, **params}, timeout=90).json()
    inner = next(iter(reply.values()))
    if str(inner.get("ResponseCode")) != "0":
        raise RuntimeError(inner.get("Error"))
    return inner


def delete_record(host, subhost, rtype, value):
    """Remove one record at host ("" = the host itself) or under it; keep all the others."""
    ns = call(command="get_dns", domain=host)["GetDns"]["NameServerSettings"]
    keep = [("", r) for r in ns["MainDomains"]] + [(r["Subhost"], r) for r in ns["SubDomains"]]
    keep = [(sub, r) for sub, r in keep
            if not (sub == subhost and r["RecordType"].upper() == rtype.upper() and r["Value"] == value)]
    params = {"command": "set_dns2", "domain": host}   # no add flag: replace this host's records
    m = s = 0
    for sub, r in keep:
        if sub == "":
            params[f"main_record_type{m}"] = r["RecordType"].lower()
            params[f"main_record{m}"] = r["Value"]
            if r.get("Value2"):
                params[f"main_recordx{m}"] = r["Value2"]
            m += 1
        else:
            params[f"subdomain{s}"] = sub
            params[f"sub_record_type{s}"] = r["RecordType"].lower()
            params[f"sub_record{s}"] = r["Value"]
            if r.get("Value2"):
                params[f"sub_recordx{s}"] = r["Value2"]
            s += 1
    if m + s == 0:
        raise RuntimeError("that was the host's last record: delete it Spaceship style instead")
    call(**params)


delete_record("advisor.example.com", "_dmarc", "TXT", "v=DMARC1; p=none")

A replace has to carry at least one record. To remove a host's last record, use the Spaceship-style DELETE, which removes exactly the records you list.

get_ns#

The authoritative nameservers of the root domain the host lives in. Read-only: nameservers can't be changed.

Shell
curl 'https://api.maildns.io/api3.json?key=KEY&command=get_ns&domain=advisor.example.com'
JSON
{
  "GetNsResponse": {
    "ResponseCode": 0,
    "Status": "success",
    "NsContent": {"Host0": "ns1.example.net", "Host1": "ns2.example.net", "NsName": "MailDNS"}
  }
}

dns_status#

MailDNS's own command: asks every authoritative nameserver directly whether it serves a record. Use it after a write and before asking Google or Microsoft to verify. Read-only.

Parameter Value
domain A host or the root of your hosts
subdomain The record's name relative to domain. @ means domain itself.
record_type txt, mx, cname, a or aaaa
value The record's value (for MX, the mail server)
  • With subdomain, record_type and value, it checks exactly that record and always returns it, with Live: "no" until every nameserver serves it. Use this form when waiting for a record you just wrote.
  • With fewer parameters, it checks every record of domain that matches the ones you gave, and returns an empty list when none match.
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=google._domainkey \
  --data-urlencode record_type=txt \
  --data-urlencode 'value=v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...IDAQAB'
JSON
{
  "DnsStatusResponse": {
    "ResponseCode": 0,
    "Status": "success",
    "Records": [
      {
        "Name": "google._domainkey.advisor.example.com",
        "RecordType": "TXT",
        "Value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...IDAQAB",
        "Live": "no",
        "Servers": {"ns1.example.net": "yes", "ns2.example.net": "no"}
      }
    ]
  }
}
Field Meaning
Name The record's full name
Live yes when every nameserver answers with the record, otherwise no
Servers Each authoritative nameserver and its answer, yes or no

How to poll it: Checking propagation.

Nameservers#

No key can change nameservers. set_ns, add_ns, register_ns, delete_ns and NS records in set_dns2 reply:

JSON
{"Response": {"ResponseCode": "-1", "Error": "This API key doesn't work for nameservers."}}

(An NS record inside set_dns2 comes back in SetDnsResponse, with "Status": "error".) Reading them with get_ns is fine.

Commands that are not available#

MailDNS manages DNS records only. Commands that change registration, forwarding, contacts, transfers, renewals or anything else outside DNS records reply:

JSON
{"Response": {"ResponseCode": "-1", "Error": "command not available on this account"}}

This covers register, renew, delete, push, set_forwarding, set_stealth, set_email_forward, set_whois, set_dns (use set_dns2), set_dnssec and the rest of Dynadot's account and registration commands. An unknown command replies incorrect command name.