""" 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