Domains

Search a name across every ending on offer, register it, renew it, and see it through to the end of its life.

Sandbox only for now

Domains can be ordered in the sandbox today. The live side needs a registrar key, and until this deployment is given one these routes answer 422 not_available there. Sandbox domains are not registered anywhere, unless this deployment is configured to register them at the registrar's own test environment instead — see Sandbox and live.

Pass a bare name, or a full domain to put that ending first.

curl "https://resellerhq.com/api/v1/domains/search?query=northwind" \
  -H "Authorization: Bearer $RHQ_API_KEY"
{
  "data": [
    { "domain": "northwind.com", "available": false, "price": 1319, "privacyPrice": 599 },
    { "domain": "northwind.io", "available": true, "price": 3959, "privacyPrice": 0 }
  ]
}

price is what you pay per year, in US cents. privacyPrice is what WHOIS privacy costs you per year on that ending: 0 where the registrar includes it, and null where the ending does not offer privacy at all. See WHOIS privacy below.

A row can report available or taken. A name the registrar could not answer for comes back with unknown: true and its ordinary price, rather than being reported taken: the ending's price is known, only its availability is not, so try the search again shortly. A premium name is left out of search results altogether, since one yearly price cannot also price a premium renewal.

Check a list

To buy several at once, check up to 100 exact domains in one call:

curl -X POST https://resellerhq.com/api/v1/domains/check \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domains":["northwind.io","northwind.dev"]}'

A name that is malformed, or whose ending is not on offer, comes back with available: false and a null price. So does a premium name, with premium: true, and one the registrar could not check, with unknown: true; try that one again shortly. A row can therefore report: available, taken, ending not sold (the ending is not on offer), premium (not sold yet), or could not check. Then register the ones you want, one request each. The dashboard's Bulk import page does exactly this from a pasted list or a CSV file.

Register

The dashboard's own registration screen does exactly what the calls below do: search a name, see its price, and register it to a customer.

Register a domain0:34

A registration is a sale to a customer, so customerId is required; see Buying for a customer for how to create one.

Every registration also needs a registrant — in the sandbox exactly as it does live. Set one once as your account's default, so no later order has to carry it:

curl -X PATCH https://resellerhq.com/api/v1/account/domains \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"defaultRegistrant":{"name":"Jo Bloggs","email":"jo@northwind.example","phone":"+1 415 555 0132","address":"1 Market Street","city":"San Francisco","postalCode":"94105","country":"US"}}'

Then register, naming only the domain, the term and the customer:

curl -X POST https://resellerhq.com/api/v1/domains \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"domain":"northwind.io","years":1,"customerId":"cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA"}'

years is 1 to 10 and defaults to 1. The wallet is charged price × years only if the registration succeeds. Add "privacy": true to the order and WHOIS privacy is charged with it, in the same single charge.

Without a default registrant, send a contact with the order instead — the same shape as the registrant in Nameservers, contact and transfer lock below — and that domain is registered to it:

curl -X POST https://resellerhq.com/api/v1/domains \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"domain":"northwind.io","years":1,"customerId":"cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA","contact":{"name":"Jo Bloggs","email":"jo@northwind.example","phone":"+1 415 555 0132","address":"1 Market Street","city":"San Francisco","postalCode":"94105","country":"US"}}'

There are three ways to name a registrant on an order, and an order uses exactly one of them:

  • contact — a one-off contact, used for this domain alone and stored nowhere else.
  • registrant — "customer" for the named customer's own stored registrant, or "default" for your account's default registrant. The stored contact is used as it stands, so the registrar contact behind it — and any validation already done on it — is reused rather than created again. Naming one that is not stored is refused with 422 registrant_required, never served from the other one.
  • neither — the order falls back to the first it finds: the registrant stored on the named customer, then your account's own default registrant.
curl -X POST https://resellerhq.com/api/v1/domains \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"domain":"northwind.io","years":1,"customerId":"cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA","registrant":"default"}'

Send contact and registrant together and the order is refused with 400 validation_error: one is a one-off, the other names a stored contact. With no registrant to be found at all, the order is refused with 422 registrant_required before anything is charged — in the sandbox exactly as live.

