# 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](https://maildns.io/docs/hosts.md). 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](https://maildns.io/docs/errors.md).

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

```bash
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

```bash
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:

```bash
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](https://maildns.io/docs/keys.md#record-rules) |
| `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](https://maildns.io/docs/propagation.md), and it takes the same key:

```bash
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.
