Hack of the day: simple python script for maintaining DNS records in Namecheap

By | February 17, 2023

I needed to add an include to the SPF record for my domain today, one which I sincerely hope will be temporary, but I fear the reason for the addition will be lost to the mists of time and I’ll be looking at my SPF record in five years saying to myself “wtf is that there?”

The obvious solution is to put a comment above or next to the SPF record explaining the change, but that’s impossible when your SPF records are maintained through a web portal, in my case Namecheap’s. Namecheap does not allow comments to be associated with DNS records created through its Advanced DNS management interface.

This finally prompted me to do something I’ve been intending to do for a long time but never got around to, in particular, writing some code to allow me to maintain my Namecheap DNS records in source control with change history rather than directly in the Namecheap web app.

I dug around a bit and found PyNamecheap, but it’s not being maintained and I didn’t want to deal with that. The Namecheap HTTP API is simple enough that I decided to just go ahead and code directly against it rather than using a wrapper library.

The result is namecheap-dns.py, a script which knows how to export DNS records from Namecheap to YAML or import from the same YAML format back into Namecheap. Share and enjoy!

#!/usr/bin/env python3
namecheap-dns.py – Export/import DNS records from/to Namecheap
This script can export DNS records from a domain in Namecheap to a YAML file or
import records from a YAML file in the same format into Namecheap. I use this
script to maintain my Namecheap DNS records in a source repository with change
history, i.e., "configuration as code" for my Namecheap DNS records.
Beware! The Namecheap API doesn't allow creating, modifying, or deleting
individual records. The only write operation supported by the API is replacing
all the records for the domain. Therefore, the expected way to use this script
with a domain that has records in it predating your use of the script is to
export all of the current records, modify the export file to reflect any
changes you want to make, and then import the modified file, possibly first
running the import with –dryrun to make sure it's going to do what you expect.
To use the script you need to enable the Namecheap API on your account and
whitelist the public IPv4 address of the host you're running the script on. See
https://www.namecheap.com/support/api/intro/ for details.
You need to have a config file, by default named namecheap-dns-config.json in
the current directory though you can specify a different file on the command
line, whose contents look like this:
ApiUser: [Namecheap username]
UserName: [Namecheap username]
ApiKey: [Namecheap API keyi]
ClientIP: [public IPv4 address of the host you're running the script on]
The YAML file containing the records looks like this:
– Address:
HostName: localhost
RecordType: A
TTL: '180'
– Address:
HostName: router
RecordType: A
– Address: email.my.domain
MXPref: 10
HostName: '@'
RecordType: MX
The order of records or of fields within individual records doesn't matter.
Of note: there's no way through this API to create records that show up on the
Namecheap Advanced DNS page as "dynamic DNS records," so if you have such a
record created through that page and then you export and import your records,
it will be converted into a regular DNS record. This doesn't seem to matter,
though, because the dynamic DNS update API will still work on it. :shrug:
Copyright 2023 Jonathan Kamens <jik@kamens.us>
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License at
<https://www.gnu.org/licenses/> for more details.
import argparse
from itertools import count
import json
from lxml import etree
import requests
import sys
import yaml
namecheap_api_url = 'https://api.namecheap.com/xml.response'
def parse_args():
parser = argparse.ArgumentParser(description='Export or import Namecheap '
'DNS records')
parser.add_argument('–config-file', action='store',
help='Config file (default namecheap-dns-config.yml)')
subparsers = parser.add_subparsers(title='subcommands', required=True)
import_parser = subparsers.add_parser(
'import', description='Import records into Namecheap')
import_parser.add_argument('–dryrun', action='store_true', default=False,
help='Say what would be done without doing it')
import_parser.add_argument('–input-file', type=argparse.FileType('r'),
default=sys.stdin, help='File to read records '
'from (default stdin)')
export_parser = subparsers.add_parser(
'export', description='Export records from Namecheap')
export_parser.add_argument('–output-file', type=argparse.FileType('w'),
default=sys.stdout, help='File to write '
'records to (default stdout)')
args = parser.parse_args()
return args
def make_namecheap_request(config, data):
request = data.copy()
'ApiUser': config['ApiUser'],
'UserName': config['UserName'],
'ApiKey': config['ApiKey'],
'ClientIP': config['ClientIP'],
response = requests.post(namecheap_api_url, request)
response_xml = etree.XML(response.content)
if response_xml.get('Status') != 'OK':
raise Exception('Bad response: {}'.format(response.content))
return response_xml
def get_records(config, sld, tld):
response = make_namecheap_request(config, {
'Command': 'namecheap.domains.dns.getHosts',
'SLD': sld,
'TLD': tld})
host_elements = response.xpath(
namespaces={'x': 'http://api.namecheap.com/xml.response'})
records = [dict(h.attrib) for h in host_elements]
for record in records:
record.pop('AssociatedAppTitle', None)
record.pop('FriendlyName', None)
record.pop('HostId', None)
record['HostName'] = record.pop('Name')
record.pop('IsActive', None)
record.pop('IsDDNSEnabled', None)
if record['Type'] != 'MX':
record.pop('MXPref', None)
record['RecordType'] = record.pop('Type')
if record['TTL'] == '1800':
return records
def do_import(args, config):
current = {dict_hash(r): r
for r in get_records(config, args.sld, args.tld)}
new = {dict_hash(r): r
for r in yaml.safe_load(args.input_file)}
changed = False
for r in current.keys():
if r not in new:
print(f'Removing {current[r]}')
changed = True
for r in new.keys():
if r not in current:
print(f'Adding {new[r]}')
changed = True
if not changed:
data = {
'Command': 'namecheap.domains.dns.setHosts',
'SLD': args.sld,
'TLD': args.tld,
for num, record in zip(count(1), new.values()):
for key, value in record.items():
data[f'{key}{num}'] = value
if not args.dryrun:
make_namecheap_request(config, data)
def do_export(args, config):
records = get_records(config, args.sld, args.tld)
yaml.dump(sorted(records, key=dict_hash), args.output_file)
def dict_hash(d):
d = d.copy()
name = d.pop('HostName')
type_ = d.pop('RecordType')
return (type_, name, json.dumps(d, sort_keys=True))
def main():
args = parse_args()
(args.sld, args.tld) = args.domain.split('.')
config = yaml.safe_load(open(args.config_file))
args.command(args, config)
if __name__ == '__main__':
Print Friendly, PDF & Email

Leave a Reply

Your email address will not be published. Required fields are marked *