ErrorMeaning
409 domain_takenSomeone else has it.
422 premium_domainA premium name; not sold here yet.
502 check_failedThe registrar could not be reached to check it. Nothing was charged.
409 domain_ownedYou already have it.
422 unsupported_tldThat ending is not on offer.
402 insufficient_fundsThe wallet cannot cover it. Nothing was charged.
422 registrant_requiredNo contact on the order, no registrant on the customer, and no default registrant set — or registrant named one of those two and it is not stored.
400 validation_errorThe order sent both contact and registrant.
422 privacy_unavailableThe order asked for privacy on an ending that does not offer it. Nothing was charged.

A registration can come back pending: the registrar is still working on it. It settles to active by itself; nothing more needs to be sent for that domain.

Renew

curl -X POST https://resellerhq.com/api/v1/domains/DOMAIN_ID/renew \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"years":1}'

Renewal adds years to the end of the current registration, at the price the domain was registered at.

A registration records the domain as a sale to the customer named on it, at their price. See Buying for a customer.

A renewal charges the domain, and WHOIS privacy too for as long as privacy is on, in one charge. The two are recorded as two sale lines.

The life of a domain

Every domain carries a status. It is the one field to branch on: what you can do to a domain is decided by where it stands with the registry.

statusWhat it meansWhat you can do
pendingThe registrar has not finished the registration. It settles by itself.Wait
pending_validationRegistered and billed, held behind contact validation.Renew, change, suspend, delete
activeRegistered and working.Renew, change, suspend, delete
expiredPast its expiry date, and still renewable.Renew, delete
redemptionPast the renewal window. The registry has deleted it and holds it for its redemption period.Restore
deletedYou gave the registration up. It stays on the account as the record of what you sold.Nothing
transferred_outAnother registrar took it over.Nothing
needs_reconciliationThe last registrar call is unknown and is being checked.Wait

We move a domain to expired once its expiry date passes, which raises a domain.expired webhook, and a nightly read of the registry's own status list moves it on to redemption when the registry gets there. The three calls below raise domain.restored, domain.deleted and domain.suspended/domain.unsuspended of their own.

WHOIS privacy

Without privacy, the registrant's name, address and phone number can be published in public WHOIS and RDAP. Privacy replaces them with the registrar's own details. It is priced per year per domain, separately from the name itself, and it is yours to mark up, include for nothing or not offer at all.

Turn it on with the registration:

curl -X POST https://resellerhq.com/api/v1/domains \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"domain":"northwind.io","years":1,"customerId":"cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA","privacy":true}'

The wallet is charged (price + privacyPrice) × years in one charge, and the customer gets two sale lines: the domain, and the privacy. The second is priced as the domain_privacy product, so a price list can mark privacy up, fix its price, or sell it for nothing; see Customer pricing.

Leave privacy out and your account's own default decides:

curl -X PATCH https://resellerhq.com/api/v1/account/domains \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"privacyByDefault":true}'

With that on, every new registration gets privacy wherever the ending offers it, and an ending that does not is registered without it rather than refused. Asking for privacy: true explicitly on such an ending is refused with 422 privacy_unavailable, before anything is charged. In the dashboard this is Settings → Domains → Privacy on by default.

Turn privacy on or off later with the update call:

curl -X PATCH https://resellerhq.com/api/v1/domains/DOMAIN_ID \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"privacy":true}'

Turning it on charges your wallet for the whole years left in the current registration term: privacyPrice times the smallest whole number of years that reaches expiresAt, counted on the calendar so a leap day inside the term costs nothing, never fewer than one and never more than ten. A term with eighteen months to run is charged two years and one with three days left is charged one. Privacy is sold by the year, like the name itself — the registrar's price for it is per year and it bills whole years, so whole years are what you are charged and whole years are what you can bill on. The charge is recorded as a domain_privacy sale line to the domain's customer, one unit per year, as a registration's privacy is. Because it is sold by the year, a part year left in the term is charged as one whole year, and each renewal from then on charges privacy for its renewal years on top while it stays on. An ending the registrar includes privacy on for nothing is charged nothing, and still gets its sale line. Asking for privacy a domain already has — {"privacy":true} on a domain whose privacy is already on — changes nothing and charges nothing. Turning privacy off refunds nothing: privacy simply stops, and the next renewal no longer charges for it. Both answers carry the domain with privacy and, while it is on, privacyPrice — what you pay per year for it.

Send an Idempotency-Key with it, as you would with a renewal: a retry after a timeout then lands on the one charge rather than paying twice.

