DEV Community

Cover image for How to Build a Domain Registration Integration with Python and the name.com API
Jakkie Koekemoer
Jakkie Koekemoer

Posted on

How to Build a Domain Registration Integration with Python and the name.com API

If you're building an app that lets users pick a custom domain, you have two options. You can send them off to a registrar's website and hope they come back. Or you can handle the whole flow yourself. Platforms like Vercel, Replit, and Netlify chose the second path. They integrated domain search, registration, and DNS management directly into their products, using name.com as the backend registrar to power it all.

This guide shows you how to build that same integration. We'll walk through the complete domain lifecycle: authentication, availability search, registration, and DNS configuration, using the name.com API. Python with the requests library is the primary language here, because it's the clearest way to show what's actually happening over the wire. That said, the same concepts apply in most languages, and you'll find equivalent code blocks in PHP, Node.js, and Ruby throughout.

A quick note on scope: each language has its own conventions for package management and environment variable handling, but those details are out of scope here. Only the necessary code snippets are included.

By the end, you'll have a working Domain Manager script that covers the full lifecycle in under 200 lines. No heavy SDK to install. No XML to parse. Just HTTP calls and JSON responses.

Here's the lifecycle we're building:

Domain Registration Integration

Section 1: Prerequisites and Authentication

Getting Your API Credentials

Before you write a single line of code, you need two things: your name.com username and an API token. Log in to your account, navigate to the API settings page via API > API Token Management, and generate a new token. The username is the same one you use to log in, not your email address.

Name.com provides a separate test environment at https://api.dev.name.com, which runs on a different set of credentials you create at www.name.com/reseller. I'd strongly recommend starting there. Accidentally registering a live domain while debugging your error handling is an expensive way to learn a lesson.

Store your credentials as environment variables. Never hardcode them in source files.

# .env file (add this to .gitignore)
NAMECOM_USERNAME=your_username
NAMECOM_API_TOKEN=your_api_token
NAMECOM_API_URL=https://api.dev.name.com  # switch to api.name.com/ for production
Enter fullscreen mode Exit fullscreen mode

The "Hello World" Request

The name.com API uses HTTP Basic Auth. Your username goes in as the username, your API token as the password. No OAuth dance, no token exchange. Just a standard header that requests handles in one line.

Here's a minimal client class that verifies your credentials are working:

Python

import os
import requests
from dotenv import load_dotenv

load_dotenv()

class NameComClient:
    def __init__(self):
        self.base_url = os.environ["NAMECOM_API_URL"]
        self.auth = (
            os.environ["NAMECOM_USERNAME"],
            os.environ["NAMECOM_API_TOKEN"]
        )
        self.headers = {"Content-Type": "application/json"}

    def hello(self):
        """Verify credentials by calling the hello endpoint."""
        response = requests.get(
            f"{self.base_url}/core/v1/hello",
            auth=self.auth,
            headers=self.headers
        )
        response.raise_for_status()
        return response.json()

# use your new NameComClient class
client = NameComClient()
print(client.hello())
Enter fullscreen mode Exit fullscreen mode

Run this and you should see your username echoed back. A 401 means your token is wrong. A 404 means the base URL is off.

PHP (GuzzleHttp)

<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;

$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/');
$dotenv->load();

$client = new Client([
    'base_uri' => $_ENV['NAMECOM_API_URL'],
    'auth' => [$_ENV['NAMECOM_USERNAME'], $_ENV['NAMECOM_API_TOKEN']],
    'headers' => ['Content-Type' => 'application/json'],
]);

$response = $client->get('/core/v1/hello');
echo $response->getBody();
Enter fullscreen mode Exit fullscreen mode

Node.js (axios)

const axios = require('axios');
require('dotenv').config();

const client = axios.create({
  baseURL: process.env.NAMECOM_API_URL,
  auth: {
    username: process.env.NAMECOM_USERNAME,
    password: process.env.NAMECOM_API_TOKEN,
  },
  headers: { 'Content-Type': 'application/json' },
});

client.get('/core/v1/hello').then(res => console.log(res.data));
Enter fullscreen mode Exit fullscreen mode

Ruby (Faraday)

require 'faraday'
require 'dotenv/load'
require 'json'

conn = Faraday.new(url: ENV['NAMECOM_API_URL']) do |f|
  f.request :authorization, :basic, ENV['NAMECOM_USERNAME'], ENV['NAMECOM_API_TOKEN']
  f.headers['Content-Type'] = 'application/json'
