Features: - Domain management (check, register, renew, contacts) - DNS management (nameservers, records) - Glue records (child nameserver) support - TLD price tracking with KRW conversion - FastAPI REST server with OpenAI schema - MCP server for Claude integration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
550 lines
19 KiB
Python
550 lines
19 KiB
Python
"""
|
|
Namecheap API Python Wrapper
|
|
"""
|
|
|
|
import requests
|
|
import xml.etree.ElementTree as ET
|
|
from dataclasses import dataclass
|
|
|
|
NS = {"nc": "http://api.namecheap.com/xml.response"}
|
|
|
|
|
|
@dataclass
|
|
class NamecheapConfig:
|
|
api_user: str
|
|
api_key: str
|
|
username: str
|
|
client_ip: str
|
|
sandbox: bool = False
|
|
|
|
@property
|
|
def base_url(self) -> str:
|
|
if self.sandbox:
|
|
return "https://api.sandbox.namecheap.com/xml.response"
|
|
return "https://api.namecheap.com/xml.response"
|
|
|
|
|
|
@dataclass
|
|
class RegistrantInfo:
|
|
first_name: str
|
|
last_name: str
|
|
address1: str
|
|
city: str
|
|
state_province: str
|
|
postal_code: str
|
|
country: str
|
|
phone: str
|
|
email: str
|
|
organization: str = ""
|
|
address2: str = ""
|
|
|
|
|
|
class NamecheapError(Exception):
|
|
"""Namecheap API error"""
|
|
def __init__(self, code: str, message: str):
|
|
self.code = code
|
|
self.message = message
|
|
super().__init__(f"[{code}] {message}")
|
|
|
|
|
|
class NamecheapAPI:
|
|
def __init__(self, config: NamecheapConfig):
|
|
self.config = config
|
|
|
|
def _base_params(self) -> dict:
|
|
return {
|
|
"ApiUser": self.config.api_user,
|
|
"ApiKey": self.config.api_key,
|
|
"UserName": self.config.username,
|
|
"ClientIp": self.config.client_ip,
|
|
}
|
|
|
|
def _request(self, command: str, **kwargs) -> ET.Element:
|
|
params = self._base_params()
|
|
params["Command"] = command
|
|
params.update(kwargs)
|
|
|
|
response = requests.get(self.config.base_url, params=params, timeout=30)
|
|
response.raise_for_status()
|
|
|
|
root = ET.fromstring(response.text)
|
|
status = root.attrib.get("Status")
|
|
|
|
if status == "ERROR":
|
|
errors = root.find("nc:Errors", NS)
|
|
if errors is not None:
|
|
error = errors.find("nc:Error", NS)
|
|
if error is not None:
|
|
raise NamecheapError(
|
|
error.attrib.get("Number", "Unknown"),
|
|
error.text or "Unknown error"
|
|
)
|
|
raise NamecheapError("Unknown", "API returned error status")
|
|
|
|
return root
|
|
|
|
# ===== Domains =====
|
|
|
|
def domains_check(self, domains: list[str]) -> dict[str, bool]:
|
|
"""Check domain availability"""
|
|
domain_list = ",".join(domains)
|
|
root = self._request("namecheap.domains.check", DomainList=domain_list)
|
|
|
|
result = {}
|
|
for domain in root.findall(".//nc:DomainCheckResult", NS):
|
|
name = domain.attrib.get("Domain", "")
|
|
available = domain.attrib.get("Available", "false").lower() == "true"
|
|
result[name] = available
|
|
|
|
return result
|
|
|
|
def domains_get_list(self, page: int = 1, page_size: int = 20) -> list[dict]:
|
|
"""Get list of domains in account"""
|
|
root = self._request(
|
|
"namecheap.domains.getList",
|
|
Page=str(page),
|
|
PageSize=str(page_size)
|
|
)
|
|
|
|
domains = []
|
|
for domain in root.findall(".//nc:Domain", NS):
|
|
domains.append({
|
|
"id": domain.attrib.get("ID"),
|
|
"name": domain.attrib.get("Name"),
|
|
"user": domain.attrib.get("User"),
|
|
"created": domain.attrib.get("Created"),
|
|
"expires": domain.attrib.get("Expires"),
|
|
"is_expired": domain.attrib.get("IsExpired") == "true",
|
|
"is_locked": domain.attrib.get("IsLocked") == "true",
|
|
"auto_renew": domain.attrib.get("AutoRenew") == "true",
|
|
"whois_guard": domain.attrib.get("WhoisGuard"),
|
|
})
|
|
|
|
return domains
|
|
|
|
def domains_get_info(self, domain: str) -> dict:
|
|
"""Get detailed info about a domain"""
|
|
root = self._request("namecheap.domains.getInfo", DomainName=domain)
|
|
|
|
info_elem = root.find(".//nc:DomainGetInfoResult", NS)
|
|
if info_elem is None:
|
|
return {}
|
|
|
|
return {
|
|
"domain": info_elem.attrib.get("DomainName"),
|
|
"owner": info_elem.attrib.get("OwnerName"),
|
|
"status": info_elem.attrib.get("Status"),
|
|
"is_premium": info_elem.attrib.get("IsPremiumName") == "true",
|
|
}
|
|
|
|
def domains_create(
|
|
self,
|
|
domain: str,
|
|
registrant: RegistrantInfo,
|
|
years: int = 1,
|
|
add_whois_guard: bool = True,
|
|
) -> dict:
|
|
"""
|
|
Register a new domain.
|
|
|
|
Args:
|
|
domain: Domain name to register (e.g., "example.com")
|
|
registrant: Registrant contact information
|
|
years: Number of years to register (1-10)
|
|
add_whois_guard: Enable WhoisGuard privacy protection
|
|
|
|
Returns:
|
|
dict with domain, registered, charged_amount, etc.
|
|
"""
|
|
params = {
|
|
"DomainName": domain,
|
|
"Years": str(years),
|
|
"AddFreeWhoisguard": "yes" if add_whois_guard else "no",
|
|
"WGEnabled": "yes" if add_whois_guard else "no",
|
|
}
|
|
|
|
# Contact info for all 4 contact types (Registrant, Tech, Admin, AuxBilling)
|
|
for prefix in ["Registrant", "Tech", "Admin", "AuxBilling"]:
|
|
params[f"{prefix}FirstName"] = registrant.first_name
|
|
params[f"{prefix}LastName"] = registrant.last_name
|
|
params[f"{prefix}Address1"] = registrant.address1
|
|
params[f"{prefix}City"] = registrant.city
|
|
params[f"{prefix}StateProvince"] = registrant.state_province
|
|
params[f"{prefix}PostalCode"] = registrant.postal_code
|
|
params[f"{prefix}Country"] = registrant.country
|
|
params[f"{prefix}Phone"] = registrant.phone
|
|
params[f"{prefix}EmailAddress"] = registrant.email
|
|
if registrant.organization:
|
|
params[f"{prefix}OrganizationName"] = registrant.organization
|
|
if registrant.address2:
|
|
params[f"{prefix}Address2"] = registrant.address2
|
|
|
|
root = self._request("namecheap.domains.create", **params)
|
|
result = root.find(".//nc:DomainCreateResult", NS)
|
|
|
|
if result is None:
|
|
return {}
|
|
|
|
return {
|
|
"domain": result.attrib.get("Domain"),
|
|
"registered": result.attrib.get("Registered") == "true",
|
|
"charged_amount": float(result.attrib.get("ChargedAmount", 0)),
|
|
"domain_id": result.attrib.get("DomainID"),
|
|
"order_id": result.attrib.get("OrderID"),
|
|
"transaction_id": result.attrib.get("TransactionID"),
|
|
"whois_guard_enabled": result.attrib.get("WhoisguardEnable") == "true",
|
|
}
|
|
|
|
def domains_renew(self, domain: str, years: int = 1) -> dict:
|
|
"""
|
|
Renew a domain.
|
|
|
|
Args:
|
|
domain: Domain name to renew (e.g., "example.com")
|
|
years: Number of years to renew (1-10)
|
|
|
|
Returns:
|
|
dict with domain, renewed, charged_amount, expiration_date, etc.
|
|
"""
|
|
root = self._request(
|
|
"namecheap.domains.renew",
|
|
DomainName=domain,
|
|
Years=str(years),
|
|
)
|
|
|
|
result = root.find(".//nc:DomainRenewResult", NS)
|
|
if result is None:
|
|
return {}
|
|
|
|
return {
|
|
"domain": result.attrib.get("DomainName"),
|
|
"domain_id": result.attrib.get("DomainID"),
|
|
"renewed": result.attrib.get("Renew") == "true",
|
|
"charged_amount": float(result.attrib.get("ChargedAmount", 0)),
|
|
"order_id": result.attrib.get("OrderID"),
|
|
"transaction_id": result.attrib.get("TransactionID"),
|
|
"expiration_date": result.attrib.get("DomainDetails", {}).get("ExpiredDate") if isinstance(result.attrib.get("DomainDetails"), dict) else None,
|
|
}
|
|
|
|
def _parse_contact(self, contact_elem) -> dict:
|
|
"""Parse contact element to dict"""
|
|
if contact_elem is None:
|
|
return {}
|
|
|
|
fields = [
|
|
"OrganizationName", "FirstName", "LastName", "Address1", "Address2",
|
|
"City", "StateProvince", "PostalCode", "Country", "Phone", "EmailAddress"
|
|
]
|
|
result = {}
|
|
for field in fields:
|
|
elem = contact_elem.find(f"nc:{field}", NS)
|
|
key = field[0].lower() + field[1:] # camelCase to snake_case style
|
|
result[key] = elem.text if elem is not None and elem.text else ""
|
|
|
|
return result
|
|
|
|
def domains_get_contacts(self, domain: str) -> dict:
|
|
"""
|
|
Get domain contact information.
|
|
|
|
Args:
|
|
domain: Domain name (e.g., "example.com")
|
|
|
|
Returns:
|
|
dict with registrant, tech, admin, aux_billing contacts
|
|
"""
|
|
root = self._request("namecheap.domains.getContacts", DomainName=domain)
|
|
|
|
result = root.find(".//nc:DomainContactsResult", NS)
|
|
if result is None:
|
|
return {}
|
|
|
|
return {
|
|
"domain": result.attrib.get("Domain"),
|
|
"registrant": self._parse_contact(result.find("nc:Registrant", NS)),
|
|
"tech": self._parse_contact(result.find("nc:Tech", NS)),
|
|
"admin": self._parse_contact(result.find("nc:Admin", NS)),
|
|
"aux_billing": self._parse_contact(result.find("nc:AuxBilling", NS)),
|
|
}
|
|
|
|
def domains_set_contacts(self, domain: str, registrant: RegistrantInfo) -> bool:
|
|
"""
|
|
Set domain contact information.
|
|
|
|
Args:
|
|
domain: Domain name (e.g., "example.com")
|
|
registrant: Contact information (applied to all contact types)
|
|
|
|
Returns:
|
|
bool indicating success
|
|
"""
|
|
params = {"DomainName": domain}
|
|
|
|
for prefix in ["Registrant", "Tech", "Admin", "AuxBilling"]:
|
|
params[f"{prefix}FirstName"] = registrant.first_name
|
|
params[f"{prefix}LastName"] = registrant.last_name
|
|
params[f"{prefix}Address1"] = registrant.address1
|
|
params[f"{prefix}City"] = registrant.city
|
|
params[f"{prefix}StateProvince"] = registrant.state_province
|
|
params[f"{prefix}PostalCode"] = registrant.postal_code
|
|
params[f"{prefix}Country"] = registrant.country
|
|
params[f"{prefix}Phone"] = registrant.phone
|
|
params[f"{prefix}EmailAddress"] = registrant.email
|
|
if registrant.organization:
|
|
params[f"{prefix}OrganizationName"] = registrant.organization
|
|
if registrant.address2:
|
|
params[f"{prefix}Address2"] = registrant.address2
|
|
|
|
root = self._request("namecheap.domains.setContacts", **params)
|
|
result = root.find(".//nc:DomainSetContactResult", NS)
|
|
|
|
return result is not None and result.attrib.get("IsSuccess") == "true"
|
|
|
|
# ===== DNS =====
|
|
|
|
def dns_get_list(self, sld: str, tld: str) -> dict:
|
|
"""Get nameserver information for a domain"""
|
|
root = self._request("namecheap.domains.dns.getList", SLD=sld, TLD=tld)
|
|
|
|
result = root.find(".//nc:DomainDNSGetListResult", NS)
|
|
if result is None:
|
|
return {}
|
|
|
|
nameservers = []
|
|
for ns in result.findall("nc:Nameserver", NS):
|
|
nameservers.append(ns.text)
|
|
|
|
return {
|
|
"domain": result.attrib.get("Domain"),
|
|
"is_using_our_dns": result.attrib.get("IsUsingOurDNS") == "true",
|
|
"nameservers": nameservers,
|
|
}
|
|
|
|
def dns_get_hosts(self, sld: str, tld: str) -> list[dict]:
|
|
"""Get DNS host records"""
|
|
root = self._request("namecheap.domains.dns.getHosts", SLD=sld, TLD=tld)
|
|
|
|
records = []
|
|
for host in root.findall(".//nc:host", NS):
|
|
records.append({
|
|
"host_id": host.attrib.get("HostId"),
|
|
"name": host.attrib.get("Name"),
|
|
"type": host.attrib.get("Type"),
|
|
"address": host.attrib.get("Address"),
|
|
"mx_pref": host.attrib.get("MXPref"),
|
|
"ttl": host.attrib.get("TTL"),
|
|
})
|
|
|
|
return records
|
|
|
|
def dns_set_hosts(self, sld: str, tld: str, records: list[dict]) -> bool:
|
|
"""
|
|
Set DNS host records.
|
|
|
|
Each record should have: name, type, address, ttl (optional), mx_pref (optional)
|
|
"""
|
|
params = {"SLD": sld, "TLD": tld}
|
|
|
|
for i, record in enumerate(records, 1):
|
|
params[f"HostName{i}"] = record["name"]
|
|
params[f"RecordType{i}"] = record["type"]
|
|
params[f"Address{i}"] = record["address"]
|
|
params[f"TTL{i}"] = record.get("ttl", "1800")
|
|
if record["type"] == "MX":
|
|
params[f"MXPref{i}"] = record.get("mx_pref", "10")
|
|
|
|
root = self._request("namecheap.domains.dns.setHosts", **params)
|
|
result = root.find(".//nc:DomainDNSSetHostsResult", NS)
|
|
|
|
return result is not None and result.attrib.get("IsSuccess") == "true"
|
|
|
|
def dns_set_custom(self, sld: str, tld: str, nameservers: list[str]) -> bool:
|
|
"""Set custom nameservers"""
|
|
ns_string = ",".join(nameservers)
|
|
root = self._request(
|
|
"namecheap.domains.dns.setCustom",
|
|
SLD=sld,
|
|
TLD=tld,
|
|
Nameservers=ns_string
|
|
)
|
|
|
|
result = root.find(".//nc:DomainDNSSetCustomResult", NS)
|
|
return result is not None and result.attrib.get("Updated") == "true"
|
|
|
|
def dns_set_default(self, sld: str, tld: str) -> bool:
|
|
"""Set default Namecheap nameservers"""
|
|
root = self._request("namecheap.domains.dns.setDefault", SLD=sld, TLD=tld)
|
|
|
|
result = root.find(".//nc:DomainDNSSetDefaultResult", NS)
|
|
return result is not None and result.attrib.get("Updated") == "true"
|
|
|
|
# ===== Nameservers (Glue Records) =====
|
|
|
|
def ns_create(self, sld: str, tld: str, nameserver: str, ip: str) -> dict:
|
|
"""
|
|
Create a child nameserver (glue record).
|
|
|
|
Args:
|
|
sld: Second-level domain (e.g., "example" for example.com)
|
|
tld: Top-level domain (e.g., "com")
|
|
nameserver: Nameserver hostname (e.g., "ns1.example.com")
|
|
ip: IP address for the nameserver
|
|
|
|
Returns:
|
|
dict with domain, nameserver, ip, success
|
|
"""
|
|
root = self._request(
|
|
"namecheap.domains.ns.create",
|
|
SLD=sld,
|
|
TLD=tld,
|
|
Nameserver=nameserver,
|
|
IP=ip,
|
|
)
|
|
|
|
result = root.find(".//nc:DomainNSCreateResult", NS)
|
|
if result is None:
|
|
return {}
|
|
|
|
return {
|
|
"domain": result.attrib.get("Domain"),
|
|
"nameserver": result.attrib.get("Nameserver"),
|
|
"ip": result.attrib.get("IP"),
|
|
"success": result.attrib.get("IsSuccess") == "true",
|
|
}
|
|
|
|
def ns_delete(self, sld: str, tld: str, nameserver: str) -> dict:
|
|
"""
|
|
Delete a child nameserver (glue record).
|
|
|
|
Args:
|
|
sld: Second-level domain (e.g., "example" for example.com)
|
|
tld: Top-level domain (e.g., "com")
|
|
nameserver: Nameserver hostname to delete (e.g., "ns1.example.com")
|
|
|
|
Returns:
|
|
dict with domain, nameserver, success
|
|
"""
|
|
root = self._request(
|
|
"namecheap.domains.ns.delete",
|
|
SLD=sld,
|
|
TLD=tld,
|
|
Nameserver=nameserver,
|
|
)
|
|
|
|
result = root.find(".//nc:DomainNSDeleteResult", NS)
|
|
if result is None:
|
|
return {}
|
|
|
|
return {
|
|
"domain": result.attrib.get("Domain"),
|
|
"nameserver": result.attrib.get("Nameserver"),
|
|
"success": result.attrib.get("IsSuccess") == "true",
|
|
}
|
|
|
|
def ns_get_info(self, sld: str, tld: str, nameserver: str) -> dict:
|
|
"""
|
|
Get info about a child nameserver (glue record).
|
|
|
|
Args:
|
|
sld: Second-level domain (e.g., "example" for example.com)
|
|
tld: Top-level domain (e.g., "com")
|
|
nameserver: Nameserver hostname (e.g., "ns1.example.com")
|
|
|
|
Returns:
|
|
dict with domain, nameserver, ip, statuses
|
|
"""
|
|
root = self._request(
|
|
"namecheap.domains.ns.getInfo",
|
|
SLD=sld,
|
|
TLD=tld,
|
|
Nameserver=nameserver,
|
|
)
|
|
|
|
result = root.find(".//nc:DomainNSInfoResult", NS)
|
|
if result is None:
|
|
return {}
|
|
|
|
statuses = []
|
|
for status in result.findall("nc:NameserverStatuses/nc:Status", NS):
|
|
if status.text:
|
|
statuses.append(status.text)
|
|
|
|
return {
|
|
"domain": result.attrib.get("Domain"),
|
|
"nameserver": result.attrib.get("Nameserver"),
|
|
"ip": result.attrib.get("IP"),
|
|
"statuses": statuses,
|
|
}
|
|
|
|
def ns_update(self, sld: str, tld: str, nameserver: str, old_ip: str, ip: str) -> dict:
|
|
"""
|
|
Update a child nameserver (glue record) IP address.
|
|
|
|
Args:
|
|
sld: Second-level domain (e.g., "example" for example.com)
|
|
tld: Top-level domain (e.g., "com")
|
|
nameserver: Nameserver hostname (e.g., "ns1.example.com")
|
|
old_ip: Current IP address
|
|
ip: New IP address
|
|
|
|
Returns:
|
|
dict with domain, nameserver, ip, success
|
|
"""
|
|
root = self._request(
|
|
"namecheap.domains.ns.update",
|
|
SLD=sld,
|
|
TLD=tld,
|
|
Nameserver=nameserver,
|
|
OldIP=old_ip,
|
|
IP=ip,
|
|
)
|
|
|
|
result = root.find(".//nc:DomainNSUpdateResult", NS)
|
|
if result is None:
|
|
return {}
|
|
|
|
return {
|
|
"domain": result.attrib.get("Domain"),
|
|
"nameserver": result.attrib.get("Nameserver"),
|
|
"ip": result.attrib.get("IP"),
|
|
"success": result.attrib.get("IsSuccess") == "true",
|
|
}
|
|
|
|
# ===== Account =====
|
|
|
|
def users_get_balances(self) -> dict:
|
|
"""Get account balance"""
|
|
root = self._request("namecheap.users.getBalances")
|
|
|
|
balance = root.find(".//nc:UserGetBalancesResult", NS)
|
|
if balance is None:
|
|
return {}
|
|
|
|
return {
|
|
"currency": balance.attrib.get("Currency"),
|
|
"available_balance": float(balance.attrib.get("AvailableBalance", 0)),
|
|
"account_balance": float(balance.attrib.get("AccountBalance", 0)),
|
|
"earned_amount": float(balance.attrib.get("EarnedAmount", 0)),
|
|
}
|
|
|
|
def users_get_pricing(self, product_type: str, product_category: str = "") -> list[dict]:
|
|
"""Get pricing for products"""
|
|
params = {"ProductType": product_type}
|
|
if product_category:
|
|
params["ProductCategory"] = product_category
|
|
|
|
root = self._request("namecheap.users.getPricing", **params)
|
|
|
|
products = []
|
|
for product in root.findall(".//nc:Product", NS):
|
|
for price in product.findall(".//nc:Price", NS):
|
|
products.append({
|
|
"name": product.attrib.get("Name"),
|
|
"duration": price.attrib.get("Duration"),
|
|
"duration_type": price.attrib.get("DurationType"),
|
|
"price": float(price.attrib.get("Price", 0)),
|
|
"currency": price.attrib.get("Currency"),
|
|
})
|
|
|
|
return products
|