Skip to content

Added support for new Hetzner Cloud Api#852

Closed
lecram89 wants to merge 3 commits into
ddclient:mainfrom
lecram89:main
Closed

Added support for new Hetzner Cloud Api#852
lecram89 wants to merge 3 commits into
ddclient:mainfrom
lecram89:main

Conversation

@lecram89
Copy link
Copy Markdown
Contributor

@lecram89 lecram89 commented Oct 12, 2025

Added support for the new Cloud DNS Api from Hetzner:
https://docs.hetzner.cloud/reference/cloud#overview

@SchoolGuy
Copy link
Copy Markdown

Is there a way to fast-track this PR? I can't migrate my zones because this PR is not part of a released version.

@tomtrix
Copy link
Copy Markdown

tomtrix commented Nov 15, 2025

Is there a way to fast-track this PR? I can't migrate my zones because this PR is not part of a released version.

Same here. I would really appreciate a fast release of this new feature!

Comment thread ddclient.in
debug("creating DNS '$type'");
$url = "https://" . opt('server', $domain) . "/zones/$zone/rrsets";
$http_method = "POST";
$data = "{\"name\":\"$hostname\",\"type\":\"$type\",\"ttl\":" . opt('ttl', $domain) . ",\"records\":[{\"value\":\"$ip\",\"comment\":\"Created by ddclient\"}],\"labels\":{\"environment\":\"prod\",\"example.com/my\":\"label\"}}";
Copy link
Copy Markdown

@LKaemmerling LKaemmerling Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you don't want to put labels on every rrset (like this) :) Would maybe a good idea to drop them if they are not needed. Or maybe put the created by into a label and let the users still be able to change the comment on each individual record.

@GamingJakob
Copy link
Copy Markdown

@lecram89 Can you fix what the reviewer pointed out?

@realizelol
Copy link
Copy Markdown

If I understand the documentation https://docs.hetzner.cloud/reference/cloud#zone-rrsets-create-an-rrset correctly then it's not needed.

For this point of view:

{
  "name": "www",
  "type": "A",
  "ttl": 3600,
  "records": [
    {
      "value": "198.51.100.1",
      "comment": "My web server at Hetzner Cloud."
    }
  ],
  "labels": {
    "environment": "prod",
    "example.com/my": "label",
    "just-a-key": ""
  }
}

only these keys are mandatory:
name, type, records [only value]

So this should be enough:

{
  "name": "www",
  "type": "A",
  "records": [
    {
      "value": "198.51.100.1"
    }
  ]
}

So doing this on the $data value:

$data = "{\"name\":\"$hostname\",\"type\":\"$type\",\"ttl\":" . opt('ttl', $domain) . ",\"records\":[{\"value\":\"$ip\"}]}";

But wouldn't it also make sense to apostrophe the part - so sth. like this?

$data = '{"name":"$hostname","type":"$type","ttl":' . opt('ttl', "$domain") . ',"records":[{"value":"$ip"}]}";

@erahhal
Copy link
Copy Markdown

erahhal commented Dec 15, 2025

I tried using this PR's implementation and encountered 404 errors. After investigating, I found the issue:

The current implementation uses an incorrect API endpoint structure:

my $url = "https://" . opt('server', $domain) . "/zones/$zone/rrsets/$hostname/$type";

This uses the zone name directly in the URL path (/zones/$zone/rrsets/...), but the Hetzner Cloud API requires a zone ID, not a zone name.

According to the Hetzner Cloud API documentation, the correct flow is:

  1. Get Zone ID first: GET /zones?name=<zone_name> - This returns a list of zones, from which you extract the zone ID
  2. List records: GET /zones/{zone_id}/records - Note: uses records, not rrsets
  3. Update record: PUT /zones/{zone_id}/records/{record_id}
  4. Create record: POST /zones/{zone_id}/records

The fix mirrors the existing hetzner protocol (which works with dns.hetzner.com/api/v1) but uses:

  • Authorization: Bearer <token> instead of Auth-API-Token: <token>
  • api.hetzner.cloud/v1 as the default server