end

response = conn.get('/core/v1/hello')
puts JSON.parse(response.body)
Enter fullscreen mode Exit fullscreen mode

Notice what's missing from every one of these examples. No SDK import. No proprietary client library. No 50-page setup guide. You're using the standard HTTP library for your language of choice, with basic authentication. It doesn't get more straightforward than that.

Section 2: Checking Domain Availability (The Search Pattern)

The Endpoint and What It Returns

Domain availability is a POST call to the /domains:search endpoint. You POST a keyword, and name.com returns a list of domain suggestions with their availability status and pricing. The price data is why this matters. You don't just need to know if a domain is available, you need to know what it costs before you show it to a user.

The response distinguishes between standard and premium domains. A premium domain like car.com might run $50,000 at registration, while mycoolstartup.com will cost considerably less. Your code needs to handle both cases, or you'll accidentally surface $50,000 domains to users expecting standard pricing.

Python Walkthrough

Add this method to your NameComClient class:

def search_domains(self, keyword: str, tlds: list = None) -> list:
        """
        Search for available domains matching a keyword.
        Returns a list of available domain options with pricing.
        """
        if tlds is None:
            tlds = ["com", "net", "org", "io"]

        payload = {
            "keyword": keyword,
            "tldFilter": tlds,
            "timeout": 10000  # milliseconds to wait for results
        }

        response = requests.post(
            f"{self.base_url}/core/v1/:domains:search",
            auth=self.auth,
            headers=self.headers,
            json=payload
        )
        response.raise_for_status()
        data = response.json()

        results = []

        for result in data.get("results", []):
            domain_info = {
                "domain": result["domainName"],
                "available": result.get("purchasable", False),
                "price": result.get("purchasePrice", 0),
                "premium": result.get("premium", False),
            }

            # Flag premium domains clearly — they can cost orders of magnitude more
            if domain_info["premium"]:
                domain_info["premium_price"] = result.get("premiumRenewalPrice")

            if domain_info["available"]:
                results.append(domain_info)

        return results
Enter fullscreen mode Exit fullscreen mode

To use it:

# search available domains
client = NameComClient()
results = client.search_domains("mycoolstartup")
for r in results:
    status = "[PREMIUM]" if r["premium"] else ""
    print(f"{r['domain']}: ${r['price']:.2f}/year {status}")
Enter fullscreen mode Exit fullscreen mode

This returns all domains that are available and match or are similar to your keyword. The purchasable field is your source of truth for availability. The purchasePrice is in dollars. Premium domains will have "premium": true. Filter those out or handle them separately depending on your use case.

PHP

function searchDomains(Client $client, string $keyword, array $tlds = ['com', 'net', 'io']): array {
    $response = $client->post('/core/v1/domains:search', [
        'json' => ['keyword' => $keyword, 'tldFilter' => $tlds, 'timeout' => 10000],
    ]);
    $data = json_decode($response->getBody(), true);
    return (array_filter($data['results'] ?? [], fn($r) => $r['purchasable'] && !$r['premium']));
}

// Use your new function
print_r(searchDomains($client, 'mycoolstartup'));
Enter fullscreen mode Exit fullscreen mode

Node.js

async function searchDomains(keyword, tlds = ['com', 'net', 'io']) {
  const { data } = await client.post('/core/v1/domains:search', {
    keyword,
    tldFilter: tlds,
    timeout: 10000,
  });
  return (data.results || []).filter(r => r.purchasable && !r.premium);
}

// Use your new function
(async () => {
  const result = await searchDomains('mycoolstartup'); // Pause until the Promise resolves
  console.log(result); // Logs "Resolved data"
})();
Enter fullscreen mode Exit fullscreen mode

Ruby

def search_domains(conn, keyword, tlds = ['com', 'net', 'io'])
  response = conn.post('/core/v1/domains:search') do |req|
    req.body = { keyword: keyword, tldFilter: tlds, timeout: 10_000 }.to_json
  end
  data = JSON.parse(response.body)
  (data['results'] || []).select { |r| r['purchasable'] && !r['premium'] }
end

# Use your new function
result = search_domains(conn, 'mycoolstartup')
puts result
Enter fullscreen mode Exit fullscreen mode

Section 3: Domain Registration (The Transaction)

What Domain Registration Actually Requires

