Docs pages

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
<?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
<?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#

Shell
curl 'https://api.maildns.io/xml.response?ApiUser=any&UserName=any&ApiKey=KEY&ClientIp=any&Command=namecheap.domains.getList'
XML
<?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#

Shell
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
<?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:

Shell
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#

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:

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=@ \
  --data-urlencode record_type=txt \
  --data-urlencode value=google-site-verification=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ

Wait for "Live": "yes" before asking Google or Microsoft to verify.