DEV Community

Cover image for Automate DNS Management with the name.com API
Jakkie Koekemoer
Jakkie Koekemoer

Posted on

Automate DNS Management with the name.com API

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'
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
  }
]
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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:

Record created successfully

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
)
Enter fullscreen mode Exit fullscreen mode

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:

Record updated successfully

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
)
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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:

Created from CI/CD environment

Top comments (0)