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.
Search
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 with422 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.
| Error | Meaning |
|---|---|
409 domain_taken | Someone else has it. |
422 premium_domain | A premium name; not sold here yet. |
502 check_failed | The registrar could not be reached to check it. Nothing was charged. |
409 domain_owned | You already have it. |
422 unsupported_tld | That ending is not on offer. |
402 insufficient_funds | The wallet cannot cover it. Nothing was charged. |
422 registrant_required | No 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_error | The order sent both contact and registrant. |
422 privacy_unavailable | The 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.
status | What it means | What you can do |
|---|---|---|
pending | The registrar has not finished the registration. It settles by itself. | Wait |
pending_validation | Registered and billed, held behind contact validation. | Renew, change, suspend, delete |
active | Registered and working. | Renew, change, suspend, delete |
expired | Past its expiry date, and still renewable. | Renew, delete |
redemption | Past the renewal window. The registry has deleted it and holds it for its redemption period. | Restore |
deleted | You gave the registration up. It stays on the account as the record of what you sold. | Nothing |
transferred_out | Another registrar took it over. | Nothing |
needs_reconciliation | The 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.
| Error | Meaning |
|---|---|
422 privacy_unavailable | This ending does not offer privacy. |
402 insufficient_funds | The wallet cannot cover the years left in the term. Nothing changed. |
403 reseller_suspended | A suspended account cannot spend, so it cannot turn privacy on. |
409 domain_busy | A renewal or another charge for this domain is in progress. Try again shortly. |
502 check_failed | The registrar could not be reached to price privacy for this ending. Nothing changed. |
409 domain_expired | The 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}'nameserversis the full new list, 2 to 6 hostnames.nullgoes back to the default.contactis the registrant: name, email, phone, address, city, postal code and country, with organisation and state optional. It is replaced whole.contactsholds the administrative, technical and billing contacts, asadmin,techandbilling. Each has the same fields ascontact. One that isnullis the same as the registrant; sendnullto put it back.lockedis whether transfers to another registrar are refused.suspendedis whether the domain is on hold at the registrar; see Suspend a domain below.privacyis whether WHOIS privacy is on; see WHOIS privacy above for what it costs and when.releaseHostedDnsswitches hosted DNS off as part of the same change. Without it, changing the nameservers of a domain whose DNS we host is refused with409 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"| Error | Meaning |
|---|---|
422 host_not_under_domain | The host is not a hostname under this exact domain. |
422 invalid_host_address | Not 1 to 10 unique, valid IPv4 or IPv6 addresses. |
409 host_in_use | One 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_refused | The registrar would not have it — the name is already registered as a host, say. Sending the same thing again will not help. |
404 not_found | No such host at the registrar, or no such domain on your account. |
502 host_failed | The 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.
| Error | Meaning |
|---|---|
409 domain_not_in_redemption | Only a domain in redemption can be restored. The message says what state it is in instead. |
409 domain_busy | A charge for this domain is still being settled. Try again shortly. |
402 insufficient_funds | The wallet cannot cover the restore. Nothing was charged. |
502 restore_failed | The 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.
| Error | Meaning |
|---|---|
409 domain_not_deletable | Only active, pending_validation and expired domains can be deleted. The message says what state it found. |
409 domain_busy | A charge for this domain is still being settled. A domain money is still moving for is not deleted under. |
502 delete_failed | The 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.
| Error | Meaning |
|---|---|
409 domain_locked | The transfer lock is still on. Turn it off first; we never turn it off for you. |
409 domain_expired | The domain is expired, still registering, or being checked with the registrar. |
502 auth_code_failed | The 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
Search for a domain
One name across every ending on offer.
Register a domain
Charge the wallet and register.
Update a domain
Nameservers, contact and transfer lock.
Renew a domain
Add years to a registration.
Restore a domain
Bring one back out of redemption.
Delete a domain
Give the registration up, with its refund.
List a domain's history
What was done to it, and by whom.
Get a transfer code
The code another registrar asks for.