Skip to content

jkindrix/ncheap

ncheap

A command-line tool for the Namecheap registrar API, built for terminal use and AI-agent operability: structured --json output, meaningful exit codes, and non-interactive operation by default.

Status: early development (0.x). Full read-only surface plus gated mutating commands (DNS, privacy, register/renew) are implemented.

Install

cargo install ncheap                  # from crates.io
cargo build --release                 # or from source: target/release/ncheap

Fleets should pin a version rather than float on latest: each GitHub release ships per-target tarballs with sha256 checksums, a version-pinned installer (https://github.com/jkindrix/ncheap/releases/download/vX.Y.Z/ncheap-installer.sh), and build-provenance attestations (verifiable with gh attestation verify). The releases/latest installer URL floats and is not recommended for automation.

Configuration

Credentials live in ~/.config/ncheap/config.toml on Linux, or ~/Library/Application Support/ncheap/config.toml on macOS (must be chmod 600; ncheap refuses group/other-readable config files):

default_profile = "production"

[profile.production]
api_user = "your-namecheap-username"
api_key = "your-api-key"
client_ip = "203.0.113.10"   # your whitelisted outbound IPv4

[profile.sandbox]
api_user = "your-sandbox-username"
api_key = "your-sandbox-api-key"
client_ip = "203.0.113.10"
sandbox = true

username defaults to api_user. Environment variables override the config file: NCHEAP_API_USER, NCHEAP_API_KEY, NCHEAP_USERNAME, NCHEAP_CLIENT_IP, NCHEAP_SANDBOX, NCHEAP_PROFILE. Pure-env operation (no config file) is supported.

Namecheap's API requires the calling IP to be whitelisted (IPv4 only) under Profile → Tools → API Access in the Namecheap dashboard. API access has eligibility requirements (at the time of writing: ≥20 domains, or ≥$50 balance, or ≥$50 spent in the last 2 years). The sandbox is a separate account with separate data; its pricing and behavior are not guaranteed to match production.

Usage

ncheap audit                           # every safety check, one report
ncheap domains list                    # all domains, auto-paginated
ncheap domains check example.com ...   # availability (up to 50 per call)
ncheap domains info example.com        # registration, privacy, DNS details
ncheap domains lock example.com        # registrar (transfer) lock status
ncheap domains lock example.com --lock     # mutating; also --unlock
ncheap domains contacts example.com    # contacts; PII redacted unless --full
ncheap dns get example.com             # nameserver mode + host records
ncheap dns add example.com --type A --name www --address 192.0.2.1   # mutating
ncheap dns remove example.com --type A --name www    # mutating
ncheap dns set example.com ns1.host ns2.host   # mutating; see safety model
ncheap dns set-default example.com     # revert to Namecheap DNS (mutating)
ncheap privacy list                    # domain privacy subscriptions
ncheap privacy enable example.com --forward-to you@example.org   # mutating
ncheap privacy disable example.com     # mutating
ncheap account balances                # amounts redacted unless --full
ncheap domains register new.com --max-price 15 --contacts-from owned.com
ncheap domains renew owned.com --max-price 20   # both mutating, price-guarded
ncheap domains contacts target.com --set-from owned.com   # mutating
ncheap transfer create inbound.com --epp-code CODE --max-price 12   # mutating
ncheap transfer status 12345           # poll an inbound transfer
ncheap account pricing --action REGISTER --product com   # cached 24h
ncheap raw domains.getTldList          # direct API call, raw XML out
ncheap raw domains.getInfo --param DomainName=example.com

raw only calls methods on a read-only allowlist (the wrapped Phase 1 methods plus domains.getTldList); mutating methods are refused, and authentication parameters cannot be supplied via --param.

Any command takes --json for the machine-readable envelope. Domains for dns commands may be IDN (normalized to punycode) and are split SLD/TLD via the Public Suffix List, so example.co.uk works; subdomains are rejected with a suggestion rather than silently trimmed.

DNSSEC / DS-record management is not possible via this tool: the Namecheap API does not expose it; use the dashboard.

List commands auto-paginate: accounts with more than 20 domains are fetched completely, not truncated at the API's default page size.

JSON envelope

Every command with --json emits one envelope on stdout:

{
  "ok": true,
  "schema": 3,
  "command": "domains.list",
  "data": [ ... ],
  "error": null,
  "meta": { "profile": "production", "sandbox": false, "api_calls": 1, "version": "0.3.0" }
}

schema identifies the envelope revision and meta.version the producing binary. command is the dotted command name (domains.list, domains.check, domains.lock, domains.info, domains.contacts, domains.register, domains.renew, dns.get, dns.set, privacy.list, privacy.enable, privacy.disable, account.balances, account.pricing, raw) — or the sentinel cli when argument parsing itself failed. All dates in envelope data are ISO-8601 (YYYY-MM-DD) — the API's native MM/DD/YYYY strings sort wrong lexically; raw output remains a verbatim passthrough. The registry_hold field (formerly is_locked) reports the API's IsLocked — a registry/dispute hold, not the registrar transfer lock, which domains lock reports. (Upstream docs do not define this distinction; the interpretation is from observed live divergence between getList.IsLocked and getRegistrarLock on accounts whose domains are transfer-locked yet report IsLocked=false.) On failure ok is false and error carries kind (usage|config|transport|api|parse|rate_limit), code (Namecheap error number, if any), and message; meta is populated whenever a profile had resolved before the failure, so failures are attributable to a profile/sandbox, and is null only for pre-configuration errors.

Exit codes

Code Meaning
0 Success (per-item results such as an unavailable domain are data, not errors)
1 Namecheap API returned an error response, or the response did not parse (error.kind distinguishes api from parse)
2 Usage error (bad arguments)
3 Configuration / credential / policy error
4 Transport / network error
5 Rate-limited. Namecheap documents no rate-limit response; ncheap maps three observed/reported shapes best-effort: HTTP 429 (after one backoff retry), in-band error 500000 (third-party reports), and HTTP 405 with an HTML body (the shape actually captured live, sandbox 2026-06-07)

Envelope compatibility

The envelope's top-level keys (ok/schema/command/data/error/meta), the error.kind values, and the exit-code meanings are stable; schema increments whenever any of them change. New fields or new kind values may be added in minor versions (additive); removing or renaming any of them is a breaking change and bumps the major version. Per-command data shapes follow the same rule. Note: if stdout closes mid-write (e.g. piping to head), ncheap exits 0 like standard tools — consumers should treat truncated JSON as incomplete output, not as a command result. The success-path end-to-end test runs against debug builds (release builds can only reach the two Namecheap hosts, by design).

Releasing

Releases are automated by dist: bump the version in Cargo.toml, update CHANGELOG.md, run cargo update -p psl (the embedded Public Suffix List snapshot is frozen into each binary at build time), commit, then tag vX.Y.Z and push the tag. CI builds the binaries, checksums, and installer. After tagging, run cargo install --path . --locked so the PATH binary tracks the release.

Safety model

Blast radius, stated plainly: the Namecheap API key is account-wide — the API offers no read-only or per-domain sub-keys. Every gate ncheap enforces (read-only allowlist, production-mutation gate, price guards, --yes) is client-side: they reduce the probability of an accident by a well-behaved caller, they do not constrain a compromised or maliciously instructed agent holding an armed profile. Treat any host running ncheap with allow_production_mutations = true as holding full registrar authority over the account. Namecheap's Universal ToS also reserves discretionary suspension for high-volume or abusive automated use — sustained agentic operation is at the account owner's risk.

  • The API key is never written to logs, error messages, or request traces. Requests are sent as POST with a form body, so the key never appears in a URL; the HTTP agent is HTTPS-only and follows no redirects. Note that a key supplied via NCHEAP_API_KEY is visible in /proc/<pid>/environ to same-user processes and may land in shell history; the 0600 config file is the preferred channel on shared or backed-up machines.
  • DNS record edits (dns add/dns remove) ride on setHosts, which is a full-zone replace with no upstream undo or compare-and-swap: ncheap fetches the zone, modifies it, and rewrites it whole, preserving the domain's EmailType (mail routing) and journaling the complete pre-image. Concurrent edits to one zone are last-writer-wins — do not run parallel editors against the same domain. Removals that would empty the zone are refused.
  • Inbound transfers (transfer create) carry the same price and spend guards as purchases. The create path cannot be exercised against the sandbox (it needs a real domain at another registrar), so its live behavior is fixture-verified only; transfer status is a plain read.
  • Every mutation is journaled to an append-only, 0600 JSONL file (note: for contact mutations, the journaled request parameters and pre-image include the contact data itself — the journal lives in the same local trust domain as the 0600 config) (~/.local/state/ncheap/mutations.jsonl): an fsync'd intent record before the request, an outcome record after, and pre-images (previous nameservers / lock state) where the API offers no undo. If the intent cannot be recorded, the mutation is refused.
  • An interrupted mutation (killed process, network drop after send) has an unknown outcome — the charge or change may have committed server-side. Never blind-retry an interrupted register/renew/dns set; consult the mutation journal and reconcile via domains list/domains info/ account balances first.
  • Purchasing commands (domains register, domains renew) additionally require --max-price and refuse pre-flight if the live listed price exceeds it — the pricing cache is never consulted for purchase decisions. Registration contacts are copied from an owned domain (--contacts-from); ncheap stores no contact data. Premium domains are refused. The actual charge can exceed the listed price slightly (ICANN fees); both figures are reported, and charged_exceeded_max_price is set when the charge came in above the cap. Early Access Phase (EAP) domains are refused like premium ones. A rolling-24h budget (max_daily_spend in the profile, config-file-only like the gate) bounds cumulative purchases via a local 0600 ledger; production purchases are refused entirely until a cap is set, so arming the mutation gate never exposes unlimited spend. Sandbox is unlimited when uncapped. The cap check holds a file lock across check-and-reserve, so concurrent purchases on one machine cannot both pass. A purchase that fails after reservation still consumes its budget for 24h (fails in the safe direction). The journal and spend ledger are append-only and never pruned; at sustained agentic volume, rotate them externally.
  • Mutating commands (dns set, privacy enable/disable, domains register/renew) are enforced at the client layer, not per-command: they are refused against production unless the profile sets allow_production_mutations = true in the config file (the environment deliberately cannot arm this), they require --yes non-interactively (or an interactive confirmation), and they never auto-retry — an ambiguous failure after a mutation surfaces instead of double-submitting. Sandbox profiles may always mutate.
  • Client-side throttling spaces requests ~3s apart within one invocation, with backoff on HTTP 429/5xx. Namecheap's FAQ documents 50/min (plus 700/hour and 8000/day) key-wide; older third-party reports say 20/min; ncheap spaces for the conservative figure. Concurrent ncheap processes on one machine coordinate through a lock file in the state directory, so parallel invocations are serialized to the same spacing (fail-open: if the state directory is unavailable, spacing falls back to per-process). Processes on different machines sharing one key still do not coordinate.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.

About

Command-line tool for the Namecheap registrar API — structured JSON output, meaningful exit codes, built for terminal and AI-agent use

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages