chuckbartowski/hetzner-sdk

PHP SDK for the Hetzner Cloud, Robot and DNS APIs with typed clients, models and high-level modules

Maintainers

Package info

github.com/ChuckBartowski11/Hetzner

pkg:composer/chuckbartowski/hetzner-sdk

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-07-21 17:48 UTC

This package is auto-updated.

Last update: 2026-07-22 11:40:34 UTC


README

Hetzner SDK PHP

๐Ÿ”ด Hetzner SDK for PHP

A modern, fully typed PHP SDK for the three Hetzner APIs โ€” Cloud, Robot (dedicated servers) and DNS.

PHP Version Symfony Tests Coverage Packagist License

Servers ยท Volumes ยท Networks ยท Load Balancers ยท Firewalls ยท Dedicated servers ยท Rescue ยท vSwitch ยท DNS zones

Installation ยท Quick Start ยท API Reference ยท Error Handling

$action = $cloud->servers()->create('web01', 'cx22', 'ubuntu-24.04');
$cloud->actions()->waitFor($action);
$dns->records()->create($zoneId, 'A', 'web01', $serverIp);
$robot->boot()->activateRescue(321);

Framework-agnostic core โ€” usable from any PHP project, script, or worker โ€” with an optional bundle for first-class Symfony integration. Token/basic auth per API, typed exceptions, action waiters, automatic pagination, opt-in retries, typed models generated from the official OpenAPI spec, and a comment-free, strictly typed codebase (PHP 8.2+, declare(strict_types=1) everywhere).

Table of Contents

Features

๐Ÿงฉ Three APIs, one SDK Cloud (api.hetzner.cloud/v1, full coverage incl. every server action), Robot webservice (dedicated servers) and DNS (dns.hetzner.com) behind three matching facades
โณ Action-aware Every asynchronous Cloud operation returns an action โ€” actions()->waitFor() blocks until completion and fails loudly with the real error when the action fails
๐Ÿ“ฆ Smart responses items() auto-detects envelope keys, totalCount() reads meta.pagination, paginate() follows next_page through a generator, and typed models hydrate via as()/asList()
๐Ÿšจ Production-grade errors Hetzner error codes (not_found, locked, rate_limit_exceeded, resource_limit_exceededโ€ฆ) mapped to dedicated exceptions, opt-in automatic retries with backoff
๐Ÿ— Generated models 21 Cloud models generated from the official OpenAPI spec (tools/generate-models.py) + hand-written Robot and DNS models โ€” field names match the API exactly
๐Ÿ”“ Nothing sealed off Each client's get/post/put/delete accept any path, so an endpoint not wrapped by a module is one call away
๐Ÿ›  Framework-agnostic Only hard dependency is symfony/http-client; the optional Symfony bundle adds semantic config and autowiring
โœ… Fully unit-tested 27 tests against MockHttpClient โ€” no network required

Requirements

Dependency Version
PHP >= 8.2
Hetzner Cloud an API token (Console ยป Security ยป API tokens)
Hetzner Robot webservice credentials (Robot ยป Settings ยป Web service settings) โ€” optional
Hetzner DNS an API token (DNS Console ยป API tokens) โ€” optional
Symfony 6.4 LTS or 7.x โ€” optional, only for the bundle integration

Installation

The package is published on Packagist:

composer require chuckbartowski/hetzner-sdk

Quick Start (plain PHP)

No framework required โ€” build the clients you need and go:

use ChuckBartowski\HetznerSdk\Client\CloudClient;
use ChuckBartowski\HetznerSdk\Client\DnsClient;
use ChuckBartowski\HetznerSdk\Client\RobotClient;
use ChuckBartowski\HetznerSdk\HetznerCloud;
use ChuckBartowski\HetznerSdk\HetznerDns;
use ChuckBartowski\HetznerSdk\HetznerRobot;

$cloud = new HetznerCloud(new CloudClient(getenv('HCLOUD_TOKEN')));

$creation = $cloud->servers()->create('web01', 'cx22', 'ubuntu-24.04', [
    'ssh_keys' => ['my-key'],
    'location' => 'fsn1',
    'labels' => ['env' => 'prod'],
]);
$cloud->actions()->waitFor($creation);
$cloud->servers()->waitForStatus($creation->data('server')['id'], 'running');

$dns = new HetznerDns(new DnsClient(getenv('HETZNER_DNS_TOKEN')));
$dns->records()->create($zoneId, 'A', 'web01', $creation->data('server')['public_net']['ipv4']['ip']);

$robot = new HetznerRobot(new RobotClient(getenv('ROBOT_USER'), getenv('ROBOT_PASSWORD')));
$robot->reset()->execute(321, 'hw');

