Moving domains between registrars is one of those tasks that feels simple until you're doing it at scale. The math is straightforward: 10 minutes per domain × 50 domains = roughly 8 hours of manual work, and that assumes nothing goes wrong. Stale auth codes, missed unlock steps, forgotten confirmation emails, and zero visibility into in-flight transfer status mean something frequently goes wrong.

Every step of the transfer lifecycle maps directly to an API call. Scripting the workflow makes it idempotent, auditable, and repeatable. This tutorial walks through a complete implementation using the name.com API, from HTTP Basic Auth setup through bulk migration with status polling and error handling.

Why Manual Domain Transfers Break at Scale

A typical manual transfer cycle: log into the losing registrar's UI, disable WHOIS privacy, unlock the domain, generate an auth code, copy it somewhere safe, initiate the transfer at the gaining registrar, wait for a confirmation email, click through an approval link, then check back daily until the transfer completes or times out. Each domain takes 8-12 minutes when everything works.

At 50 domains, you're looking at 8+ hours spread across multiple sessions, with state tracked in a spreadsheet that has no retry logic, no idempotency, and no audit trail. The fix isn't faster clicking — every one of those steps is available through a registrar API.

The Domain Transfer Lifecycle

Before initiating any transfer, validate four preconditions: the domain is unlocked (clientTransferProhibited flag cleared), WHOIS privacy is disabled on TLDs that require it, the domain is more than 60 days old since registration or last transfer (ICANN policy), and zone records are backed up since DNS configuration doesn't travel with the domain.

The transfer state machine: initiated → pendingTransfer → pendingApproval (some registrars) → complete. It can also go to cancelled (within 5 days) or failed (invalid auth code or locked domain).

Retrieving the Domain Auth Code

Auth codes are time-sensitive. Retrieve them immediately before calling the transfer initiation endpoint, not as a pre-batch step hours earlier. For most TLDs, codes are valid for up to 7 days.

Bash
curl -u "yourusername:your_api_token" \
  https://api.name.com/core/v1/domains/example.com:getAuthCode
python
def get_auth_code(session, domain):
    resp = session.get(f"{BASE_URL}/domains/{domain}:getAuthCode")
    resp.raise_for_status()
    return resp.json()["authCode"]

Initiating the Domain Transfer

Bash
curl -u "yourusername:your_api_token" --request POST \
  --url https://api.name.com/core/v1/transfers \
  --header 'Content-Type: application/json' \
  --data '{
    "authCode": "Xk9#mP2qL8wR",
    "domainName": "example.com",
    "privacyEnabled": true
  }'
python
def initiate_transfer(session, domain, auth_code):
    payload = {
        "domainName": domain,
        "authCode": auth_code
    }
    resp = session.post(f"{BASE_URL}/transfers", json=payload)
    if resp.status_code == 200:
        return resp.json()
    raise RuntimeError(f"Transfer failed for {domain}: {resp.status_code} {resp.text}")

Polling Transfer Status with Exponential Backoff

The total transfer window is up to 7 days. Your polling loop needs to be patient. Use exponential backoff starting at 5-minute intervals, doubling each pass, capped at 60 minutes:

python
import time

def poll_transfer(session, domain, max_hours=168):  # 7 days
    interval = 300       # start at 5 minutes
    max_interval = 3600  # cap at 60 minutes
    elapsed = 0

    while elapsed < max_hours * 3600:
        resp = session.get(f"{BASE_URL}/transfers/{domain}")
        data = resp.json()
        status = data.get("status")

        if status == "complete":
            print(f"{domain}: transfer complete")
            return status
        elif status in ("cancelled", "failed"):
            print(f"{domain}: terminal state {status} - {data}")
            return status
        elif status == "pendingApproval":
            print(f"{domain}: pending approval - check registrar dashboard")

        time.sleep(interval)
        elapsed += interval
        interval = min(interval * 2, max_interval)

    raise TimeoutError(f"Transfer polling timed out for {domain}")

Scripting a Bulk Domain Migration

The full bulk migration script reads from a CSV with two columns: domain and auth_code. Leave auth_code blank for domains already registered at name.com — the script retrieves it programmatically. The idempotency check at the top of the loop prevents duplicate transfers on re-runs. If the script fails at domain 23 of 50, re-running it skips the first 22 already logged in the output CSV.

python
import csv
import time
import requests
from datetime import datetime

session = requests.Session()
session.auth = ("yourusername", "your_api_token")
BASE_URL = "https://api.name.com/core/v1"

def run_bulk_transfer(input_csv, output_csv):
    existing = load_existing_transfers(output_csv)
    domains = list(csv.DictReader(open(input_csv)))

    for row in domains:
        domain = row["domain"]
        auth_code = row.get("auth_code", "").strip()

        # Idempotency: skip if already initiated
        if domain in existing:
            print(f"{domain}: already initiated, transfer ID {existing[domain]}")
            continue

        # Retrieve auth code if not in CSV
        if not auth_code:
            try:
                auth_code = get_auth_code(session, domain)
            except Exception as e:
                print(f"{domain}: auth code retrieval failed - {e}")
                log_result(output_csv, domain, "", "auth_code_failed")
                continue

        # Initiate transfer
        try:
            data = initiate_transfer(session, domain, auth_code)
            log_result(output_csv, domain, domain, data.get("status"))
            print(f"{domain}: initiated, status={data.get('status')}")
        except Exception as e:
            print(f"{domain}: initiation failed - {e}")
            log_result(output_csv, domain, "", "initiation_failed")
            continue

        time.sleep(2)  # throttle between requests

Sequential processing with a 2-second sleep is conservative by design. The name.com API enforces a rate limit of 20 requests per second and 3,000 requests per hour, so sequential processing with a short delay keeps you well within both ceilings for batch operations.