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#
GETwith a query string, orPOSTwith the same parameters as a form-encoded body (application/x-www-form-urlencoded). PreferPOSTfor writes: long DKIM values and your key stay out of URLs.- Every call has
keyandcommand. 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 Pythonrequests, pass a dict asdata=orparams=. - Only JSON.
/api3.xmlrepliesXML 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, ...):
{"SetDnsResponse": {"ResponseCode": 0, "Status": "success"}}Errors have "ResponseCode": "-1", "Status": "error" and an Error text:
{"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:
{"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#
curl 'https://api.maildns.io/api3.json?key=KEY&command=account_info'{
"AccountInfoResponse": {
"ResponseCode": 0,
"Status": "success",
"AccountInfo": {"Username": "acme-inboxes", "Hosts": "3"}
}
}list_domain#
One entry per host on your key.
curl 'https://api.maildns.io/api3.json?key=KEY&command=list_domain'{
"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.ExpirationandRegistrationare the root domain's dates, as milliseconds since 1970 in a string.NameServerSettings.Typeis always"Dynadot DNS"andStatusis 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).
curl 'https://api.maildns.io/api3.json?key=KEY&command=domain_info&domain=advisor.example.com'{
"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).
curl 'https://api.maildns.io/api3.json?key=KEY&command=get_dns&domain=advisor.example.com'{
"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:
{
"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:
{"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%3D3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQThe same call as a POST:
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=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQAdd 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:
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)#
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=1Delete 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:
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.
curl 'https://api.maildns.io/api3.json?key=KEY&command=get_ns&domain=advisor.example.com'{
"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_typeandvalue, it checks exactly that record and always returns it, withLive: "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
domainthat matches the ones you gave, and returns an empty list when none match.
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'{
"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:
{"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:
{"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.