ErrorMeaning
422 privacy_unavailableThis ending does not offer privacy.
402 insufficient_fundsThe wallet cannot cover the years left in the term. Nothing changed.
403 reseller_suspendedA suspended account cannot spend, so it cannot turn privacy on.
409 domain_busyA renewal or another charge for this domain is in progress. Try again shortly.
502 check_failedThe registrar could not be reached to price privacy for this ending. Nothing changed.
409 domain_expiredThe domain is expired, still registering, or being checked with the registrar.

In the dashboard this is Manage → WHOIS privacy on the domain's own page, and a bulk action on the domains list for several at once. Its dialog shows the amount, and the years it pays for, before you confirm.

Nameservers, contact and transfer lock

A registered domain starts on the registrar's own nameservers, with the registrant it was registered to and its transfer lock on. Change any of those, or WHOIS privacy, with one request, sending only the fields you want to change. Nameservers, contacts and the transfer lock are free to change; only privacy turned on is charged, as WHOIS privacy above describes.

curl -X PATCH https://resellerhq.com/api/v1/domains/dom_01JABCDEF \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"nameservers":["ns1.example-dns.com","ns2.example-dns.com"],"locked":false}'
  • nameservers is the full new list, 2 to 6 hostnames. null goes back to the default.
  • contact is the registrant: name, email, phone, address, city, postal code and country, with organisation and state optional. It is replaced whole.
  • contacts holds the administrative, technical and billing contacts, as admin, tech and billing. Each has the same fields as contact. One that is null is the same as the registrant; send null to put it back.
  • locked is whether transfers to another registrar are refused.
  • suspended is whether the domain is on hold at the registrar; see Suspend a domain below.
  • privacy is whether WHOIS privacy is on; see WHOIS privacy above for what it costs and when.
  • releaseHostedDns switches hosted DNS off as part of the same change. Without it, changing the nameservers of a domain whose DNS we host is refused with 409 dns_hosted: its nameservers are ours while we host the zone.

In the sandbox these settings are stored and returned, and nothing is sent to a registrar.

Child nameservers

Some domains need their own nameservers, hosted under themselves — ns1.acme.com for acme.com, rather than someone else's. The registry calls this a host, or a glue record: a name and the addresses it answers on, registered once at the registrar and then named in nameservers like any other.

Create one:

curl -X POST https://resellerhq.com/api/v1/domains/DOMAIN_ID/hosts \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"host":"ns1.acme.com","addresses":["203.0.113.10"]}'
{ "host": "ns1.acme.com", "addresses": ["203.0.113.10"] }

host must be a hostname under this exact domain — ns1.acme.com for acme.com, never another domain's name and never the domain's own apex. addresses is 1 to 10 unique IPv4 or IPv6 addresses.

List what is registered under a domain:

curl https://resellerhq.com/api/v1/domains/DOMAIN_ID/hosts \
  -H "Authorization: Bearer $RHQ_API_KEY"
{ "hosts": [{ "host": "ns1.acme.com", "addresses": ["203.0.113.10"] }] }

To add a second address — or change the addresses in any way — send the full new list; the update always replaces it whole:

curl -X PATCH https://resellerhq.com/api/v1/domains/DOMAIN_ID/hosts/ns1.acme.com \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"addresses":["203.0.113.10","2001:db8::1"]}'

A host cannot be renamed. To change the name itself, delete it and create the new one.

Point the domain at a host once it exists, with the ordinary update call:

curl -X PATCH https://resellerhq.com/api/v1/domains/DOMAIN_ID \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"nameservers":["ns1.acme.com","ns2.acme.com"]}'

Delete a host once nothing names it as a nameserver any more:

curl -X DELETE https://resellerhq.com/api/v1/domains/DOMAIN_ID/hosts/ns1.acme.com \
  -H "Authorization: Bearer $RHQ_API_KEY"
ErrorMeaning
422 host_not_under_domainThe host is not a hostname under this exact domain.
422 invalid_host_addressNot 1 to 10 unique, valid IPv4 or IPv6 addresses.
409 host_in_useOne of your domains still names this host as a nameserver. The message says which, and the body's details.domain carries the same domain as data; point that domain elsewhere first. A domain you have deleted or transferred away does not hold a host back.
409 registrar_refusedThe registrar would not have it — the name is already registered as a host, say. Sending the same thing again will not help.
404 not_foundNo such host at the registrar, or no such domain on your account.
502 host_failedThe registrar could not be reached; try again shortly.