Key changes needed:

# 1. First get the zone ID
my $url = "https://$config{$key}{'server'}/zones?name=" . $config{$key}{'zone'};
# ... fetch and parse to get $zone_id ...

# 2. Get records using zone ID
$url = "https://$config{$key}{'server'}/zones/$zone_id/records";

# 3. Update or create using zone ID
if ($dns_rec_id) {
    $url = "https://$config{$key}{'server'}/zones/$zone_id/records/$dns_rec_id";
    $http_method = "PUT";
} else {
    $url = "https://$config{$key}{'server'}/zones/$zone_id/records";
    $http_method = "POST";
}

my $data = "{\"zone_id\":\"$zone_id\", \"name\": \"$hostname\", \"value\": \"$ip\", \"type\": \"$type\", \"ttl\": $config{$key}{'ttl'}}";

This PR targets master which uses ddclient::Protocol->new() syntax. For backporting to v3.11.2 (current stable in some distros), the protocol definition needs to use the hash-based syntax that matches other protocols in that version.

@LKaemmerling
Copy link
Copy Markdown

This uses the zone name directly in the URL path (/zones/$zone/rrsets/...), but the Hetzner Cloud API requires a zone ID, not a zone name.

@erahhal using the zone name is totally fine: https://docs.hetzner.cloud/reference/cloud#zone-rrsets-list-rrsets

@enspiro
Copy link
Copy Markdown

enspiro commented Feb 1, 2026

Hi Guys,
i am depended on that new Hetzner Cloud API functionality.
Is there a planned date by when this will be released?

@markuspabst
Copy link
Copy Markdown

Hi Guys, i am depended on that new Hetzner Cloud API functionality. Is there a planned date by when this will be released?

This also works:
https://github.com/filiparag/hetzner_ddns

@heiko-schabert
Copy link
Copy Markdown

Anything I can support to get this merged?

Hetzner will shutdown the old approach till end May 2026
Migration FAQ.

@30350n
Copy link
Copy Markdown

30350n commented Mar 18, 2026

I just gave this a try by overriding the NixOS package and one thing I noticed is that this introduces JSON as a dependency, while the rest of ddclient seems to rely on JSONPP. So this is something that might need to be adjusted.

@cr3
Copy link
Copy Markdown
Contributor

cr3 commented Mar 25, 2026

Since there have been no updates for a while, I addressed all the concerns in this conversation here so that we can merge these changes before end of May.

  • @LKaemmerling, I removed unnecessary labels on hetznercloud update (6ce7130)
  • @realizelol , I also removed the record comment on hetznercloud update (099c6a8)
  • @erahhal , the code fine, I tested the code in my branch and I get:
    SUCCESS: [hetznercloud][mail.taram.ca]> IPv4 address set to 134.65.182.79
  • @30350n , good catch, hetznercloud needed to be added to some needs_json array (c27e6a9)

@heiko-schabert
Copy link
Copy Markdown

heiko-schabert commented Mar 25, 2026

I just gave this a try by overriding the NixOS package and one thing I noticed is that this introduces JSON as a dependency, while the rest of ddclient seems to rely on JSONPP. So this is something that might need to be adjusted.

True. Currently integrate this branch. Adding ps.JSON to myperl is enough. It build ans runs. But, one observation is that the behavior changed.

Previously If records (e.g. A/AAAA) were not available, these were created with the IP/IPv6 addresses
Now ddclient fails

Have not done a deep dive, but will. Someone else can confirm?

@LKaemmerling
Copy link
Copy Markdown

LKaemmerling commented Apr 14, 2026

Just to line this out, the old DNS API will be turned off at 20. May 09:00 UTC. As of this, the currently running brownouts will be permanent and also cover Read Endpoints soon after. The automatic migration is already running, so expect to have the zones migrated at latest at the beginning of May. (Zones with active API tokes, like the ones ddclient use, will be the last zones on the list)

@lecram89 lecram89 closed this May 17, 2026
@lecram89
Copy link
Copy Markdown
Contributor Author

I closed this pull request as this already got merged. Thanks @cr3!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.