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

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

```json
{"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](https://maildns.io/docs/errors.md).

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

## Endpoints

| Method and path | Does |
|---|---|
| [`GET /api/v1/domains`](#list-hosts) | Your hosts |
| [`GET /api/v1/domains/{domain}`](#get-one-host) | One host |
| [`GET /api/v1/dns/records/{domain}`](#list-records) | Records |
| [`PUT /api/v1/dns/records/{domain}`](#add-records) | Add records |
| [`DELETE /api/v1/dns/records/{domain}`](#delete-records) | Remove records |
| [`GET /api/v1/dns/status/{domain}`](#check-records) | 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](https://maildns.io/docs/keys.md#record-rules)).

```json
{"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.

```bash
curl 'https://api.maildns.io/api/v1/domains?take=100&skip=0' \
  -H 'X-API-Key: KEY' -H 'X-API-Secret: SECRET'
```

```json
{
  "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.

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

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

```json
{
  "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**.

```bash
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 `PUT` is harmless.
- `PUT` never removes anything, except with `force` (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 `items` list is 422.

## Delete records

`DELETE /api/v1/dns/records/{domain}` with a JSON array of items. Replies **204**.

```bash
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 `exchange` for MX, `cname` for CNAME, `address` for A and AAAA). `ttl` is ignored.
- Items that don't exist are ignored, so repeating a `DELETE` is harmless.
- To change a record, `DELETE` the old one, then `PUT` the new one.

## Nameservers

No key can change nameservers. A nameserver update, or an `NS` item in `PUT`, replies HTTP 403:

```json
{"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`, `type` and `value`**, it checks exactly that record and always returns it, with `"live": false` until every nameserver serves it.
- **With fewer parameters**, it checks every record of `{domain}` that matches the ones you gave.

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

```json
{
  "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](https://maildns.io/docs/propagation.md).