Hosts live at the registrar, not on your domain's own record, so a list always reads them fresh — nothing here is stored on the domain itself.

Hosted DNS

A domain's DNS can be hosted here, included with the registration: no wallet movement, nothing charged. Switch it on per domain and the domain moves onto our nameservers, where you keep its records — A, AAAA, CNAME, MX, TXT, SRV, CAA, NS and ALIAS — one at a time or a whole set in one call.

curl -X PUT https://resellerhq.com/api/v1/domains/DOMAIN_ID/dns \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"hosted":true}'

Every domain carries dns: { supported, hosted }: whether the registrar behind it hosts zones at all, and whether we host this one. Switching hosting off puts the nameservers you choose back and gives the zone up, with the records stored with it. See the DNS guide for the record types, what is refused before the registrar sees it, and how long a change takes to be seen. In the dashboard this is the DNS panel on the domain's own page.

Suspend a domain

A suspension puts the domain on hold at the registrar: it stops resolving, so the website and the email on it go dark, but it stays registered, stays yours, and still renews. It is what to reach for when a customer has not paid, or when a name is being used for something you will not host — and it is undone as easily as it is done.

curl -X PATCH https://resellerhq.com/api/v1/domains/DOMAIN_ID \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"suspended":true}'

Send {"suspended": false} to lift it. Nothing is charged either way, and nothing is refunded: the registration is untouched. The hold is a registry status alongside the transfer lock, so suspending a domain never unlocks it and unsuspending never locks it. The domain reads back with suspended: true, and a suspension raises both domain.updated (with suspended in changed) and a domain.suspended of its own, so you can subscribe to just the one you care about.

A hold can only be set on a domain the registrar will still change — active or pending_validation. Anything else is refused with 409 domain_expired, whose message says which state it found.

In the dashboard this is Manage → Suspend on the domain's own page, and a bulk action on the domains list for several at once.

Restore a domain in redemption

Once a domain passes its renewal window the registry deletes it and holds it for a redemption period of its own. A renewal can no longer help; only a restore can, and a restore costs many times a renewal because the registry charges for it. So ask what it costs first:

curl https://resellerhq.com/api/v1/domains/DOMAIN_ID/restore-quote \
  -H "Authorization: Bearer $RHQ_API_KEY"
{ "cost": 3957, "renewalYears": 1, "renewalCost": 1319, "total": 5276, "unknown": false }

cost is the restore itself and renewalCost the year the registry charges along with it, in US cents; total is what your wallet is charged. unknown: true means the registrar could not price it and total comes from our catalogue instead — still chargeable, but the figure may move. In the sandbox a restore is three times the ending's price, plus one year at that price. If the registrar cannot be reached at all, the quote answers 502 check_failed rather than a figure we cannot stand behind.

Then restore it:

curl -X POST https://resellerhq.com/api/v1/domains/DOMAIN_ID/restore \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

The domain comes back active with the new expiry date the registry gave it, and the charge is recorded as one sale to the customer, described as the-domain.com, restored, under a restore action of its own. A price list can aim a rule at restores, and a restore is never read as the domain's newest renewal price; see Customer pricing. Send an Idempotency-Key and a retry can never charge twice.

ErrorMeaning
409 domain_not_in_redemptionOnly a domain in redemption can be restored. The message says what state it is in instead.
409 domain_busyA charge for this domain is still being settled. Try again shortly.
402 insufficient_fundsThe wallet cannot cover the restore. Nothing was charged.
502 restore_failedThe registrar would not restore it. Nothing was charged; try again shortly.

Should the registrar accept a restore and then not finish it, the money is held until it resolves and a domain.restore_failed webhook tells you when it did not: the hold is given back and the domain is still in redemption.

In the dashboard this is Manage → Restore on the domain's own page, which shows the quote before you commit.

Delete a domain

Deleting gives the registration up at the registrar. The name stops being yours and goes back to the registry, which holds it through its own delete and redemption periods before anyone else can have it. This cannot be undone from here.

Registries credit a registration deleted very soon after it was made — the add grace period, five days at most registries — and nothing else. Ask what you would get back:

