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.

URL forwarding

Redirects the domain - or a name under it - to a destination you already have, with no server of your own. It is a permanent (301) redirect: the registrar's own zone API offers no temporary kind, so there is nothing to choose.

curl -X PUT https://resellerhq.com/api/v1/domains/DOMAIN_ID/forwarding \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"records":[{"target":"https://example.com"},{"subdomain":"shop","target":"https://shop.example.com"}]}'

subdomain is empty or left out for the root. The answer is the full new set, as URL records carrying "managedBy":"url_forwarding". This call replaces every URL-forwarding record at once - anything hand-made, or made by email forwarding, is untouched - so send every entry you want kept, and an empty records list removes URL forwarding entirely.

A destination that is not an absolute http:// or https:// URL is refused with 422 invalid_forward_url. A name that already holds a record of any kind - including another URL forward - is refused the same way a second CNAME there would be.

Email forwarding

Forwards mail addressed to this domain to another inbox you already have - your own Gmail, say

  • with no mail server of your own.
curl -X PUT https://resellerhq.com/api/v1/domains/DOMAIN_ID/email-forwarding \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"records":[{"address":"sales","target":"[email protected]"},{"target":"[email protected]"}]}'

address is the local part: sales forwards mail sent to [email protected]. Leave it out, or send "*", for the catch-all - everything else sent to the domain. The answer is the full new set, as MBOXFW records carrying "managedBy":"email_forwarding". Like URL forwarding, this call replaces every email-forwarding record at once, and an empty records list removes it entirely.

A destination that is not an email address is refused with 422 invalid_forward_address.

Managed records

A record either of these made carries managedBy - "url_forwarding" or "email_forwarding" - when you read the zone back (GET …/dns/records). Change or remove it from its own route above, never the generic record routes, which refuse both:

{
  "error": "dns_record_managed",
  "message": "This record is managed by forwarding; change it from the forwarding settings instead",
  "requestId": "req-4f"
}

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. A URL-forwarding entry is exclusive the same way.
422 invalid_forward_urlA URL-forwarding destination is not an absolute http:// or https:// URL.
422 invalid_forward_addressAn email-forwarding destination is not an email address.
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_record_managedThis record was made by URL or email forwarding. Change it from PUT …/forwarding or PUT …/email-forwarding instead.
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.

A domain connected for mailboxes has its DNS hosted with them: a nameserver change away from the mail provider's own is refused with 409 nameservers_used_by_mailboxes unless the same call also sends acknowledgeMailboxes: true, and turning DNSSEC on or off (or managing its keys) is refused outright with 409 dnssec_not_available - there is nothing to sign while the zone is theirs.

One change to a domain's DNS at a time. A record change, switching hosting on or off, and the nameserver change that gives a hosted zone up all share one lock per domain: whichever of them is accepted first holds it from that moment until the registrar has taken the change, and anything else that would touch the zone or the delegation while it stands - another record change, a second attempt to switch hosting, a release racing a record change - 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.

DNSSEC

Signs a domain so a resolver can tell its records are genuine rather than forged along the way. Every domain carries whether its own ending can be signed at all, whether it is signed now, and what the registry holds for it:

{ "dns": { "dnssecSupported": true, "signed": false, "dsRecords": [], "keys": [] } }

dnssecSupported is a fact about the ending, not about the registrar - it can be true even where supported above is false, and the other way round, since a registrar that hosts zones fine may still refuse DNSSEC on a given TLD. An ending that refuses it answers 422 dnssec_unavailable to both routes below. dsRecords is the DS data the registry holds right now; empty means not signed. keys only means anything once DNS is hosted elsewhere - it is the last list sent with PUT …/dnssec.

A zone we host

One flag. We manage the keys and any rollover ourselves:

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

dnssec can be sent together with hosted or on its own, as long as the domain is already hosted with us. Turning hosted DNS off turns DNSSEC off with it - there is no zone left to sign

  • and the DS records go with it the same way the records do.

DNS hosted elsewhere

Send the keys your own DNS provider generated, so the registry can add the matching DS record:

curl -X PUT https://resellerhq.com/api/v1/domains/DOMAIN_ID/dnssec \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"keys":[{"flags":257,"algorithm":13,"publicKey":"BASE64_PUBLIC_KEY"}]}'

flags is 256 for a zone-signing key or 257 for a key-signing key; algorithm is one of the IANA DNSSEC algorithm numbers (3, 5, 6, 7, 8, 10, 12, 13, 14, 15, 16, 17, 23); publicKey is base64, whitespace tolerated and stripped before it is checked or sent - a key copied out of a zone file wraps across lines. An empty keys list removes every key and, with it, every DS record the registry holds - the only way one is ever removed. This route is refused with 409 dns_hosted while DNS is hosted with us: sign or unsign that zone with PUT …/dns instead.

What can go wrong

  • A wrong key makes the domain unreachable until it is fixed or removed: a resolver that validates DNSSEC refuses every answer it cannot verify against the DS record at the registry, rather than falling back to an unsigned one. Copy each value exactly from your DNS provider, and check a domain with a DNSSEC checker before relying on it.
  • A malformed key - the wrong flags, an algorithm not in the list above, or an empty or non-base64 publicKey - is refused with 422 invalid_dnssec_key, naming which key, before anything reaches the registry.
  • Signing or unsigning a hosted zone is a change at the registry, so - like a nameserver change
    • it can take up to a day to reach every resolver. A DS record that has not arrived yet reads back as an empty dsRecords list even though signed is already true.
  • DS data is read-only in the direction that adds it: you can only ever remove what the registry holds (by clearing the keys, or by unsigning a hosted zone), never add a DS record directly. Sending a matching key is the only way one appears.
ErrorMeaning
422 dnssec_unavailableThis ending does not support DNSSEC.
422 invalid_dnssec_keyOne of the keys sent is malformed. The message names which key and why.
422 too_many_dnssec_keysAt most 4 keys are allowed in one list.
409 dns_hostedDNS for this domain is hosted with us; sign or unsign it with PUT …/dns instead of sending keys.

Branded nameservers

Useful when you want a domain's nameservers to read as your own name rather than ours or the registrar's - a customer who looks a domain up sees ns1.yourcompany.com, not ns1.sandbox-registrar.test.

It takes a domain you already hold with us, to carry 2 to 4 host names pointed at fixed, registrar-documented addresses - the same kind of hostnames child nameservers already are, just carrying every other domain's DNS rather than their own.

Set it up once, in Settings → Ordering defaults: pick the domain, name its hostnames, and wait for them to be ready. From then on it is a plain nameserver change on whichever domains you choose to point at it - one at a time, or several together from the domains list's bulk actions - or automatically on every new domain, once the set is ready.

Unlike a change to nameservers elsewhere, moving a domain onto the branded set never gives up hosted DNS: a domain that is already hosted keeps its zone and records, and one that is not starts being hosted with us. releaseHostedDns is never needed for this change.

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. Below the table, URL forwarding and Email forwarding are two short sections of their own, each with its own Add forwarding dialog; a record either one made shows in the records table above with a "Forwarding" badge and a link to its own section, rather than a row menu.

Below that, on an ending that supports DNSSEC, a DNSSEC panel: a Sign this zone switch and the DS record the registry holds when we host the zone, or a Manage keys button opening a dialog to paste the keys your own DNS provider generated when we do not. An ending that cannot be signed shows no panel at all.

In the API reference