This is where things get more complex. Registering a domain isn't just naming it. ICANN requires contact information for four roles: Registrant, Admin, Tech, and Billing. Each contact needs a name, organization, address, phone number, and email. In most cases, all four roles will be the same person. If not, you'll need to expand the code to accept a different contact object for each role.

Python Walkthrough

Add this method to your NameComClient class:

   def register_domain(self, domain_name: str, contact_details: dict, years: int = 1) -> dict:
        """
        Register a domain 
        """
        payload = { 
            "domain": {
                "domainName": domain_name,
                "years": years,
                    "contacts": { 
                        "tech": {
                            "firstName": contact_details['firstName'],
                            "lastName": contact_details['lastName'],
                            "companyName": contact_details['companyName'],
                            "address1": contact_details['address1'],
                            "address2": contact_details['address2'],
                            "city": contact_details['city'],
                            "state": contact_details['state'],
                            "zip": contact_details['zip'],
                            "country": contact_details['country'],
                            "email": contact_details['email'],
                            "phone": contact_details['phone']
                        }
                    },
                    "privacyEnabled": True
                } 
            }

        try:
            response = requests.post(
                f"{self.base_url}/core/v1/domains",
                auth=self.auth,
                headers=self.headers,
                json=payload
            )
            response.raise_for_status()
            return response.json()

        except requests.exceptions.HTTPError as e:
            error_data = e.response.text

            raise ValueError(f"An error has occured while trying to register: {domain_name}\nThe error is: {error_data}")
Enter fullscreen mode Exit fullscreen mode

To use it:

client = NameComClient()
contact1 = {
    "firstName": "Jonny",
    "lastName": "IT Guy",
    "companyName": "My Company",
    "address1": "1234 Any Street",
    "address2": "None",
    "city": "Anytown",
    "state": "NY",
    "zip": "11222",
    "country": "US",
    "email": "support@example.com",
    "phone": "+15555555"
}
try:
    result = client.register_domain("mycoolstartup.com", contact_details=contact1)
    print(f"Registered: {result['domain']['domainName']} — expires {result['domain']['expireDate']}")
except ValueError as e:
    print(f"Cannot register: {e}")
Enter fullscreen mode Exit fullscreen mode

A 409 Conflict response means the domain was taken between your availability check and your registration attempt. It happens.

PHP

function registerDomain(Client $client, string $domain, array $contact_details, int $years): array {
    try {
        $response = $client->post('/core/v1/domains', [
            'json' => [
                "domain" => [
                    "domainName" => $domain,
                    "years" => $years,
                    "contacts" => [
                        "tech" => [
                            "firstName" => $contact_details['firstName'],
                            "lastName" => $contact_details['lastName'],
                            "companyName" => $contact_details['companyName'],
                            "address1" => $contact_details['address1'],
                            "address2" => $contact_details['address2'],
                            "city" => $contact_details['city'],
                            "state" => $contact_details['state'],
                            "zip" => $contact_details['zip'],
                            "country" => $contact_details['country'],
                            "email" => $contact_details['email'],
                            "phone" => $contact_details['phone'],
                        ],
                    ],
                    "privacyEnabled" => 1,
                ],
            ],
        ]);
        return json_decode($response->getBody(), true);
    } catch (\GuzzleHttp\Exception\ClientException $e) {
        $body = json_decode($e->getResponse()->getBody(), true);
        throw new \RuntimeException('Registration failed: ' . ($body['message'] ?? 'unknown'));
    }
}

// Use your new function:
$contact_details = [
    "firstName" => "Jonny",
    "lastName" => "IT Guy",
    "companyName" => "My Company",
    "address1" => "1234 Any Street",
    "address2" => "None",
    "city" => "Anytown",
    "state" => "NY",
    "zip" => "11222",
    "country" => "US",
    "email" => "support@example.com",
    "phone" => "+15555555",
];

print_r(registerDomain($client, 'mycoolstartup.com', $contact_details, 1));
Enter fullscreen mode Exit fullscreen mode

Node.js

async function registerDomain(domainName, contact_details, years) {
  try {
    const { data } = await client.post('/core/v1/domains', {
      domain: {
        domainName: domainName,
        years: years,
        contacts: {
          tech: {
            firstName:   contact_details.firstName,
            lastName:    contact_details.lastName,
            companyName: contact_details.companyName,
            address1:    contact_details.address1,
            address2:    contact_details.address2,
            city:        contact_details.city,
            state:       contact_details.state,
            zip:         contact_details.zip,
            country:     contact_details.country,
            email:       contact_details.email,
            phone:       contact_details.phone
          }
        },
        privacyEnabled: true
      }
    });
    return data;
  } catch (err) {
    const message = err.response?.data?.message || 'Unknown error';
    throw new Error(`Registration failed: ${message}`);
  }
}