curl https://resellerhq.com/api/v1/domains/DOMAIN_ID/delete-quote \
  -H "Authorization: Bearer $RHQ_API_KEY"
{ "refund": 1319, "graceEndsAt": "2026-10-12T11:04:31.000Z" }

refund is what the wallet is credited if you delete now, and graceEndsAt when that window closes — null where the registry has no such window at all, and a refund of 0 once it has passed. The figure is the registration charge alone:

  • not WHOIS privacy, which is never refunded, here or when you turn it off;
  • and never a renewal, not even one bought the same day: the registry credits the registration and nothing else, and its own window for a renewal is a separate one we do not claim against.

So a delete never gives back more than the registration took. The window is the registrar's to state, so an unreachable registrar answers 502 check_failed here rather than a refund of 0.

Then delete it:

curl -X DELETE https://resellerhq.com/api/v1/domains/DOMAIN_ID \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

The registrar is told first and the money moves only afterwards, so nothing is ever refunded for a delete the registrar did not accept. The answer is the domain with status: "deleted": the row stays on your account as the record of what you sold, and nothing more can be done to it. The refund is keyed on the domain, so a retried delete credits the wallet exactly once.

ErrorMeaning
409 domain_not_deletableOnly active, pending_validation and expired domains can be deleted. The message says what state it found.
409 domain_busyA charge for this domain is still being settled. A domain money is still moving for is not deleted under.
502 delete_failedThe registrar would not delete it. Nothing changed and nothing was refunded.

What the customer was already invoiced for is left alone by a delete; credit it yourself if the sale is going back too.

In the dashboard this is Manage → Delete on the domain's own page, which shows the refund line before you commit.

What was done to a domain

Every change to a domain is recorded, and the record reads back per domain, newest first:

curl "https://resellerhq.com/api/v1/domains/DOMAIN_ID/history?limit=25" \
  -H "Authorization: Bearer $RHQ_API_KEY"
{
  "data": [
    {
      "id": "evt_01JABCDEF...",
      "type": "domain.suspended",
      "at": "2026-10-07T11:04:31.000Z",
      "actor": { "kind": "api_key", "label": "Billing automation" },
      "detail": "Put on hold at the registrar: it stops resolving but stays registered"
    }
  ],
  "nextCursor": null,
  "total": 1
}

type is the same event type a webhook carries, detail says what changed in words, and actor says who asked for it: an api_key with the key's name, a user with the colleague's name, or system for our own worker. It is null on anything done before we started recording that. Pages work as everywhere else: limit and the nextCursor from the page before.

Moving a domain away

A domain belongs to whoever it is registered to, and they can move it to another registrar whenever they like. Two things are needed for that: the transfer lock off, and the domain's transfer code — the EPP code, or auth code — which the gaining registrar asks for.

Turn the lock off first, with the update call above:

curl -X PATCH https://resellerhq.com/api/v1/domains/DOMAIN_ID \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"locked":false}'

Then ask for a code:

curl -X POST https://resellerhq.com/api/v1/domains/DOMAIN_ID/auth-code \
  -H "Authorization: Bearer $RHQ_API_KEY"
{ "authCode": "7Qf2xKpR4mTz9bHd" }

Read it now and hand it to whoever is taking the domain. We do not keep it: it is in that one response and nowhere else — not in the domain, not in a list, not in a webhook — so if it is lost, ask for another. Asking for another replaces the one before it, and the old code stops working, so do not ask again while a transfer is in progress.

ErrorMeaning
409 domain_lockedThe transfer lock is still on. Turn it off first; we never turn it off for you.
409 domain_expiredThe domain is expired, still registering, or being checked with the registrar.
502 auth_code_failedThe registrar would not issue a code. Nothing changed; try again shortly.

In the dashboard this is Manage → Get transfer code on the domain's own page, which shows the code once with a copy button.

Once the other registrar finishes the transfer, the domain's status becomes transferred_out: it stays on your account as a record of what you sold, is no longer renewed or charged for, and cannot be changed. A domain.transferred_out webhook tells you when that happens — usually a few days after the code changes hands, since a transfer runs on the registry's clock, not ours. The request for a code is itself recorded as domain.auth_code_generated, which says which domain and which kind of credential asked, and never the code.

Not built yet

Transfers in and automatic renewal are not part of the API yet.

In the API reference