Client constructor signatures:

new CloudClient(string $token, float $timeout = 30.0, bool $retryFailed = false, int $maxRetries = 3, ?HttpClientInterface $httpClient = null);
new RobotClient(string $username, string $password, float $timeout = 30.0, bool $retryFailed = false, int $maxRetries = 3, ?HttpClientInterface $httpClient = null);
new DnsClient(string $token, float $timeout = 30.0, bool $retryFailed = false, int $maxRetries = 3, ?HttpClientInterface $httpClient = null);

Symfony Integration (optional)

Register the bundle:

// config/bundles.php
return [
    ChuckBartowski\HetznerSdk\HetznerSdkBundle::class => ['all' => true],
];

Then create config/packages/hetzner_sdk.yaml โ€” configure only the APIs you use:

hetzner_sdk:
    cloud:
        token: '%env(HCLOUD_TOKEN)%'
    robot:
        username: '%env(ROBOT_USER)%'
        password: '%env(ROBOT_PASSWORD)%'
    dns:
        token: '%env(HETZNER_DNS_TOKEN)%'
    retry_failed: true

Configuration reference

Key Type Default Description
cloud.token string '' Cloud API token, sent as Authorization: Bearer
robot.username / robot.password string '' Robot webservice credentials (HTTP basic auth)
dns.token string '' DNS API token, sent as Auth-API-Token
timeout float 30.0 Per-request timeout in seconds
retry_failed bool false Retry transient failures (429, 5xx) with exponential backoff
max_retries int 3 Maximum retry attempts when retry_failed is enabled

The HetznerCloud, HetznerRobot and HetznerDns facades are then autowirable. A facade whose credentials are missing throws an AuthenticationException on first use, before any network request.

Architecture

src/
โ”œโ”€โ”€ HetznerSdkBundle.php         Symfony bundle: config tree + service wiring (optional)
โ”œโ”€โ”€ HetznerCloud.php             HetznerRobot.php        HetznerDns.php
โ”œโ”€โ”€ Client/
โ”‚   โ”œโ”€โ”€ CloudClient.php          Bearer auth, pagination via meta.next_page, retries
โ”‚   โ”œโ”€โ”€ RobotClient.php          Basic auth, form-encoded bodies
โ”‚   โ””โ”€โ”€ DnsClient.php            Auth-API-Token, zone-file import/export support
โ”œโ”€โ”€ Response/
โ”‚   โ””โ”€โ”€ ApiResponse.php          Normalized response (+ action()/nextPage() helpers)
โ”œโ”€โ”€ Exception/
โ”‚   โ”œโ”€โ”€ HetznerSdkExceptionInterface.php   ApiException.php
โ”‚   โ”œโ”€โ”€ ResourceNotFoundException.php      ConflictException.php
โ”‚   โ”œโ”€โ”€ RateLimitException.php             ResourceLimitException.php
โ”‚   โ”œโ”€โ”€ AuthenticationException.php        TransportException.php
โ”œโ”€โ”€ Model/
โ”‚   โ”œโ”€โ”€ Cloud/                   21 models generated from the official OpenAPI spec
โ”‚   โ”œโ”€โ”€ Robot/                   DedicatedServer
โ”‚   โ””โ”€โ”€ Dns/                     Zone, Record
โ””โ”€โ”€ Api/
    โ”œโ”€โ”€ Cloud/                   Server, Volume, Image, Network, Firewall, FloatingIp,
    โ”‚                            PrimaryIp, LoadBalancer, Certificate, PlacementGroup,
    โ”‚                            SshKey, Action, Catalog
    โ”œโ”€โ”€ Robot/                   DedicatedServer, Reset, Boot, Rdns, Ip, Vswitch,
    โ”‚                            RobotFirewall, RobotKey
    โ””โ”€โ”€ Dns/                     Zone, Record

API Reference

Every method returns an ApiResponse and throws on failure (see Error Handling).

Cloud โ€” Servers

$cloud->servers() โ€” full lifecycle plus every documented server action:

Group Methods
CRUD list(query), find(id), create(name, serverType, image, options), update(id, fields), remove(id)
Power powerOn, powerOff, reboot, reset, shutdown
Provisioning rebuild(id, image), changeType(id, type, upgradeDisk), resetPassword, enableRescue(id, type, sshKeys) / disableRescue, createImage(id, options)
Backups & ISO enableBackup / disableBackup, attachIso(id, iso) / detachIso
Network attachToNetwork(id, networkId), detachFromNetwork, changeAliasIps, changeDnsPtr(id, ip, ptr)
Misc changeProtection(id, delete, rebuild), requestConsole, addToPlacementGroup / removeFromPlacementGroup, metrics(id, type, start, end), actions(id)
Waiter waitForStatus(id, 'running') โ€” polls until the server reaches the given status

All mutating calls return an action โ€” chain with $cloud->actions()->waitFor($response).

Cloud โ€” Volumes, Images & Placement Groups

$cloud->volumes() โ€” list, find, create(name, sizeGb) (attach at creation with ['server' => $id]), update, remove, attach(id, serverId, automount), detach, resize(id, sizeGb) (grow only), changeProtection. $cloud->images() โ€” list(query) (filter type=snapshot|backup|system), find, update (convert backup โ†’ snapshot), remove, changeProtection. $cloud->placementGroups() โ€” list, find, create(name, type) (spread), update, remove.

Cloud โ€” Networks & Firewalls

$cloud->networks() โ€” list, find, create(name, ipRange), update, remove, addSubnet(id, type, networkZone, ipRange), deleteSubnet, addRoute / deleteRoute, changeIpRange, changeProtection. $cloud->firewalls() โ€” list, find, create(name, rules), update, remove, setRules(id, rules) (replaces all rules), applyToResources(id, resources), removeFromResources.

Cloud โ€” Floating & Primary IPs

$cloud->floatingIps() โ€” list, find, create(type, options) (ipv4/ipv6), update, remove, assign(id, serverId), unassign, changeDnsPtr, changeProtection. $cloud->primaryIps() โ€” same surface with assign(id, assigneeId, assigneeType); primary IPs survive server deletion when auto_delete is false.

Cloud โ€” Load Balancers & Certificates

