Domains

Buy a domain in your user's name, connect one they own or transfer it in, and point it at an app or an inbox.

A domain gives an app or an inbox an address of its own. Buy one through NoClick, registered in the person's name and charged to your account like anything else, connect one they already own elsewhere, or transfer it in from its registrar. Then point it at what it should serve.

NoClick bills only your account, never your users: a domain you buy for a user is charged to you, and what you charge them for it is up to you (charging your users).

Find a name

import noclick

for offer in noclick.Domain.search("acmebooking", tlds=["com", "ai"]):
    print(offer.name, offer.available, offer.price_usd, offer.min_years, offer.renewal_usd, offer.premium)

search takes a word or a whole name (acmebooking.com) and returns up to limit offers (10 by default, at most 50): the exact names first, then close alternatives. A premium name is one the registry prices above its usual rate, often far above. Names that imitate well-known brands or banks aren't offered.

Each TLD sets the terms a name can be registered for: min_years is the shortest and max_years the longest. Most start at a year; .ai registers for two years at least, and .co for five at most. price_usd is what buying the name charges now, for min_years, and renewal_usd what each renewal of min_years costs: each is the registrar's own price.

Trademark claims

During a TLD's trademark-claims period, a name that matches a registered trademark carries the Trademark Clearinghouse's notice: claims_notice holds it, in Markdown, and it's None for every other name. The registrant must be shown it before buying the name. Once they have, and still want it, buy with accept_claims=True:

[offer] = noclick.Domain.search("acmebooking.ai", limit=1)
if offer.claims_notice:
    print(offer.claims_notice)   # show it to the registrant, and ask whether they still want the name

A name with a notice bought without accept_claims=True raises noclick.ActionRequired whose details carry claims_notice, and nothing is bought.

Buy a domain

[offer] = noclick.Domain.search("acmebooking", tlds=["com"], limit=1)
owner = noclick.DomainContact(
    name="Dana Reyes", email="dana@acmebooking.test", phone="+14155550188",
    address="1 Ferry Building", city="San Francisco", region="CA", postal_code="94111", country="US",
    agreement_accepted_at=accepted_at,   # when Dana accepted the registration agreement
)
purchase = noclick.Domain.buy(offer, max_price_usd=15, registrant=owner)
domain = purchase.wait()
print(domain.name, domain.expires_at)

A domain is registered to a person, the registrant: you, or inside as_user the user it's bought for. ICANN requires their real details, and the registry a region (the state, province or region of the address). Domains are registered through NoClick's registrar partner, which the public registration record names; only the registrant's details are private.

years is how long it's registered for, between the offer's min_years and max_years. Leave it out to buy the TLD's minimum (two years for .ai); a term the TLD doesn't offer raises noclick.InvalidRequest naming years, and its message lists the terms it does.

A user's domain is registered to that person and charged to you, so buying one inside as_user needs their details as registrant=; without them it raises noclick.InvalidRequest naming registrant, and nothing is sent:

ana = noclick.DomainContact(
    name="Ana Silva", email="ana@example.com", phone="+14155550123",
    address="500 Market St", city="San Francisco", region="CA", postal_code="94105", country="US",
    agreement_accepted_at=accepted_at,   # when Ana accepted the registration agreement
)
with noclick.as_user("user_42"):
    domain = noclick.Domain.buy("anasilva.dev", max_price_usd=20, registrant=ana).wait()

max_price_usd is your ceiling on what the purchase charges now. Above it, nothing is bought and it raises noclick.PriceExceeded. The purchase is charged to your account, like any other spend: NoClick checks your account can pay before registering, and that its balance holds at least $5 on any plan (noclick.PaymentRequired otherwise), and charges it when the registry confirms the registration. A registration that fails is never charged.

buy returns an operation, a noclick.DomainPurchase, that moves through registering and pending_dns while its DNS is set up, and wait() gives the noclick.Domain once it's active. If registering fails (someone took the name a moment earlier), the operation fails and nothing is charged.

