Skip to content

Repository files navigation

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

About

Modern PHP SDK for the Hetzner Cloud, Robot and DNS APIs

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages