chuckbartowski / hetzner-sdk
PHP SDK for the Hetzner Cloud, Robot and DNS APIs with typed clients, models and high-level modules
Requires
- php: >=8.2
- symfony/http-client: ^6.4|^7.0
- symfony/http-client-contracts: ^3.0
Requires (Dev)
- phpunit/phpunit: ^10.5|^11.0
- symfony/config: ^6.4|^7.0
- symfony/dependency-injection: ^6.4|^7.0
- symfony/http-kernel: ^6.4|^7.0
Suggests
- symfony/config: To register HetznerSdkBundle in a Symfony application
- symfony/dependency-injection: To register HetznerSdkBundle in a Symfony application
- symfony/http-kernel: To register HetznerSdkBundle in a Symfony application
README
๐ด Hetzner SDK for PHP
A modern, fully typed PHP SDK for the three Hetzner APIs โ Cloud, Robot (dedicated servers) and DNS.
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
- Requirements
- Installation
- Quick Start (plain PHP)
- Symfony Integration (optional)
- Architecture
- API Reference
- Cloud โ Servers
- Cloud โ Volumes, Images & Placement Groups
- Cloud โ Networks & Firewalls
- Cloud โ Floating & Primary IPs
- Cloud โ Load Balancers & Certificates
- Cloud โ SSH keys, Actions & Catalog
- Robot โ Dedicated servers
- Robot โ Reset, WOL & Boot
- Robot โ IPs, rDNS & vSwitch
- Robot โ Firewalls & SSH keys
- DNS โ Zones & Records
- Responses
- Error Handling
- Testing
- Security Notes
- WHMCS module
- License
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
composer require chuckbartowski/hetzner-sdkin your WHMCS root.- Copy the
hetznersdkfolder into<whmcs>/modules/servers/. - Add a server with Type: Hetzner Cloud (SDK) and your Cloud API token in the Access Hash field.
- 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