When your policy holds purchases for a person to approve (a require_approval rule for v1.domains_create; transfers in and renewals are v1.domains_transfer_in and v1.domains_renew), the purchase waits in awaiting_human until the approver answers. Nobody answering within 24 hours (the approval's life) expires it, and nothing is charged. Once approved, the price is checked against max_price_usd again as it runs. The operation outlives your process: noclick.DomainPurchase.from_id(op_id) picks it up anywhere.

A bought domain works at the bare name and at www, and its DNS is hosted by NoClick, so pointing it at something needs nothing more from you. NoClick signs its DNS (DNSSEC) wherever its registry takes keys. If the registry refuses a registration after first accepting it, the domain isn't yours and what it was charged is given back.

The registration agreement

Every registrant accepts NoClick's domain registration agreement before a domain is registered to them. noclick.Domain.agreement() gives its page: the agreement itself (it names the sponsoring registrar, as ICANN requires), the fees for registering, renewing, renewing after expiry and restoring, and ICANN's registrant rights and responsibilities. Show it to the person, and record when they accept it as agreement_accepted_at on their DomainContact:

from datetime import datetime, timezone

agreement = noclick.Domain.agreement()
print(agreement.url, agreement.version)   # show this page, with a step where they accept it

accepted_at = datetime.now(timezone.utc)   # recorded when they accept, and kept with their details

A registration needs it: a registrant, or your account's default registrant, without agreement_accepted_at raises noclick.InvalidRequest naming registrant.agreement_accepted_at. An agent connected through MCP never sends agreement_accepted_at or accept_claims: it fills in the registrant's details, and the person accepts the agreement (and any trademark notice) on NoClick's approval page, which records when. Your terms make you a reseller of NoClick's registration service to your users, which is why they see the agreement page: it carries the fees and ICANN's registrant rights.

organization on a DomainContact makes that organization the domain's legal owner (ICANN's "Registered Name Holder"). Never put your company on a user's domain unless your company means to own it.

Own a domain from your code

from_name with create_if_missing=True makes owning a domain idempotent, as with every other resource: the first run buys it (max_price_usd is required, and years is the TLD's minimum unless you pass it), and every run after finds it. An auto_renew in your code is kept in step. A name someone else holds raises noclick.NotAvailable.

domain = noclick.Domain.from_name("acmebooking.com", create_if_missing=True, max_price_usd=15)
print(domain.status)   # registering, then active

Your account's own domains need a registrant too, and your code shouldn't carry one. Set your account's default registrant once (with its agreement_accepted_at, like any registrant), and a domain bought for the account itself (outside as_user) without registrant= is registered to it:

noclick.Domain.set_default_registrant(noclick.DomainContact(
    name="Dana Reyes", email="dana@acmebooking.test", phone="+14155550188", organization="Acme Booking",
    address="1 Ferry Building", city="San Francisco", region="CA", postal_code="94111", country="US",
    agreement_accepted_at=accepted_at,
))
print(noclick.Domain.default_registrant())

set_default_registrant(None) clears it. Until one is set, buying a domain for your account without registrant= raises noclick.ActionRequired, and nothing is bought; its next_step links to the console page where the default registrant is set:

try:
    domain = noclick.Domain.from_name("acmebooking.com", create_if_missing=True, max_price_usd=15).hydrate()
except noclick.ActionRequired as err:
    print(err.message, err.next_step)

A user's domain is never registered to your account's contact: inside as_user, buy and from_name(..., create_if_missing=True) need registrant=, the user's own details.

Confirm the registrant's email

ICANN requires every new registrant to confirm their email address. After registering, the registrant is emailed a link to confirm it at registrant_email, sent in your brand on ICANN's behalf, and until they click it the domain shows registrant_unverified:

domain = noclick.Domain.from_name("anasilva.dev")
if domain.status == "registrant_unverified":
    print(f"Ask them to confirm the email sent to {domain.registrant_email} by {domain.registrant_verify_by}")

The domain keeps working meanwhile, and NoClick reminds them. Unconfirmed by registrant_verify_by (15 days), it stops resolving until they confirm. A person confirms once: their other domains don't ask again.

Keep the registrant's details current

ICANN requires accurate details, so update them when the person's change. domain.registrant is the contact on file, and update(registrant=...) replaces it:

import dataclasses

domain = noclick.Domain.from_name("anasilva.dev")
domain.update(registrant=dataclasses.replace(domain.registrant, email="ana@newmail.example"))
print(domain.status)   # registrant_unverified until Ana confirms the new address

Changing the registrant's name, organization or email asks the new registrant to confirm their email again (the domain is registrant_unverified meanwhile) and may start the registry's transfer lock (transfer_locked_until).

Connect a domain someone already owns

domain = noclick.Domain.connect("acme.test")
if domain.setup_url:
    print("Set it up in one click:", domain.setup_url)
for record in domain.records:
    print(record.type, record.name, record.value, record.status)

A connected domain stays with its registrar and its DNS host; the person adds the records in records there. Where that DNS host supports the open Domain Connect standard, setup_url does it in one click: they sign in there and approve, on the host's consent page, which names NoClick alongside your brand. A link sets up one name at a time (the domain's own, then its wildcard, then what it serves); once its records are seen, setup_url is the link for the next, and None when nothing is left to add. Until the records are seen the domain is pending_dns, and each record's status is pending, then verified. NoClick checks every few minutes; verify() checks now:

domain = noclick.Domain.from_name("acme.test")
domain.verify()
print(domain.status)   # active once every required record is verified

Connect a subdomain (book.acme.com) and it needs one CNAME. Connect a bare domain (acme.com) and www is its primary address. The bare name then needs the DNS host's CNAME flattening (an ALIAS or ANAME record), or a forward from acme.com to www.acme.com, which is why that record's required is False. Connecting the same name again returns the same domain; a domain another account has connected raises noclick.Conflict.

Every subdomain

wildcard=True adds the *.<name> records to records as well, so once they're in place every subdomain can serve an app or send an app's email without a record of its own. Each subdomain goes live once its certificate is issued, usually within minutes of its first use. It's how each of your users' apps gets an address under yours (a subdomain for each user's app):

domain = noclick.Domain.connect("apps.acme.test", wildcard=True)
for record in domain.records:
    print(record.type, record.name, record.value)   # includes *.apps.acme.test

A domain you bought through NoClick needs no wildcard: NoClick writes each subdomain's record as it's used, and the subdomain is live as soon as its certificate is issued, usually within a few minutes.

Transfer a domain in

A domain registered elsewhere can move to NoClick, so it renews on your account with the rest. Ask what moving it costs first; nothing is charged:

quote = noclick.Domain.transfer_quote("acme.test")
print(quote.eligible, quote.price_usd, quote.years, quote.renewal_usd)
if not quote.eligible:
    print(quote.reason, quote.locked_until)

A domain can move unless it's locked at its registrar (unlock it there), it's inside the registry's transfer lock after a domain is registered, transferred in or its registrant changes (locked_until), its TLD isn't one NoClick registers, or it's already with NoClick's registrar under another account (moving a domain between accounts isn't supported yet). reason says which. NoClick works this out from the domain's public registration data (its registrar lock, its transfer lock, whether NoClick registers the TLD) and the registrar's price, so the quote is a best answer until transfer_in is accepted.

Its current registrar gives an auth code (also called an EPP code) for the move. Pass it with a ceiling:

transfer = noclick.Domain.transfer_in("acme.test", auth_code="Xy7#pQ2!", max_price_usd=15)
domain = transfer.wait()
print(domain.source, domain.expires_at)   # transferred, quote.years later than before

transfer_in is charged like a purchase: the quote's price_usd, which adds years to the registration (the TLD's minimum, one year for most), charged to your account once the transfer completes, refused with noclick.PriceExceeded above max_price_usd, and held in awaiting_human while your policy holds purchases. It's registered to a person on the same rule as buy, with when they accepted the registration agreement: inside as_user it needs the user's details as registrant=, and your account's own domain uses the default registrant.

It returns an operation, a noclick.DomainTransfer, and the domain is transferring_in until the registry and the current registrar have moved it, up to about 5 days. The current owner may get an email from their registrar asking them to approve the move; approving it there makes it quicker. If the registry refuses, the transfer fails with its reason and nothing is charged: a wrong auth code is noclick.InvalidRequest naming auth_code, raised by wait() once the registry says so.

The domain's DNS keeps working throughout: its nameservers don't change. Once it's active, its DNS can stay where it's hosted, and the domain works like a connected one there (records lists what to add), or move to NoClick:

domain = noclick.Domain.from_name("acme.test")
for record in domain.use_noclick_dns():
    print("Copied", record.type, record.name, record.value)

use_noclick_dns copies the records NoClick can read where the DNS is hosted now, then changes the nameservers, and returns what it copied for review. A domain whose current DNS host signs it (DNSSEC) moves within two days instead: its old key is withdrawn first, so no resolver still trusting that key ever sees the domain unsigned. Add any it missed with domain.dns.set(...). noclick_dns says where a domain's DNS is.

A domain you connected can be transferred in too, and it keeps its id and what it serves. Pass the Domain and the transfer acts for that domain's own user (or your account, for an account domain), whatever as_user says; pass its name and it acts for the current user, as other calls do. Once it has moved, the registry's transfer lock holds it at NoClick until transfer_locked_until, as after a purchase.

Point it at an app or an inbox

app = noclick.Project.from_name("booking")
app.add_domain("acmebooking.com")

inbox = noclick.Inbox.create(local_part="help", domain="mail.acme.test")
print(inbox.address)   # help@mail.acme.test

Serving something adds the records it needs: an app needs its web records, an inbox its mail records (MX, and the TXT and CNAME that authenticate its mail). On a bought domain NoClick adds them at once. On a connected one they appear in records and the domain is pending_dns again until they're added. domain.serves lists what it points at, one entry per name of it, each as {kind, id, name, hostname}: project, inbox, or brand for the one that serves your brand's pages, and hostname the name that serves it (an app on a bought domain is served at its name and at www).

An inbox needs the domain's mail records, and a connected domain's existing mail must keep working, so an inbox on a connected domain goes on a subdomain of it, like help@mail.acme.com. Its records, NS records that hand that subdomain to NoClick, appear in the domain's records; an inbox on the bare connected domain raises noclick.Conflict. On a bought domain any address works. Role names such as help, support and sales are yours to use; see Inboxes.

Manage the DNS of a bought domain

domain = noclick.Domain.from_name("acmebooking.com")
for record in domain.dns.records():
    print(record.name, record.type, record.value, record.managed)

domain.dns.set("TXT", "@", "google-site-verification=3pL0x0kA1b2C")

dns.set adds a record, or keeps it if the same one is there. A CNAME replaces the name's existing CNAME, since a name holds at most one; other types add alongside what's there. dns.delete(record) removes one you read from records(). Records NoClick keeps for what the domain serves are managed: changing or deleting one raises noclick.Conflict, so change what the domain serves instead. On a domain whose DNS is hosted elsewhere (a connected one, or a transferred one before use_noclick_dns()), these raise noclick.NotAvailable.

Renew

A bought domain renews itself 30 days before expires_at for renewal_years (the TLD's minimum), charging renewal_usd to your account, whoever it's registered to. If your account can't pay, NoClick retries and emails you, and the domain shows expiring until the renewal is paid. If the renewal fails on NoClick's side, nothing is charged, NoClick retries every day, and the domain shows expiring until it renews. To extend it now instead:

domain = noclick.Domain.from_name("acmebooking.com")
renewal = domain.renew(years=2, max_price_usd=30)
print(renewal.wait().expires_at)

renew works like buy: years is the TLD's minimum when left out, there's a ceiling, it's charged to your account when the renewal lands, and it may wait in awaiting_human while your policy holds it. Turn auto-renew off and the domain lapses at expires_at:

noclick.Domain.from_name("acmebooking.com").update(auto_renew=False)

NoClick never deletes a domain, but an expired one stops resolving. renew() brings it back until renewable_until, the end of the guaranteed renewal window; after that it can't be renewed through the API and renew() raises noclick.NotAvailable. Restoring a domain the registry is about to delete isn't an API call either: NoClick support may be able to restore it, at the registry's restore fee.

Transfer a domain out

domain = noclick.Domain.from_name("acmebooking.com")
code = domain.transfer_out()
print(code.auth_code)

transfer_out unlocks the domain and gives the auth code the new registrar asks for. Where the registry limits how long a code works, code.expires_at says until when; call transfer_out() again for a fresh one. The domain shows transferring_out until the move completes, then leaves your list. The registry's transfer lock after a domain is registered, transferred in or its registrant changes holds it until transfer_locked_until, and before then transfer_out raises noclick.NotAvailable.

Disconnect a connected domain

noclick.Domain.from_name("acme.test").delete()

The domain stops serving your apps; its DNS stays as it is where it's hosted. It raises noclick.Conflict while an inbox, an app or your brand's pages still use the domain. A bought domain can't be deleted: turn off auto_renew to let it lapse, or transfer it out.

Statuses

statusMeaning
awaiting_humanBought, waiting for your policy's approver; nothing is charged unless they approve and it registers
registeringThe registry is registering it; it's charged to your account once the registry confirms
transferring_inThe registry and its current registrar are moving it to NoClick, up to about 5 days; it's charged once the transfer completes
pending_dnsWaiting for its records: being set up (bought), or waiting for them to be added (connected)
activeServing
registrant_unverifiedServing, but the registrant hasn't confirmed their email yet; unconfirmed by registrant_verify_by, it stops resolving
expiringWithin 30 days of expires_at and not renewed yet: auto-renew is off, your account couldn't pay for the renewal, or it failed on NoClick's side and is retried daily
expiredPast expires_at and no longer resolving; renew() brings it back until renewable_until
transferring_outUnlocked and moving to another registrar
failedThe purchase or transfer failed, and nothing was charged (a connected domain whose transfer fails stays connected)

Limits and billing

An account and each user can buy a limited number of domains a day (noclick.QuotaExceeded). A purchase above $2,500 may wait while NoClick arranges it. A domain is charged to your account in dollars when the registry confirms its registration or transfer and each time it renews, at the prices search or transfer_quote showed, and it shows as Domains on the Usage page. Connecting a domain is free.

  • noclick.PriceExceeded: the purchase, transfer or renewal costs more than max_price_usd.
  • noclick.PaymentRequired: your account can't pay for the purchase, transfer or renewal.
  • noclick.UpstreamError: NoClick couldn't place the purchase, transfer or renewal on its side right now. Nothing was charged and nothing changed; try again later (NoClick has been alerted).
  • noclick.NotAvailable: the name is taken or not offered, a transfer either way is inside the registry's transfer lock (or a transfer in the registry won't take, with its reason), a renewal after renewable_until, a DNS change on a domain whose DNS is hosted elsewhere, or a name connected (or served) that can never be served, such as one under example.com, with the reason and its hostname (nothing is kept waiting for it).
  • noclick.Conflict: a domain another account has connected, a change to a managed record, an inbox on a bare connected domain (or on a bought one that takes mail elsewhere), or deleting a bought domain or one an inbox, an app or your brand uses.
  • noclick.InvalidRequest: a user's domain bought or transferred in without registrant=, a registrant without agreement_accepted_at or region, a years the TLD doesn't offer, registrant details the registry refuses, such as a country that isn't a two-letter code, or an auth code the registry refuses.
  • noclick.ActionRequired: a purchase for your account with no registrant= and no default registrant (next_step links to where it's set), or a name with a claims_notice bought without accept_claims=True (its details carry the notice).

The Python reference has every argument of search, agreement, buy, connect, transfer_quote, transfer_in, use_noclick_dns, verify, update, renew and transfer_out, and DomainDns for records.