DNS API Tutorial

The DNS API provides a REST interface for managing the records in DNS zones (domains) hosted by Mythic Beasts.

Getting started

  • Create an API key using the customer control panel, and ensure that it has access rights for the DNS API. These can be granted for all zones on your account, for specific zones, or for individual records.

  • For this tutorial, we will use the curl command line utility. To make this easier, store the credentials in a .netrc file in your home directory. These will allow you to use curl -n to do HTTP Basic Authentication on each request. Your .netrc file should look something like this:

machine api.mythic-beasts.com
login abcd1234
password efgh56778

It's also possible to use HTTP Bearer authentication (see the auth service documentation). This is slightly more efficient if you are making multiple requests.

Simple operations

Listing zones

You can get a list of all zones that your API key has at least some permissions on:

$ curl -n https://api.mythic-beasts.com/dns/v2/zones
{ 
  "zones": [
    "example.com"
  ]
}

Getting records

$ curl -n https://api.mythic-beasts.com/dns/v2/zones/example.com/records
{
  "records": [
    {
      "data": "1.2.3.4", 
      "host": "test1", 
      "ttl": 300, 
      "type": "A"
    }, 
    {
      "data": "mx.mythic-beasts.com.", 
      "host": "@", 
      "ttl": 300, 
      "type": "MX",
      "mx_priority": 10
    }
  ]
}

It's also possible to get records for specific hosts and types:

$ curl -n https://api.mythic-beasts.com/dns/v2/zones/example.com/records/test1/A
{
  "records": [
    {
      "data": "1.2.3.4", 
      "host": "test1", 
      "ttl": 300, 
      "type": "A"
    }
  ]
}

Creating records

New records can be created using POST requests:

$ curl -n -X POST https://api.mythic-beasts.com/dns/v2/zones/example.com/records/test1/A -d data=4.3.2.1 
{
  "message": "1 records added", 
  "records_added": 1
}

We now have two A records for test1.example.com:

$ curl -n https://api.mythic-beasts.com/dns/v2/zones/example.com/records/test1/A

{
  "records": [
    {
      "data": "1.2.3.4", 
      "host": "test1", 
      "ttl": 300, 
      "type": "A"
    }, 
    {
      "data": "4.3.2.1", 
      "host": "test1", 
      "ttl": 300, 
      "type": "A"
    }
  ]
}

Data for new records can be provided in three different ways:

  • Form parameters, as shown above. This is the simplest way, particularly if using curl, but has limited ability to create multiple records in a single request.
  • JSON input. Specifying a Content-Type of application/json allows record data to be provided in the same format as the response shown above.
  • Zone file format, allowing new records to be provided in an the RFC 1035 zone file format, as used by bind.

Updating records

We can replace existing records using PUT requests. PUT requests will replace all records identified by the URL that it is made to. For example:

$ curl -n -X PUT https://api.mythic-beasts.com/dns/v2/zones/example.com/records/test1/A -d data=6.7.8.9
{
  "message": "1 record added", 
  "records_added": 1, 
  "records_removed": 2
}

This has replaced both of our previous A records with a new one.

We can use query parameters to be more specific about which records we want to replace. For example, if we just want to change a specific MX record, we can select by MX priority.

$ curl -n -X PUT https://api.mythic-beasts.com/dns/v2/zones/example.com/records/@/MX?mx_priority=10 -d data=mx2.mythic-beasts.com -d mx_priority=10
{
  "message": "1 records added", 
  "records_added": 1, 
  "records_removed": 1
}

Zone file formatted inputs and outputs

Operations that return records can use RFC 1035 zone file format by specifying an Accept header:

$ curl -n -H "Accept: text/dns" https://api.mythic-beasts.com/dns/v2/zones/example.com/records/test1/A
test1                  300 A     1.2.3.4
test1                  300 A     4.3.2.1

It's also possible to use this format as input by specificy a Content-Type of text/dns. For example, we can pipe the output of ssh-keygen -r directly into the API in order to create SSHFP records:

ssh-keygen -r test1 | curl -n -X PUT --data-binary @- -H "Content-Type: text/dns" https://api.mythic-beasts.com/dns/v2/zones/example.com/records/test1/SSHFP
{
  "message": "8 records added", 
  "records_added": 8, 
  "records_removed": 0
}
curl -n -H "Accept: text/dns" https://api.mythic-beasts.com/dns/v2/zones/example.com/records/test1/SSHFP
test1                  300 SSHFP  1  1 e579ff6aabc2f0acf714deca53108a0c1ea7d799
test1                  300 SSHFP  1  2 7c47d5dfb748ff1fd244b7289d815e83dad8c2c1652b92ac8aed8ff166733d07
test1                  300 SSHFP  2  1 c5caf4cc8870acc7fd113e5a7c866822ec0d94de
test1                  300 SSHFP  2  2 9f11843fa1d9da318aa4bc09bbcaacaf4a9868c4d83dfc4bad6853d0c9597a31
test1                  300 SSHFP  3  1 eb8644f5fcfd555341f2063bd92044075e20da89
test1                  300 SSHFP  3  2 60f3e9780f9b87e5b4d6344f2ab46decbf705123e96ef07c3247f714ca220fc4
test1                  300 SSHFP  4  1 139426de48381ea46ad75dde4e412bf1c9b11e61
test1                  300 SSHFP  4  2 6f094181b510bbb573048835665773eb1a2a65fd4341d95207479ed71296491b

Advanced operations

The previous examples have updated records by POSTing or PUTing to a URL for a specific host and record type. It's also possible to submit updates to the base "all records" URL for the zone. In these case, we need to provide all fields required for the record as parameters. For example:

$ curl -n -X POST https://api.mythic-beasts.com/dns/v2/zones/example.com/records -d host=@ -d type=MX -d mx_priority=10 -d data=mx.mythic-beasts.com.
{
  "message": "1 records added", 
  "records_added": 1
}

Care must be taken when using this with PUT requests as by default it will replace the entire contents of the zone file with your new record(s) (although in practice, such requests will usually fail because the API will refuse to replace records that have come from a template – see below).

Identifying records to replace

If you want to replace multiple records, it's possible to do this by submitting DELETE requests for the old records, and then POSTing the new records. The problem with this approach is that it's not atomic: there may be a period when your DNS is live with neither the old nor the new records.

A better approach is to use a PUT request to replace the records in a single operation. To do this, you need specify exactly which records to replace. This can be done using query string parameters as shown before. If you want to replace multiple records that can't be selected by a single query, you can URL-encode separate queries, and provide them as multiple select parameters.

For example, suppose we want to replace all A and AAAA records we can use two queries, type=A and type=AAAA. The = encodes to %3D, so we can send a PUT request to:

https://api.mythic-beasts.com/dns/v2/zones/example.com/records?select=type%3DA&select=type%3DAAAA

It's possibe to filter on multiple fields in each select by combining them with and escaped & (%26). The filter will select records that match all of the specified fields in any of the select parameters.

This approach can be combined with record or host specific paths. For example, to target the A and AAAA records of the test1 host, we could PUT to:

https://api.mythic-beasts.com/dns/v2/zones/example.com/records/test1?select=type%3DA&select=type%3DAAAA

The filter will select records that match the path and all of the fields in any of the select parameters.

It's possible to see exactly which records will be replaced by making a GET request to the same URL.

Editing your zone file

The ability to export and import records in zone file format makes it easy to do bulk updates on your zone. Your zone may contain some records that cannot be edited. These are:

  • Template records, which come from the template that your zone is using. These provide NS records, and optionally other records.
  • Generated records, which are created by ANAME records.

In order to edit your zone file, you will need to filter these out, which you can do using the exclude-template and exclude-generated parameters. To export all the editable fields in your zone:

curl -n -H "Accept: text/dns" 'https://api.mythic-beasts.com/dns/v2/zones/example.com/records?exclude-template&exclude-generated' > my-zone-file

You can now edit your zone file, then replace it with a PUT request to the same URL

cat my-zone-file | curl -n -X PUT --data-binary @- -H "Content-Type: text/dns" 'https://api.mythic-beasts.com/dns/v2/zones/example.com/records?exclude-template&exclude-generated' 

As noted above, operations are atomic. If any record in your new zone file is invalid, the import will fail, and your zone file will be unmodified.

Verifying that changes are live

Changes made via the API should be live on our authoritative nameservers in less than two minutes. The API provides a mechanism for checking if the records served by the authoritative nameservers matches the latest changes made via the API. This can be useful when performing DNS-based challenges, such as those used by Let's Encrypt.

To check if records are live, simply specify the verify parameter to GET request for a specific host and record type. For example, to verify that the A record for www.example.com reflects the latest changes made, use:

curl -n 'https://api.mythic-beasts.com/dns/v2/zones/example.com/records/www/A?verify' -D-

If the nameservers are up-to-date, a 200 response will be returned in the normal way. Otherwise, this will return 409, with details of the differences. For example:

HTTP/1.1 409 Inconsistent
Date: Mon, 27 Apr 2020 20:02:48 GMT
Server: Apache/2.4.38 (Debian)
Content-Length: 156
Content-Type: application/json

{
  "errors": [
    "Received 8 record(s), expected 2 from ns1.mythic-beasts.com.",
    "Received 8 record(s), expected 2 from ns2.mythic-beasts.com."
  ]
}

This makes it easy to script a check. For example, the script below polls the API every 10 seconds to check the requested records.

#!/bin/bash

ZONE=example.com
RECORD=www
TYPE=A

for i in $(seq 1 12); do
    RES=$(curl -n https://api.mythic-beasts.com/dns/zones/$ZONE/records/$RECORD/$TYPE?verify -qs -w '%{http_code}' -o /dev/null)
    case $RES in
        200)    echo Records updated
                exit 0
                ;;
        409)    echo "Not yet updated ($i/12)"
                ;;
        *)      echo "Unexpected error: $RES"
                exit 1
                ;;
    esac
    sleep 10
done
echo Timed out
exit 1