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.
curl -u "yourusername:your_api_token" \
https://api.name.com/core/v1/domains/example.com:getAuthCodedef 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
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
}'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:
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.
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 requestsSequential 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.