Infrastructure as Code has changed how engineering teams manage servers, databases, and networks. But DNS is still a manual bottleneck for a lot of teams. You log into a dashboard, click through forms, and hope you don't fat-finger a record mid-deployment.
That works fine when you're managing a handful of domains. It stops working when you're juggling dozens of domains, spinning up ephemeral environments, or coordinating deployments across multiple services.
The name.com API is the fix. It's a REST API that lets you programmatically search, register, and manage your DNS records. Unlike registrars that bolt on API access as an enterprise upsell or an afterthought, name.com provides API access as a core feature, with standard HTTP methods, JSON responses, and real documentation.
By the end, you'll have the building blocks for DNS automation that can be triggered directly from your CI/CD pipeline.
Authentication and Security
API access starts in your name.com dashboard. Navigate to API, then API Token Management. You get two environments: production (api.name.com) and development/test (api.dev.name.com). The test environment uses your username with -test appended and a separate token. This lets you safely test DNS changes without touching live domains.
The API uses HTTP Basic Auth. Your username and token combine into a base64-encoded header. Here's how to construct it:
import base64
# Environment variables for credentials
username = os.getenv('NAMECOM_USERNAME')
token = os.getenv('NAMECOM_TOKEN')
# Construct the Basic Auth header
credentials = f"{username}:{token}"
encoded_credentials = base64.b64encode(credentials.encode()).decode()
auth_header = f"Basic {encoded_credentials}"
# Use in requests
headers = {
'Authorization': auth_header,
'Content-Type': 'application/json'
}
Always include this header in your API requests.
Never hardcode credentials in scripts. Use environment variables or a secrets manager like AWS Secrets Manager, HashiCorp Vault, or your CI/CD platform's native secrets feature. A .env file works for local development, but add it to .gitignore immediately.
For team environments, rotate tokens on a regular schedule. Name.com lets you generate multiple tokens, so you can implement token-per-service or token-per-developer patterns. If a token gets compromised, you can revoke it without disrupting other services.
One gotcha worth knowing: two-factor authentication on your name.com account blocks API access. If you need 2FA for security compliance, create a dedicated API-only account with restricted permissions.
Understanding API Constraints
The name.com API has a rate limit of 3,000 requests per hour. For most automation tasks, this is plenty. Updating records for a dozen domains or running scheduled zone backups fits comfortably within it.
Problems can show up during bulk operations. If you're migrating hundreds of records or running parallel updates across multiple environments, you'll start hitting 429 Too Many Requests responses. The solution is exponential backoff:
import time
import requests
def make_request_with_backoff(url, headers, method='GET', data=None, max_retries=5):
"""
Makes an API request with exponential backoff on rate limit errors.
Args:
url: API endpoint URL
headers: Request headers including auth
method: HTTP method (GET, POST, PUT, DELETE)
data: JSON payload for POST/PUT requests
max_retries: Maximum number of retry attempts
Returns:
Response object or None if all retries failed
"""
for attempt in range(max_retries):
if method == 'GET':
response = requests.get(url, headers=headers)
elif method == 'POST':
response = requests.post(url, headers=headers, json=data)
elif method == 'PUT':
response = requests.put(url, headers=headers, json=data)
elif method == 'DELETE':
response = requests.delete(url, headers=headers)
if response.status_code == 429:
wait_time = 2 ** attempt
print(f"Rate limited. Waiting {wait_time} seconds before retry {attempt + 1}/{max_retries}")
time.sleep(wait_time)
continue
return response
print(f"Failed after {max_retries} attempts")
return None
Beyond rate limits, you'll also encounter standard HTTP status codes. 401 Unauthorized means your credentials are wrong, so check your username, token, and base64 encoding. 403 Forbidden usually means you're missing the Content-Type: application/json header on POST or PUT requests. 404 Not Found means the domain or record doesn't exist. In sandbox environments, you need to register domains before you can manage their DNS.
Retrieving Data and Handling Pagination
The name.com API doesn't have a "Download Zone File" button. You reconstruct the zone view programmatically by fetching all records through the List Records endpoint.
The catch is pagination. Large zones with hundreds of records don't return in a single response. The API uses page and perPage parameters, with a default of 1,000 records per page. That handles most domains, but for larger zones or when you want smaller batches, adjust perPage accordingly.
Here's the logic for reconstructing a complete zone:
import requests
import json
def get_all_dns_records(domain, headers):
"""
Retrieves all DNS records for a domain by handling pagination.
Args:
domain: Domain name (e.g., 'draftexample.work')
headers: Request headers with authentication
Returns:
List of all DNS records for the domain
"""
base_url = "https://api.name.com/core/v1"
all_records = []
page = 1
per_page = 1000
while True:
url = f"{base_url}/domains/{domain}/records?page={page}&perPage={per_page}"
response = requests.get(url, headers=headers)
if response.status_code != 200:
print(f"Error fetching records: {response.status_code}")
print(response.text)
break
data = response.json()
records = data.get('records', [])
if not records:
break
all_records.extend(records)
if len(records) < per_page:
break
page += 1
print(f"Fetched page {page-1}: {len(records)} records")
print(f"Total records retrieved: {len(all_records)}")
return all_records
# Example usage
domain = "draftexample.work"
records = get_all_dns_records(domain, headers)
# Save to local "zone state" for analysis
with open(f"{domain}_zone_backup.json", 'w') as f:
json.dump(records, f, indent=2)
Each record includes an id field. That's your handle for updates and deletions. The response structure looks like this:
[
{
"id": 12345,
"domainName": "draftexample.work",
"host": "test",
"fqdn": "test.draftexample.work",
"type": "A",
"answer": "127.0.0.1",
"ttl": 300
}
]
Store these IDs when building automation. A common pattern is maintaining a local state file that maps logical names, like "production-web-server," to record IDs. This lets you update records by intent rather than hunting through API responses every time.
Managing State with CRUD Operations
DNS automation comes down to four operations: creating records, reading records (covered above), updating records, and deleting records.
Creating Records
The POST /core/v1/domains/{domain}/records endpoint creates new records. You provide the record type, hostname, and answer. The API validates record-type-specific rules:
def create_dns_record(domain, record_type, host, answer, ttl=300, headers=None):
"""
Creates a new DNS record.
Args:
domain: Domain name
record_type: Type of record (A, CNAME, TXT, MX, etc.)
host: Hostname (use '' for apex domain)
answer: Record value (IP for A, hostname for CNAME, etc.)
ttl: Time to live in seconds (minimum 300)
headers: Request headers with authentication
Returns:
Created record data or None on failure
"""
url = f"https://api.name.com/core/v1/domains/{domain}/records"
payload = {
"host": host,
"type": record_type,
"answer": answer,
"ttl": ttl
}
if record_type == "MX":
payload["priority"] = 10
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
record = response.json()
print(f"Created {record_type} record: {host}.{domain} -> {answer}")
return record
else:
print(f"Failed to create record: {response.status_code}")
print(response.text)
return None
# Example: Create an A record for a staging environment
create_dns_record(
domain="draftexample.work",
record_type="A",
host="staging",
answer="192.168.2.50",
ttl=300,
headers=headers
)
Validation matters here. An A record needs a valid IPv4 address. CNAME records can't coexist with other record types at the same hostname. TXT records need proper quoting for spaces. The API returns 400 Bad Request with details when validation fails.
You can verify the record was created in your name.com dashboard:
Updating Records
Updates use PUT /core/v1/domains/{domain}/records/{record_id}. You need the record ID from a previous GET request:
def update_dns_record(domain, record_id, record_type, host, answer, ttl=300, headers=None):
"""
Updates an existing DNS record.
Args:
domain: Domain name
record_id: ID of the record to update
record_type: Type of record
host: Hostname
answer: New record value
ttl: Time to live in seconds
headers: Request headers with authentication
Returns:
Updated record data or None on failure
"""
url = f"https://api.name.com/core/v1/domains/{domain}/records/{record_id}"
payload = {
"host": host,
"type": record_type,
"answer": answer,
"ttl": ttl
}
response = requests.put(url, headers=headers, json=payload)
if response.status_code == 200:
print(f"Updated record {record_id}: {host}.{domain} -> {answer}")
return response.json()
else:
print(f"Failed to update record: {response.status_code}")
print(response.text)
return None
# Example: Update production IP after server migration
update_dns_record(
domain="draftexample.work",
record_id=123456,
record_type="A",
host="staging",
answer="192.168.2.100", # New IP
ttl=300,
headers=headers
)
Updates are atomic at the record level. The API replaces the entire record, not individual fields. This prevents partial updates that could create invalid states.
Once you've run update_dns_record, the change shows up in the name.com dashboard:
Deleting Records
Deletion uses DELETE /core/v1/domains/{domain}/records/{record_id}. It's worth adding safety checks to prevent accidental deletion of critical infrastructure:
def delete_dns_record(domain, record_id, headers, require_confirmation=True):
"""
Deletes a DNS record with safety checks.
Args:
domain: Domain name
record_id: ID of the record to delete
headers: Request headers with authentication
require_confirmation: If True, requires explicit confirmation
Returns:
True if successful, False otherwise
"""
url = f"https://api.name.com/core/v1/domains/{domain}/records/{record_id}"
response = requests.get(url, headers=headers)
if response.status_code == 200:
record = response.json()
print(f"About to delete: {record['type']} {record['fqdn']} -> {record['answer']}")
critical_hosts = ['', 'www', 'mail', 'mx']
if record['host'] in critical_hosts and require_confirmation:
confirm = input("This is a critical record. Type 'DELETE' to confirm: ")
if confirm != 'DELETE':
print("Deletion cancelled")
return False
response = requests.delete(url, headers=headers)
if response.status_code == 204:
print(f"Deleted record {record_id}")
return True
else:
print(f"Failed to delete record: {response.status_code}")
print(response.text)
return False
# Example: Clean up old staging environment
delete_dns_record(
domain="draftexample.work",
record_id=123456,
headers=headers,
require_confirmation=False # Automated cleanup
)
For bulk deletions, track which records were successfully deleted. If the operation fails partway through, you can resume without duplicating deletions.
A Strategic Use Case: Ephemeral Environments
DNS automation unlocks workflows that simply aren't practical with manual management. Ephemeral environments are a good example.
There are times when you spin up a full environment, like dev or staging, using CI/CD pipelines. DNS entries are usually a part of that setup that you end up doing by hand. With the name.com API, you can automate that step entirely.
Feature branch deployments create temporary infrastructure, and each pull request can get its own subdomain:
def create_ephemeral_environment(domain, branch_name, server_ip, headers):
"""
Creates DNS records for a temporary feature branch environment.
Args:
domain: Base domain
branch_name: Git branch name (sanitized for DNS)
server_ip: IP address of the ephemeral server
headers: Request headers with authentication
Returns:
Created record or None
"""
safe_branch = branch_name.lower().replace('_', '-').replace('/', '-')
host = f"feature-{safe_branch}"
return create_dns_record(
domain=domain,
record_type="A",
host=host,
answer=server_ip,
ttl=300, # Short TTL for quick cleanup
headers=headers
)
# In your CI/CD pipeline
branch = os.getenv('GITHUB_BRANCH') # e.g., 'user-auth-refactor'
server_ip = provision_server() # Your infrastructure provisioning
record = create_ephemeral_environment(
domain="draftexample.work",
branch_name=branch,
server_ip=server_ip,
headers=headers
)
print(f"Environment available at: https://feature-{branch}.example.com")
Keep in mind that the provision_server function (or similar function) is normally run on your CI/CD infrastructure and should already be in place for this code to work.
If that runs successfully from your CI/CD environment, you'll end up with a record that looks like this:



Top comments (0)