// Use your new function
let contact1 = {
    "firstName": "Jonny",
    "lastName": "IT Guy",
    "companyName": "My Company",
    "address1": "1234 Any Street",
    "address2": "None",
    "city": "Anytown",
    "state": "NY",
    "zip": "11222",
    "country": "US",
    "email": "support@example.com",
    "phone": "+15555555"
};

(async () => {
  const result = await registerDomain('mycoolstartup.com', contact1, 1); // Pause until the Promise resolves
  console.log(result); // Logs "Resolved data"
})();
Enter fullscreen mode Exit fullscreen mode

Ruby

def register_domain(conn, domain_name, contact_details, years)
  payload = {
    domain: {
        domainName: domain_name,
        years: years,
        contacts: {
          tech: {
            firstName:   contact_details[:firstName],
            lastName:    contact_details[:lastName],
            companyName: contact_details[:companyName],
            address1:    contact_details[:address1],
            address2:    contact_details[:address2],
            city:        contact_details[:city],
            state:       contact_details[:state],
            zip:         contact_details[:zip],
            country:     contact_details[:country],
            email:       contact_details[:email],
            phone:       contact_details[:phone]
          }
        },
        privacyEnabled: true
      }
    }
  response = conn.post('/core/v1/domains') { |req| req.body = payload.to_json }
  raise "Registration failed: #{JSON.parse(response.body)['message']}" unless response.status == 200
  JSON.parse(response.body)
end

# Use your new function
contact1 = {
    firstName: "Jonny",
    lastName: "IT Guy",
    companyName: "My Company",
    address1: "1234 Any Street",
    address2: "None",
    city: "Anytown",
    state: "NY",
    zip: "11222",
    country: "US",
    email: "support@example.com",
    phone: "+15555555"
}

result = register_domain(conn, 'mycoolstartup.com', contact1, 1)
puts result
Enter fullscreen mode Exit fullscreen mode

Section 4: Managing DNS Records (The Configuration)

The Scenario

You've registered mycoolstartup.com. Now you need to point it at your server at 192.168.113.42. That means creating an A record with a TTL of 300 seconds. Here's how to do it programmatically.

Listing Existing DNS Records

Before you add records, check what's already there. Name.com sets up default nameserver records when you register a domain, and you don't want to create conflicts.

Add this method to your NameComClient class:

def list_dns_records(self, domain_name: str) -> list:
        """List all DNS records for a domain."""
        response = requests.get(
            f"{self.base_url}/core/v1/domains/{domain_name}/records",
            auth=self.auth,
            headers=self.headers
        )
        response.raise_for_status()
        return response.json().get("records", [])

# Use your new function
client = NameComClient()
records = client.list_dns_records("mycoolstartup.com")
for record in records:
    print(f"{record['type']} {record['host']} -> {record['answer']}")
Enter fullscreen mode Exit fullscreen mode

Creating an A Record

def point_to_ip(self, domain_name: str, ip_address: str, host: str = "") -> dict:
    """
    Create an A record pointing a domain (or subdomain) to an IP address.
    host="" targets the apex domain (@).
    host="www" targets www.mycoolstartup.com.
    """
    payload = {
        "host": host,
        "type": "A",
        "answer": ip_address,
        "ttl": 300,  # 5-minute TTL, sensible default, fast to propagate
    }

    response = requests.post(
        f"{self.base_url}/core/v1/domains/{domain_name}/records",
        auth=self.auth,
        headers=self.headers,
        json=payload
    )
    response.raise_for_status()
    return response.json()

# Usage: point www to the new server
client = NameComClient()
client.point_to_ip("mycoolstartup.com", "192.168.113.42", host="www")
print("A record created. DNS will propagate within ~10 minutes.")
Enter fullscreen mode Exit fullscreen mode

PHP

function point_to_ip(Client $client, string $domain, string $ip, string $host = ''): array {
    $response = $client->post("/core/v1/domains/{$domain}/records", [
        'json' => ['host' => $host, 'type' => 'A', 'answer' => $ip, 'ttl' => 300],
    ]);
    return json_decode($response->getBody(), true);
}

