Why I really like the hosting.de API (and why it is perfect for Claude Code)

This week I moved this blog, yveshoppe.de and a few other sites from an old Ubuntu box to a new server. The DNS part took maybe two minutes, and I did not open a single web interface for it. That is mostly thanks to the hosting.de API, so imho it deserves a post.

No, this is not sponsored. I just have a whole lot of DNS zones there by now, from SeminarPlan to Fundus and Empoche, most of them static sites on one Hetzner server. And the API is one of the few I never had to fight with.

One pattern for everything

The whole API follows one scheme (the API docs are actually readable, too). Every call is a POST with a JSON body, the auth token is part of that body, and the URL is always <service>/v1/json/<method>:

https://secure.hosting.de/api/dns/v1/json/zonesFind
https://secure.hosting.de/api/dns/v1/json/recordsFind
https://secure.hosting.de/api/dns/v1/json/zoneUpdate
https://secure.hosting.de/api/domain/v1/json/domainsFind

DNS, domains, SSL, mail and webhosting all work the same way. Read calls are called ...Find and all take the same filter, limit, page and sort parameters. Responses always look the same too: a status, a response with data, totalEntries and totalPages, and errors if something went wrong.

Once you made one call work, you basically know how to make all of them work.

A quick look at a zone

I keep the token in a gitignored config file, for the shell an environment variable works fine:

$ curl -s https://secure.hosting.de/api/dns/v1/json/zonesFind \
    -H 'Content-Type: application/json' \
    -d '{"authToken":"'"$HOSTINGDE_TOKEN"'","filter":{"field":"zoneName","value":"bytee.net"},"limit":1}' \
  | jq -r '.response.data[0].records[] | "\(.type)\t\(.name)\t\(.content)"'
A	bytee.net	198.51.100.20
A	www.bytee.net	198.51.100.20
MX	bytee.net	mail.tutanota.de
TXT	bytee.net	"v=spf1 include:spf.tutanota.de include:mailgun.org ~all"
...

The filter also takes wildcards (*) and can be combined with subFilter and subFilterConnective (AND / OR), so you can build quite precise queries without downloading everything first.

Moving a site: change only what needs to change

For the actual switch I use a tiny Node script with two commands, show and point. point sets the A records for @ and www and leaves everything else alone. That matters, because my zones also contain MX, DKIM and MTA-STS records for mail, and I really do not want to touch those during a server move.

zoneUpdate makes this easy. You do not send the whole zone, just a diff:

await api('/dns/v1/json/zoneUpdate', {
  zoneConfig: zone.zoneConfig,
  recordsToAdd: [
    { name: 'yveshoppe.de', type: 'A', content: ip, ttl: 3600 },
    { name: 'www.yveshoppe.de', type: 'A', content: ip, ttl: 3600 },
  ],
  recordsToDelete: existingARecords,
});
$ node network/scripts/dns.mjs point yveshoppe.de 198.51.100.20
OK: yveshoppe.de + www → 198.51.100.20 gesetzt.

Writes are processed asynchronously, so a successful write may come back with status pending instead of success. Treat both as OK, otherwise your script will report an error for a change that actually went through.

Right after that, certbot could validate against the new server, because the authoritative nameservers had the new records immediately. Only resolvers that cached the old records with a 24h TTL needed a while. Lesson for next time: lower the TTL a day before.

The query that found my forgotten zones

My favourite call of the week was recordsFind. Before shutting down the old box I wanted to know whether anything still points to it. That is one request across all zones:

$ curl -s https://secure.hosting.de/api/dns/v1/json/recordsFind \
    -H 'Content-Type: application/json' \
    -d '{"authToken":"'"$HOSTINGDE_TOKEN"'","filter":{"field":"recordContent","value":"203.0.113.10"},"limit":1000}' \
  | jq -r '.response.data[] | "\(.name) \(.type) \(.content)"'
*.yveshoppe.de A 203.0.113.10
geek-life.net A 203.0.113.10
www.geek-life.net A 203.0.113.10
...

That turned up a wildcard record I had completely forgotten, and two zones for domains I gave up years ago. In a web interface I would have clicked through all those zones and probably missed one.

Other things you can do with it

A few ideas, some I use, some are on my list:

  • Bootstrap a new site: create the zone, point it to the server, done. Together with an nginx vhost and certbot, a new static site is live in a few minutes.
  • DNS-01 challenges for wildcard certs: lego (and therefore Traefik) has a hosting.de provider built in.
  • Audits: find all zones without SPF, DMARC or CAA records, find records with a huge TTL, find everything that still points to an IP you want to get rid of.
  • Expiry checks: domainsFind lists all domains in your account, nice for a small cron job that tells you what will run out or renew soon.
  • Backups: dump all zones as JSON once a day into a git repo. Cheap, and you get a history of every DNS change.

Why it works so well with Claude Code

I do most of this together with Claude Code now. I describe what I want ("move these sites to the new server, keep the mail records") and the agent does the work. The hosting.de API is a really good fit for this, and it is the same boring design that makes it nice for humans:

  • Predictable naming. If zonesFind exists, the model correctly guesses that recordsFind and domainsFind exist and take the same filter. Fewer made up endpoints, fewer wasted round trips.
  • One transport, plain JSON. No SDK, no OAuth dance, no XML. A 60 line script is enough, and the agent can read and extend it easily.
  • Diffs instead of full replacements. recordsToAdd and recordsToDelete mean the agent only touches what it is asked to touch. A mistake does not wipe your MX records.
  • Clear separation of reading and writing. All ...Find calls are read only. I can let the agent query freely, while every write goes through my small script.

The last point is important for me. Claude Code asks before running commands, and in auto mode a classifier explicitly treats DNS, domain and certificate changes as sensitive. During the move it blocked a step until I said OK. Annoying for a second, but that is exactly what you want from an agent that holds the keys to your domains.

Two things I would recommend if you try this:

Keep the API token out of the repository (gitignored config file or environment variable) and never let the agent print it. If your account allows it, use a separate key that is only allowed to do what the agent needs.

Build small, boring wrappers like show and point instead of letting the agent compose raw write requests every time. It is easier to review and much harder to break something by accident.

Conclusion

There is nothing fancy about the hosting.de API, and that is exactly why I like it. It is consistent, the filters are powerful, and writes are explicit. That makes it pleasant to script by hand and even more pleasant to hand over to an LLM agent.

If you know a registrar or DNS provider with an even nicer API, let me know.