Spaceship-style API#
https://api.maildns.io/api/v1 takes the same paths, headers and JSON bodies as Spaceship's DNS records API. Code written for Spaceship works after you change the base URL, the key and the secret.
Requests and replies#
Every call sends three headers:
X-API-Key: KEY
X-API-Secret: SECRET
Content-Type: application/json- Reads reply 200 with JSON. Writes reply 204 with no body once the change is saved at the authoritative nameservers.
- Errors reply 4xx or 5xx with a JSON body in Spaceship's shape:
{"detail": "www.example.com is not one of your hosts", "data": []}| Status | Meaning |
|---|---|
| 400 | The body is not valid JSON |
| 401 | Wrong key or secret, or a call from an IP address the key is not locked to |
| 403 | A name that is not at or under one of your hosts, or a nameserver change |
| 404 | A domain that is not yours, or an unknown path |
| 422 | An invalid record or request: type, value, TTL, CNAME clash, too many records |
| 429 | Too many requests, or an earlier change to the same root domain is still running. Wait Retry-After seconds (or a few seconds if there is none) and retry. |
| 503 | The change could not be made right now. Nothing was changed. |
More in Errors.
{domain} in a path is one of your hosts (advisor.example.com) or the root domain they live in (example.com), except in /domains/{domain}, which takes a host. See Hosts.
Endpoints#
| Method and path | Does |
|---|---|
GET /api/v1/domains |
Your hosts |
GET /api/v1/domains/{domain} |
One host |
GET /api/v1/dns/records/{domain} |
Records |
PUT /api/v1/dns/records/{domain} |
Add records |
DELETE /api/v1/dns/records/{domain} |
Remove records |
GET /api/v1/dns/status/{domain} |
Whether a record is live on every nameserver (MailDNS only) |
Record items#
Names are relative to {domain}; @ is {domain} itself. ttl is optional (300 to 86400, default 3600); it is used where the domain supports a TTL per record, otherwise the record gets the domain's TTL (more).
{"type": "TXT", "name": "@", "value": "google-site-verification=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ", "ttl": 3600}
{"type": "MX", "name": "@", "exchange": "smtp.google.com", "preference": 1, "ttl": 3600}
{"type": "CNAME", "name": "mail", "cname": "ghs.googlehosted.com", "ttl": 3600}
{"type": "A", "name": "@", "address": "203.0.113.10", "ttl": 3600}
{"type": "TXT", "name": "google._domainkey", "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqh...", "ttl": 3600}| Type | Value field | Also |
|---|---|---|
TXT |
value: the text, without quotes, up to 2,048 characters |
|
MX |
exchange: the mail server |
preference: 0 to 65535 |
CNAME |
cname: the target name |
|
A |
address: an IPv4 address |
|
AAAA |
address: an IPv6 address |
Replies also carry "group": {"type": "custom"} on each item, as Spaceship does. You don't need to send it.
List hosts#
GET /api/v1/domains?take=100&skip=0: one item per host on your key. take is 1 to 100.
curl 'https://api.maildns.io/api/v1/domains?take=100&skip=0' \
-H 'X-API-Key: KEY' -H 'X-API-Secret: SECRET'{
"items": [
{
"name": "advisor.example.com",
"unicodeName": "advisor.example.com",
"isPremium": false,
"autoRenew": true,
"registrationDate": "2025-01-01T00:00:00.000Z",
"expirationDate": "2027-01-01T00:00:00.000Z",
"lifecycleStatus": "registered",
"verificationStatus": null,
"eppStatuses": ["clientTransferProhibited"],
"suspensions": [],
"privacyProtection": {"level": "high", "contactForm": true},
"nameservers": {"provider": "basic", "hosts": ["ns1.example.net", "ns2.example.net"]}
}
],
"total": 1
}The dates are the root domain's. Fields other than name, unicodeName, the dates, lifecycleStatus and nameservers are there so Spaceship parsers don't break; don't rely on them.
Get one host#
GET /api/v1/domains/{domain} with a host: the same item, on its own. Anything else is 404.
curl https://api.maildns.io/api/v1/domains/advisor.example.com \
-H 'X-API-Key: KEY' -H 'X-API-Secret: SECRET'List records#
GET /api/v1/dns/records/{domain}?take=500&skip=0. take is 1 to 500.
curl 'https://api.maildns.io/api/v1/dns/records/advisor.example.com?take=500&skip=0' \
-H 'X-API-Key: KEY' -H 'X-API-Secret: SECRET'{
"items": [
{"type": "MX", "name": "@", "ttl": 3600, "exchange": "smtp.google.com", "preference": 1, "group": {"type": "custom"}},
{"type": "TXT", "name": "@", "ttl": 3600, "value": "v=spf1 include:_spf.google.com ~all", "group": {"type": "custom"}},
{"type": "TXT", "name": "_dmarc", "ttl": 3600, "value": "v=DMARC1; p=quarantine", "group": {"type": "custom"}}
],
"total": 3
}With the root domain in the path, you get every record of your hosts in that root, with names like advisor and _dmarc.advisor.
Add records#
PUT /api/v1/dns/records/{domain} with {"force": false, "items": [...]}. Replies 204.
curl -X PUT https://api.maildns.io/api/v1/dns/records/advisor.example.com \
-H 'X-API-Key: KEY' -H 'X-API-Secret: SECRET' -H 'Content-Type: application/json' \
-d '{
"force": false,
"items": [
{"type": "MX", "name": "@", "exchange": "smtp.google.com", "preference": 1, "ttl": 3600},
{"type": "TXT", "name": "@", "value": "v=spf1 include:_spf.google.com ~all", "ttl": 3600},
{"type": "TXT", "name": "_dmarc", "value": "v=DMARC1; p=quarantine", "ttl": 3600}
]
}'- The items are added to what is there. Items that are already there (same type, name and value) are skipped, so repeating a
PUTis harmless. PUTnever removes anything, except withforce(next point).- A CNAME can't share its name with other records. If an item would break that rule, the call is refused with 422, unless
"force": true: then the records already at that name are replaced by the new ones. - An empty
itemslist is 422.
Delete records#
DELETE /api/v1/dns/records/{domain} with a JSON array of items. Replies 204.
curl -X DELETE https://api.maildns.io/api/v1/dns/records/advisor.example.com \
-H 'X-API-Key: KEY' -H 'X-API-Secret: SECRET' -H 'Content-Type: application/json' \
-d '[{"type": "TXT", "name": "_dmarc", "value": "v=DMARC1; p=none"}]'- Removes exactly the records you list, matched on type, name and value (the
exchangefor MX,cnamefor CNAME,addressfor A and AAAA).ttlis ignored. - Items that don't exist are ignored, so repeating a
DELETEis harmless. - To change a record,
DELETEthe old one, thenPUTthe new one.
Nameservers#
No key can change nameservers. A nameserver update, or an NS item in PUT, replies HTTP 403:
{"detail": "This API key doesn't work for nameservers.", "data": []}nameservers in the domain items is there to read.
Check records#
GET /api/v1/dns/status/{domain}?name=&type=&value= is MailDNS's own endpoint: it asks every authoritative nameserver directly whether it serves a record. Use it after a write and before asking Google or Microsoft to verify.
- With
name,typeandvalue, it checks exactly that record and always returns it, with"live": falseuntil every nameserver serves it. - With fewer parameters, it checks every record of
{domain}that matches the ones you gave.
curl -G https://api.maildns.io/api/v1/dns/status/advisor.example.com \
-H 'X-API-Key: KEY' -H 'X-API-Secret: SECRET' \
--data-urlencode name=@ \
--data-urlencode type=TXT \
--data-urlencode value=google-site-verification=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ{
"items": [
{
"name": "advisor.example.com",
"type": "TXT",
"value": "google-site-verification=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ",
"live": true,
"servers": {"ns1.example.net": true, "ns2.example.net": true}
}
]
}name in the reply is the record's full name. How to poll it: Checking propagation.