Namecheap-style API#
https://api.maildns.io/xml.response takes the same commands, parameters and XML replies as Namecheap's API for DNS records. Point a Namecheap client at that URL and give it your MailDNS key as the API key.
Requests and replies#
GET with a query string, or POST form-encoded, with Namecheap's global parameters on every call:
| Parameter | Value |
|---|---|
ApiKey |
Your MailDNS key |
ApiUser, UserName |
Any value. Kept so existing clients work. |
ClientIp |
Any value. Your key's own IP lock (if it has one) does this job. |
Command |
The command, for example namecheap.domains.dns.getHosts |
A domain is sent as SLD plus TLD, so a host works too: advisor.example.com is SLD=advisor.example and TLD=com; the root example.com is SLD=example and TLD=com. See Hosts. URL-encode every value.
Every reply is HTTP 200 with Namecheap's envelope. Check Status:
<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.sethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.setHosts">
<DomainDNSSetHostsResult Domain="advisor.example.com" IsSuccess="true" />
</CommandResponse>
<Server>MailDNS</Server>
<GMTTimeDifference>--0:00</GMTTimeDifference>
<ExecutionTime>0.412</ExecutionTime>
</ApiResponse>Errors have Status="ERROR" and one Error with a number and a text:
<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="ERROR" xmlns="http://api.namecheap.com/xml.response">
<Errors>
<Error Number="2011280">This API key doesn't work for nameservers.</Error>
</Errors>
<Warnings />
<RequestedCommand>namecheap.domains.dns.setcustom</RequestedCommand>
<Server>MailDNS</Server>
<GMTTimeDifference>--0:00</GMTTimeDifference>
<ExecutionTime>0.003</ExecutionTime>
</ApiResponse>Number |
Meaning |
|---|---|
| 1011102 | Wrong key, or a call from an IP address the key is not locked to |
| 2019166 | A domain that is not yours |
| 2016166 | A name outside your hosts |
| 2011170 | An invalid record: type, address, TTL, CNAME clash, too many records. The text says which. |
| 2011280 | Nameservers |
| 500000 | Too many requests, or an earlier change to the same root domain is still running. Wait and retry. |
| 5050900 | The change could not be made right now. Nothing was changed. |
The texts are the ones listed in Errors.
Commands#
| Command | Parameters | Does |
|---|---|---|
namecheap.domains.getList |
Your hosts | |
namecheap.domains.dns.getList |
SLD, TLD |
The nameservers (read-only) |
namecheap.domains.dns.getHosts |
SLD, TLD |
The records |
namecheap.domains.dns.setHosts |
SLD, TLD, HostName{n}, RecordType{n}, Address{n}, MXPref{n}, TTL{n}, EmailType |
Replace the records of each host the call mentions |
namecheap.domains.dns.setCustom, namecheap.domains.dns.setDefault |
Refused: nameservers |
List hosts#
curl 'https://api.maildns.io/xml.response?ApiUser=any&UserName=any&ApiKey=KEY&ClientIp=any&Command=namecheap.domains.getList'<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.getlist</RequestedCommand>
<CommandResponse Type="namecheap.domains.getList">
<DomainGetListResult>
<Domain ID="1001" Name="advisor.example.com" User="acme-inboxes" Created="01/01/2025" Expires="01/01/2027" IsExpired="false" IsLocked="true" AutoRenew="true" WhoisGuard="ENABLED" IsPremium="false" IsOurDNS="true" />
<Domain ID="1002" Name="billing.example.com" User="acme-inboxes" Created="01/01/2025" Expires="01/01/2027" IsExpired="false" IsLocked="true" AutoRenew="true" WhoisGuard="ENABLED" IsPremium="false" IsOurDNS="true" />
</DomainGetListResult>
<Paging>
<TotalItems>2</TotalItems>
<CurrentPage>1</CurrentPage>
<PageSize>20</PageSize>
</Paging>
</CommandResponse>
<Server>MailDNS</Server>
<GMTTimeDifference>--0:00</GMTTimeDifference>
<ExecutionTime>0.051</ExecutionTime>
</ApiResponse>The dates describe the root domain.
Read records#
curl 'https://api.maildns.io/xml.response?ApiUser=any&UserName=any&ApiKey=KEY&ClientIp=any&Command=namecheap.domains.dns.getHosts&SLD=advisor.example&TLD=com'<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="advisor.example.com" EmailType="MX" IsUsingOurDNS="true">
<host HostId="106926652" Name="@" Type="MX" Address="smtp.google.com" MXPref="1" TTL="3600" />
<host HostId="106926653" Name="@" Type="TXT" Address="v=spf1 include:_spf.google.com ~all" MXPref="10" TTL="3600" />
<host HostId="106926654" Name="_dmarc" Type="TXT" Address="v=DMARC1; p=quarantine" MXPref="10" TTL="3600" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>MailDNS</Server>
<GMTTimeDifference>--0:00</GMTTimeDifference>
<ExecutionTime>0.233</ExecutionTime>
</ApiResponse>Name is relative to the domain you sent; @ is the domain itself. With the root (SLD=example), you get only your hosts' records, with names like advisor and _dmarc.advisor.
Write records#
setHosts replaces: every host the call mentions ends up with exactly the records in the call, including the records under it. On Namecheap itself it replaces the whole domain; MailDNS limits it to the hosts you name. To add a record, read the host with getHosts and send everything back, plus the new record.
A complete Google Workspace setup for a new host, in one call:
curl https://api.maildns.io/xml.response \
--data-urlencode ApiUser=any --data-urlencode UserName=any \
--data-urlencode ApiKey=KEY --data-urlencode ClientIp=any \
--data-urlencode Command=namecheap.domains.dns.setHosts \
--data-urlencode SLD=advisor.example --data-urlencode TLD=com \
--data-urlencode EmailType=MX \
--data-urlencode HostName1=@ --data-urlencode RecordType1=TXT \
--data-urlencode Address1=google-site-verification=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ \
--data-urlencode HostName2=@ --data-urlencode RecordType2=MX \
--data-urlencode Address2=smtp.google.com --data-urlencode MXPref2=1 \
--data-urlencode HostName3=@ --data-urlencode RecordType3=TXT \
--data-urlencode 'Address3=v=spf1 include:_spf.google.com ~all' \
--data-urlencode HostName4=_dmarc --data-urlencode RecordType4=TXT \
--data-urlencode 'Address4=v=DMARC1; p=quarantine'| Parameter | Value |
|---|---|
HostName{n} |
The name relative to the domain: @, _dmarc, google._domainkey |
RecordType{n} |
A, AAAA, CNAME, MX or TXT |
Address{n} |
The value. TXT without surrounding quotes, in one piece. |
MXPref{n} |
MX priority, 0 to 65535 |
TTL{n} |
Optional; see the TTL rule |
EmailType |
Namecheap needs MX when the call has MX records. MailDNS accepts it and doesn't need it. |
n counts from 1. A call must carry at least one record.
Add one record from Python#
import os
import xml.etree.ElementTree as ET
import requests
API = "https://api.maildns.io/xml.response"
NS = {"nc": "http://api.namecheap.com/xml.response"}
BASE = {"ApiUser": "any", "UserName": "any", "ClientIp": "any", "ApiKey": os.environ["MAILDNS_KEY"]}
def call(command, **params):
r = requests.post(API, data={**BASE, "Command": command, **params}, timeout=90)
root = ET.fromstring(r.content)
if root.get("Status") != "OK":
raise RuntimeError(root.find("nc:Errors/nc:Error", NS).text)
return root
def add_record(sld, tld, name, rtype, address, mxpref=10, ttl=3600):
"""setHosts replaces, so send every record the host already has, plus the new one."""
hosts = call("namecheap.domains.dns.getHosts", SLD=sld, TLD=tld).findall(".//nc:host", NS)
rows = [(h.get("Name"), h.get("Type"), h.get("Address"), h.get("MXPref"), h.get("TTL")) for h in hosts]
if (name, rtype, address) not in [(n, t, a) for n, t, a, _, _ in rows]:
rows.append((name, rtype, address, str(mxpref), str(ttl)))
params = {"SLD": sld, "TLD": tld}
if any(t == "MX" for _, t, _, _, _ in rows):
params["EmailType"] = "MX"
for i, (n, t, a, pref, row_ttl) in enumerate(rows, start=1):
params.update({f"HostName{i}": n, f"RecordType{i}": t, f"Address{i}": a,
f"MXPref{i}": pref, f"TTL{i}": row_ttl})
call("namecheap.domains.dns.setHosts", **params)
add_record("advisor.example", "com", "google._domainkey", "TXT",
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...IDAQAB")Nameservers#
namecheap.domains.dns.setCustom, namecheap.domains.dns.setDefault, the namecheap.domains.ns.* commands and NS records in setHosts are refused with Error Number="2011280": This API key doesn't work for nameservers. Reading them with namecheap.domains.dns.getList is fine. Registration commands (create, renew, transfer, contacts) are not available.
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.