DNS

Host a domain's DNS with us: turn the zone on, keep its records, and know how long a change takes to be seen.

Sandbox only for now

Hosted DNS can be used 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. A sandbox zone is stored and read back exactly as a live one is, and is served by no real nameserver.

What hosted DNS is here

A domain you register through us can have its DNS hosted by us too, so a customer's name points at a server and receives mail without a second supplier. It is included with the domain: turning it on charges nothing, holds nothing and moves nothing in the wallet.

It is switched on one domain at a time. Every domain carries what you need to decide:

{ "dns": { "supported": true, "hosted": false } }

supported is whether the registrar behind this domain hosts zones at all — a registrar that does not answers 409 dns_not_supported to every route below. hosted is whether we host this one. Both fields are always there.

Turn it on:

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}'

The answer is the domain, now on the registrar's own DNS nameservers — in the sandbox, these two:

{
  "id": "dom_01K6XJ4Q8ZT3M9R5V2B7WDHNCA",
  "domain": "acme.com",
  "nameservers": ["ns1.dns.sandbox-registrar.test", "ns2.dns.sandbox-registrar.test"],
  "dns": { "supported": true, "hosted": true }
}

Three things happen, in this order: the zone is created at the registrar, the domain is pointed at the registrar's own DNS nameservers, and the nameservers it had are kept so turning hosting off can put them back. The zone starts empty — the records you add are the whole of what it answers with.

Read the domain back for the nameservers it is now on, as the sample above does. Those hostnames are the registrar's to publish, so we report what it gave us and never a guessed name; a registrar that publishes none leaves the domain reading back on its default nameservers.

Asking for the state it is already in changes nothing and asks the registrar nothing, so a retry after a timeout is safe.

Reading records needs product.read, and every change below needs domain.manage; see Team.

Records

A record has a name, a type, content, a TTL, and a priority on the two types that take one. Nine types are on offer:

TypeWhat it is forContent
APoint a name at a serverAn IPv4 address, like 203.0.113.10
AAAAThe same, over IPv6An IPv6 address, like 2001:db8::1
CNAMEPoint one name at another nameA hostname. Never at the root, and never beside another record with the same name
MXWhere mail for the domain goesA mail host, with a priority; lower is tried first
TXTFree text: domain verification, SPFUp to 4096 characters
SRVA service's host and portweight port target, like 10 5060 sip.example.com
CAAWhich certificate authorities may issue<flag> <tag> "<value>", the tag being issue, issuewild or iodef
NSDelegate a subdomain elsewhereA hostname
ALIASPoint the root at another nameA hostname. This is what a CNAME cannot do

The root is @. @ and the domain itself mean the same thing, and a bare label like www is taken as being under the domain. A name is never empty: "" is refused with 400 validation_error, so write the root as @. Names are stored and returned absolute and lower case, with any trailing dot stripped, so www, www.acme.com and www.acme.com. are one name. A wildcard (*) and the underscore labels SRV and the mail conventions use (_sip._tcp) are both fine. A name that reads as another domain of its own — www.other-company.com, pasted in by mistake — is refused rather than quietly filed under this domain.

TTL is in seconds, from 60 to 604800, and defaults to 3600 when you send none. It is how long resolvers may cache the record, and it is what decides how long a change takes to be seen.

List what the zone holds:

curl https://resellerhq.com/api/v1/domains/DOMAIN_ID/dns/records \
  -H "Authorization: Bearer $RHQ_API_KEY"
{
  "records": [
    {
      "id": "dns_01K6XJ4Q8ZT3M9R5V2B7WDHNCA",
      "name": "www.acme.com",
      "type": "A",
      "content": "203.0.113.10",
      "ttl": 3600,
      "priority": null
    }
  ]
}

The id is ours, and stable: it is what you change or delete that one record by. priority is always there, and null on a type that has none. A zone holds at most 200 records.

Add a record

curl -X POST https://resellerhq.com/api/v1/domains/DOMAIN_ID/dns/records \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"www","type":"A","content":"203.0.113.10"}'

The answer is the record as it was stored, with its id, at 201. Mail takes a priority:

curl -X POST https://resellerhq.com/api/v1/domains/DOMAIN_ID/dns/records \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"@","type":"MX","content":"mail.example.com","priority":10,"ttl":300}'

Change a record

Send only the fields you want to change:

curl -X PATCH https://resellerhq.com/api/v1/domains/DOMAIN_ID/dns/records/dns_01K6XJ4Q8ZT3M9R5V2B7WDHNCA \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content":"203.0.113.20","ttl":60}'

The id does not change, so the record you edited is the record you read back. Changing the type to one that has no priority drops the priority with it, rather than sending the registrar a priority it would refuse.

Delete a record

curl -X DELETE https://resellerhq.com/api/v1/domains/DOMAIN_ID/dns/records/dns_01K6XJ4Q8ZT3M9R5V2B7WDHNCA \
  -H "Authorization: Bearer $RHQ_API_KEY"

204, with no body. A deleted record's id is gone for good: a later call with it answers 404 not_found.

Replace the whole set

If DNS for a domain lives in a system of your own, swap the zone in one call rather than reconciling it record by record:

curl -X PUT https://resellerhq.com/api/v1/domains/DOMAIN_ID/dns/records \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"records":[{"name":"@","type":"TXT","content":"v=spf1 include:example.com -all"},{"name":"shop","type":"CNAME","content":"shops.example.com"}]}'