// Use your new function
print_r(point_to_ip($client, 'mycoolstartup.com', '192.168.113.42', 'www'));
Enter fullscreen mode Exit fullscreen mode

Node.js

async function point_to_ip(domainName, ip, host = '') {
  const { data } = await client.post(`/core/v1/domains/${domainName}/records`, {
    host, type: 'A', answer: ip, ttl: 300,
  });
  return data;
}

// Use your new function
(async () => {
  const result = await point_to_ip('mycoolstartup.com', '192.168.13.42', 'www'); // Pause until the Promise resolves
  console.log(result); // Logs "Resolved data"
})();
Enter fullscreen mode Exit fullscreen mode

Ruby

def point_to_ip(conn, domain_name, ip, host = '')
  response = conn.post("/core/v1/domains/#{domain_name}/records") do |req|
    req.body = { host: host, type: 'A', answer: ip, ttl: 300 }.to_json
  end
  JSON.parse(response.body)
end

# Use your new function
result = point_to_ip(conn, 'mycoolstartup.com', '192.168.13.42', 'www')
puts result
Enter fullscreen mode Exit fullscreen mode

Beyond A records, you can create CNAME, MX, TXT, and NS records using the same endpoint. Just swap the type field and set answer accordingly. A TXT record for domain verification with Google Search Console or Mailgun follows the exact same pattern with "type": "TXT".

Section 5: Common Domain API Integration Patterns

Handling Rate Limits

The name.com API returns 429 Too Many Requests if you send too many calls in a short window. This probably won't come up in a standard single-user UI flow, but it gets real quickly when you're running bulk operations like checking hundreds of domains at once.

The simplest fix is a retry loop with exponential backoff:

import time

def api_request_with_retry(self, method: str, endpoint: str, **kwargs) -> dict:
    """Make an API request with automatic retry on rate limiting."""
    max_retries = 3
    for attempt in range(max_retries):
        response = requests.request(
            method,
            f"{self.base_url}/core/v1/{endpoint}",
            auth=self.auth,
            headers=self.headers,
            **kwargs
        )
        if response.status_code == 429:
            wait_seconds = 2 ** attempt  # 1s, 2s, 4s
            print(f"Rate limited. Retrying in {wait_seconds}s...")
            time.sleep(wait_seconds)
            continue
        response.raise_for_status()
        return response.json()
    raise RuntimeError("Max retries exceeded after rate limiting.")
Enter fullscreen mode Exit fullscreen mode

For bulk domain checks, say you're building a feature that checks 50 TLD variants of a brand name, keep your batch size under 20 per request and add a time.sleep(0.5) between calls. That's enough breathing room for most production workloads.

Name.com also supports webhooks. You can configure an endpoint to receive a notification when a domain registration completes on the registry side. That's the production-grade version of this pattern: queue the task, let name.com ping you when it's done, then update your UI.

Final Thoughts

You've just walked through the full domain lifecycle in four endpoints and fewer than 200 lines of Python. The PHP, Node.js, and Ruby versions are structurally identical: set up Basic Auth, send JSON, parse the response.

That simplicity is the point. Legacy registrar APIs like Namecheap's require XML/SOAP parsing, a format that modern JSON-native languages handle awkwardly. AWS Route 53 works, but Boto3's IAM configuration is significant overhead for a task that's fundamentally just "register a domain." The name.com RESTful JSON API means any language with a decent HTTP library can become a domain client in an afternoon.

Get your API token from your account settings and start building.


Frequently Asked Questions

What is a domain registration API?

A domain registration API is a programmatic interface that lets developers search for domain availability, register domains, and manage DNS records directly within their application, without redirecting users to a registrar's website. The name.com RESTful API exposes these capabilities over standard HTTPS with JSON request and response bodies.

Which Python library should I use for the name.com domain API?

The requests library is the clearest choice for making HTTP calls to the name.com API. It handles Basic Auth in a single line and parses JSON responses natively. No SDK or domain-specific library is required.

How do I check domain availability programmatically?

Send a POST request to /domains:search with your keyword and an optional list of TLD filters. The response includes each domain's purchasable status and purchasePrice. Filter on purchasable: true and check the premium flag before surfacing results to users. Premium domains can cost orders of magnitude more than standard registrations.

What HTTP status codes should I handle from the name.com domain API?

The most important ones: 401 Unauthorized (bad credentials), 400 Bad Request (invalid request data, usually a generic issue with the POST body), and 429 Too Many Requests (rate limit hit, implement exponential backoff).

Top comments (0)