$cloud->loadBalancers() โ€” list, find, create(name, type, options), update, remove, metrics, services (addService, updateService, deleteService), targets (addTarget, removeTarget โ€” server, label selector or IP), changeAlgorithm, changeType, attachToNetwork / detachFromNetwork, enablePublicInterface / disablePublicInterface, changeDnsPtr, changeProtection. $cloud->certificates() โ€” list, find, createManaged(name, domainNames) (Let's Encrypt), upload(name, certificate, privateKey), update, remove, retry(id) (failed managed issuance).

Cloud โ€” SSH keys, Actions & Catalog

$cloud->sshKeys() โ€” list, find, create(name, publicKey, labels), update, remove. $cloud->actions() โ€” list(query), find(id), and the central waiter: waitFor(actionId | actionArray | ApiResponse, timeout, interval). $cloud->catalog() โ€” serverTypes(), serverType(id), loadBalancerTypes(), locations(), datacenters(), isos(), pricing().

Robot โ€” Dedicated servers

$robot->servers() โ€” list(), find(serverNumber), rename(serverNumber, name), cancellationStatus, cancel(serverNumber, date, reason), revokeCancellation.

Robot โ€” Reset, WOL & Boot

$robot->reset() โ€” list(), options(serverNumber), execute(serverNumber, type) (sw, hw, man), wake(serverNumber) (Wake-on-LAN). $robot->boot() โ€” status(serverNumber), rescue system (rescueStatus, activateRescue(serverNumber, os, sshKeyFingerprints), deactivateRescue) and Linux installation (linuxStatus, activateLinux(serverNumber, dist, lang, sshKeyFingerprints), deactivateLinux). Activation returns the one-time root password โ€” reboot to apply.

Robot โ€” IPs, rDNS & vSwitch

$robot->ips() โ€” list(?serverNumber), find(ip), updateTrafficWarnings, virtual MACs (macAddress, generateMac, deleteMac), subnets(?serverNumber), subnet(netIp). $robot->rdns() โ€” list(), find(ip), set(ip, ptr), remove(ip). $robot->vswitches() โ€” list(), find(id), create(name, vlan) (VLAN 4000โ€“4091), update, remove(id, cancellationDate), addServers(id, serverNumbers), removeServers.

Robot โ€” Firewalls & SSH keys

$robot->firewalls() โ€” per-server stateful firewall: find(serverNumber), apply(serverNumber, config), remove, plus templates (templates(), template(id), createTemplate(config)). $robot->keys() โ€” list(), find(fingerprint), create(name, data), rename, remove.

DNS โ€” Zones & Records

$dns->zones() โ€” list(query), find(id), create(name, ttl), update, remove, zone files (importZoneFile(id, contents), exportZoneFile(id), validateZoneFile(contents)). $dns->records() โ€” list(zoneId), find(id), create(zoneId, type, name, value, ?ttl), update(...), remove(id), bulkCreate(records), bulkUpdate(records).

$zone = $dns->zones()->create('example.com');
$dns->records()->bulkCreate([
    ['zone_id' => $zone->data('zone')['id'], 'type' => 'A', 'name' => '@', 'value' => '203.0.113.10'],
    ['zone_id' => $zone->data('zone')['id'], 'type' => 'A', 'name' => 'www', 'value' => '203.0.113.10'],
]);

Responses

All calls return an immutable ApiResponse. Here is what actually comes back from GET /v1/servers:

{
    "servers": [
        {
            "id": 42,
            "name": "web01",
            "status": "running",
            "server_type": { "name": "cx22" },
            "public_net": { "ipv4": { "ip": "203.0.113.10" } }
        }
    ],
    "meta": { "pagination": { "page": 1, "total_entries": 1, "next_page": null } }
}
$response = $cloud->servers()->list();

$response->success;              // bool
$response->statusCode;           // int
$response->data;                 // the decoded body above
$response->data('servers');      // keyed access with optional default
$response->items();              // the wrapped list, envelope auto-detected (meta ignored)
$response->first();              // first item or null
$response->totalCount();         // meta.pagination.total_entries
$response->action();             // the action array when the call started one, null otherwise
$response->errors;               // list<string>, e.g. 'invalid_input: name is invalid'

Typed models

21 Cloud models are generated from Hetzner's official OpenAPI spec (tools/generate-models.py), plus hand-written Robot and DNS models:

use ChuckBartowski\HetznerSdk\Model\Cloud\Server;
use ChuckBartowski\HetznerSdk\Model\Dns\Record;

$server = $cloud->servers()->find(42)->as(Server::class);
$server->name;          // ?string
$server->status;        // ?string 'running'
$server->publicNet;     // ?array  ['ipv4' => ['ip' => โ€ฆ]]
$server->raw;           // complete original payload, nothing lost

foreach ($dns->records()->list($zoneId)->asList(Record::class) as $record) {
    echo "{$record->name} {$record->type} {$record->value}", PHP_EOL;
}

Hydration is lossless and lenient: unknown fields stay in ->raw, malformed values degrade to null/[].

Pagination

foreach ($cloud->client()->paginate('/servers', itemsKey: 'servers') as $server) {
    echo $server['name'], PHP_EOL;
}

The Cloud paginator follows meta.pagination.next_page; the DNS paginator pages until a short page. Robot endpoints return complete lists.

Error Handling

All SDK exceptions implement HetznerSdkExceptionInterface. HTTP status and Hetzner's error codes drive the mapping:

Exception Thrown when Extras
ApiException The API reported a failure (module methods validate automatically) getErrors(), getStatusCode(), getRaw()
โ†ณ ResourceNotFoundException 404 or code not_found
โ†ณ ConflictException 409 or codes conflict, uniqueness_error, locked
โ†ณ RateLimitException 429 or code rate_limit_exceeded getRetryAfter(): ?int
โ†ณ ResourceLimitException code resource_limit_exceeded (project limits)
AuthenticationException Missing credentials or HTTP 401 thrown before any request when credentials are empty
TransportException Network error, TLS failure, timeout, or invalid JSON wraps the symfony/http-client exception
try {
    $cloud->actions()->waitFor($cloud->servers()->create('web01', 'cx22', 'ubuntu-24.04'));
} catch (ResourceLimitException) {
    $this->askForLimitIncrease();
} catch (RateLimitException $e) {
    $this->requeue(delaySeconds: $e->getRetryAfter() ?? 30);
}

Testing

The suite runs entirely offline against MockHttpClient:

composer install
vendor/bin/phpunit

Security Notes

  • Secrets are passed with #[\SensitiveParameter], so they never appear in stack traces.
  • Cloud tokens are per-project โ€” one read-write token per application, revocable individually. Robot webservice credentials are account-wide: store them in your vault and restrict which service uses them.
  • remove() on servers, volumes and load balancers is irreversible โ€” enable delete protection (changeProtection) on critical resources and gate destructive calls behind confirmation flows.
  • Robot cancel() schedules a real contract cancellation โ€” double-gate it.

WHMCS module

A ready-to-use WHMCS provisioning module ships in whmcs/modules/servers/hetznersdk/. It provisions a Hetzner Cloud server through this SDK โ€” create + wait for the action, suspend (power off), unsuspend, terminate, reboot.

Install

  1. composer require chuckbartowski/hetzner-sdk in your WHMCS root.
  2. Copy the hetznersdk folder into <whmcs>/modules/servers/.
  3. Add a server with Type: Hetzner Cloud (SDK) and your Cloud API token in the Access Hash field.
  4. Set the config options: Server type (cx22โ€ฆ), Image (ubuntu-24.04โ€ฆ), Location (fsn1โ€ฆ).

The server is named whmcs-<serviceid>, so the module finds it again on suspend/terminate. Creation blocks on the returned action via actions()->waitFor().

License

MIT