The answer is the full new set, in the order you sent it. Four things to know about it:

  • Every record is checked before any of it is written, so one bad record in a hundred leaves the zone exactly as it was.
  • The ids are new. These are new records, not edits of the ones they replace, and the ids of the old set stop working.
  • An empty records list empties the zone.
  • At most 200 records, as everywhere else.

What is refused, and when

Every record is checked here, by us, before the registrar sees any of it — and when a record is refused nothing is sent upstream at all, so a rejected change cannot half-land. The message always names the record it is about:

curl -X POST https://resellerhq.com/api/v1/domains/DOMAIN_ID/dns/records \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"mail","type":"MX","content":"mail.example.com"}'
{
  "error": "invalid_dns_record",
  "message": "Check the MX record for mail.acme.com: a MX record needs a priority between 0 and 65535",
  "requestId": "req-4f"
}

The rules each refusal comes from:

  • A content must be an IPv4 address and AAAA content an IPv6 one; CNAME, ALIAS, NS and MX content must be a hostname.
  • MX and SRV need a priority from 0 to 65535, and every other type refuses one.
  • A CNAME at the root is refused — use an ALIAS record there instead.
  • A CNAME beside any other record with the same name is refused, in either direction: adding the CNAME second is refused, and so is adding anything else at a name a CNAME already holds. Two CNAME records at one name likewise.
  • A record identical to one already in the zone — same name, type and content — is refused.
  • An SRV name must read _service._proto, and its content weight port target. A CAA content must read <flag> <tag> "<value>".

The plain bounds are the request's own. A TTL outside 60 to 604800, a priority outside 0 to 65535, a content over 4096 characters or a name over 253 come back as 400 validation_error, naming the field, before any of the rules above is reached.

ErrorMeaning
422 invalid_dns_recordThe record's name, type, content or priority breaks one of the rules above. The message says which record and why.
422 dns_record_conflictA CNAME cannot sit beside another record with the same name. The message names both records.
422 too_many_dns_recordsA zone holds at most 200 records. The message says how many this change would have left.
409 dns_not_hostedHosted DNS is off for this domain. Turn it on first.
409 dns_not_supportedThe registrar behind this domain does not host DNS.
409 dns_busyAnother change to the same zone is still being written. Nothing of yours was sent or stored: read the records back and send it again.
409 domain_expiredThe domain is in a state the registrar will not change — expired, still registering, or being checked.
404 not_foundNo such record, or no such domain on your account.
502 dns_failedThe registrar could not complete the change. Nothing is stored here, so the zone still reads as whatever the registrar still serves. Try again shortly.

That last one is why a 200 means what it says: a change goes to the registrar first and is stored here only once the registrar has taken it.

One change to a zone at a time. A zone is one list, read and written whole, so a change holds it from the moment it is accepted until the registrar has taken it, and a second change arriving in that window gets 409 dns_busy without anything of it being sent upstream. If you are loading many records, send them in one PUT …/dns/records rather than in parallel calls.

Switching hosting off

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":false,"nameservers":["ns1.example-dns.com","ns2.example-dns.com"]}'

The domain goes onto the nameservers you send — 2 to 6 hostnames, as in any nameserver change. Leave nameservers out and it goes back to the ones it was on before hosting was turned on, or to the registrar's own if it was on those.

The records go with the zone

The zone is given up with the hosting. The records stored here go the moment hosting does — the record routes answer 409 dns_not_hosted from then on — and the registrar deletes the unused zone 7 days later. Turning hosting on again gives you an empty zone, not the one you had, so take a copy with GET …/dns/records first if you may want it back.

Changing the nameservers by hand does the same thing. While we host a domain's DNS its nameservers are ours, so pointing it elsewhere means giving the zone up. A plain update is refused rather than doing that quietly:

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.theirs.example","ns2.theirs.example"]}'
{
  "error": "dns_hosted",
  "message": "Hosted DNS is on for this domain, so its nameservers are ours. Send releaseHostedDns: true to switch hosting off with this change: the registrar deletes the unused zone after 7 days and the records stored here go with it",
  "requestId": "req-4f"
}

Send releaseHostedDns: true alongside and the one call does both — hosting off, new nameservers on:

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.theirs.example","ns2.theirs.example"],"releaseHostedDns":true}'

How long a change takes to be seen

The API answers as soon as the registrar has the change. Being seen is a separate clock, and it is not one we can wind forward.

  • A record you change or delete is cached by every resolver that already asked for it, for as long as that record's own TTL. The TTL that governs is the one on the record you are replacing, not the new one: drop a record from 3600 to 60 and the hour still has to pass before anyone notices the 60. So when a change is planned, lower the TTL first, wait out the old value, then make the change on the short TTL and put the TTL back afterwards.
  • A record you add at a name nothing has looked up yet shows up as soon as our nameservers answer for it — though a resolver that asked for that name and got nothing may hold the nothing for a short while too.
  • Turning hosting on or off, and any nameserver change, is a change at the registry rather than in the zone. Registries and resolvers cache a delegation for far longer than a record: allow up to a day for one to reach everyone, and expect some resolvers to be answering from the old nameservers in the meantime.
  • Neither clock can be hurried. There is no flush, here or at the registrar: a cache belongs to whoever is holding it. Checking from a machine that never asked before — or against our nameservers directly — tells you the zone is right; it does not tell you the internet has caught up yet.

In the dashboard

A domain's own page carries a DNS panel saying whether hosting is on and how many records there are, and its Manage DNS link opens the zone as a page of its own: one table of records, Add record, a row menu to edit or delete one, and a Manage menu holding the Google Workspace, Microsoft 365 and point-at-an-IP presets and Turn hosted DNS off.

In the API reference