chuckbartowski / plesk-sdk
PHP SDK for the Plesk REST API (v2) with typed clients, high-level modules and a CLI bridge
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 PleskSdkBundle in a Symfony application
- symfony/dependency-injection: To register PleskSdkBundle in a Symfony application
- symfony/http-kernel: To register PleskSdkBundle in a Symfony application
README
🌐 Plesk SDK for PHP
A modern, fully typed PHP SDK for the Plesk REST API (v2), with a built-in CLI bridge.
Domains · Customers · Mail · DNS · Databases · FTP · Extensions · API keys · CLI bridge
Installation · Quick Start · API Reference · Error Handling
$plesk->domains()->create('example.com', ['hosting_type' => 'virtual']); $plesk->mail()->create('contact@example.com', 'S3cure!Pass'); $plesk->cli()->call('subscription', ['--info', 'example.com']);
The CLI bridge makes every plesk bin utility callable over HTTPS — the SDK uses it transparently where REST has gaps (mailboxes, suspensions, services). Framework-agnostic core with an optional Symfony bundle, API-key or basic auth, typed exceptions, comment-free strictly typed codebase (PHP 8.2+).
Table of Contents
- Features
- Requirements
- Installation
- Quick Start (plain PHP)
- Symfony Integration (optional)
- Architecture
- Authentication
- API Reference
- Responses
- Error Handling
- Testing
- Security Notes
- WHMCS module
- License
Features
- Full REST API v2 coverage: server, clients, domains, DNS, databases, FTP users, extensions, API keys.
- CLI bridge (
POST /cli/{command}/call): everyplesk binutility becomes callable over HTTPS — the SDK uses it transparently where REST has gaps (mailboxes, suspend/activate, services) and exposes it directly for everything else. - Framework-agnostic: one plain facade (
Plesk) you can instantiate anywhere; the only hard dependency issymfony/http-client, a standalone component that works in any PHP project. - Two auth modes:
X-API-Keyheader (recommended) or HTTP basic auth — plus a helper to mint API keys from credentials. - A single normalized response object (
ApiResponse) with status code, data accessor, error list, andstdout()/stderr()helpers for CLI calls. - Typed exception hierarchy under one marker interface, so you can catch narrowly or broadly.
- Optional Symfony bundle with semantic configuration and autowirable services.
- Fully unit-tested against
MockHttpClient(no network required).
Requirements
| Dependency | Version |
|---|---|
| PHP | >= 8.2 |
| Plesk | Obsidian 18.x (REST API v2 enabled by default) |
| Symfony | 6.4 LTS or 7.x — optional, only for the bundle integration |
Installation
The package is published on Packagist:
composer require chuckbartowski/plesk-sdk
Quick Start (plain PHP)
No framework required — build the client and go:
use ChuckBartowski\PleskSdk\Client\PleskClient; use ChuckBartowski\PleskSdk\Plesk; $plesk = new Plesk(new PleskClient( host: 'plesk.example.com', apiKey: getenv('PLESK_API_KEY'), )); $plesk->domains()->create('example.com', [ 'hosting_type' => 'virtual', 'hosting_settings' => ['ftp_login' => 'deploy', 'ftp_password' => 'S3cret!'], ]); $plesk->mail()->create('contact@example.com', 'S3cure!Pass'); $plesk->dns()->addRecord('example.com', 'A', 'www', '203.0.113.10');
Client constructor signature:
new PleskClient( string $host, string $apiKey = '', // preferred string $login = '', // fallback: basic auth pair string $password = '', int $port = 8443, bool $verifySsl = true, float $timeout = 30.0, ?HttpClientInterface $httpClient = null, // inject your own (retries, proxy, mock…) );
Symfony Integration (optional)
Register the bundle:
// config/bundles.php return [ ChuckBartowski\PleskSdk\PleskSdkBundle::class => ['all' => true], ];
Then create config/packages/plesk_sdk.yaml:
plesk_sdk: host: '%env(PLESK_HOST)%' api_key: '%env(PLESK_API_KEY)%' port: 8443 verify_ssl: true timeout: 30
# .env.local PLESK_HOST=plesk.example.com PLESK_API_KEY=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
Configuration reference
| Key | Type | Default | Description |
|---|---|---|---|
host |
string | required | Plesk server hostname (no scheme, no port) |
api_key |
string | '' |
API key sent as X-API-Key (preferred) |
login / password |
string | '' |
Basic auth pair, used when api_key is empty |
port |
int | 8443 |
Plesk panel TLS port |
verify_ssl |
bool | true |
TLS peer/host verification |
timeout |
float | 30.0 |
Per-request timeout in seconds |
The Plesk facade is then autowirable in controllers, services, commands, and message handlers. The bundle reuses your application's http_client service when available and falls back to a native client otherwise.
Architecture
src/
├── PleskSdkBundle.php Symfony bundle: config tree + service wiring (optional)
├── Plesk.php Facade: entry point for all modules
├── Client/
│ └── PleskClient.php REST transport, auth, JSON handling, CLI bridge
├── Response/
│ └── ApiResponse.php Immutable normalized response (+ stdout/stderr helpers)
├── Exception/
│ ├── PleskSdkExceptionInterface.php
│ ├── ApiException.php API answered but reported a failure
│ ├── AuthenticationException.php
│ └── TransportException.php Network / TLS / timeout / invalid JSON
└── Api/
├── AbstractApi.php
├── ServerApi.php CustomerApi.php DomainApi.php DnsApi.php
├── DatabaseApi.php FtpApi.php MailApi.php ExtensionApi.php
└── AuthApi.php CliApi.php
Design decisions:
- Facade + lazy modules:
Pleskinstantiates each module on first use and caches it. - Modules always validate: every module method calls
ensureSuccess()internally and throwsApiExceptionon failure. To inspect a failed response without an exception, drop down to the client level. - CLI exit codes are failures: a CLI call returning a non-zero
codeproduces a failed response withstderras the error, even though HTTP said 200. - Nothing is sealed off: the client's
get/post/put/delete/climethods accept any path, so a REST endpoint added tomorrow is usable today.
Authentication
Two modes, checked in order:
- API key (recommended) — sent as
X-API-Key. Create one from credentials once, then store only the key:
$bootstrap = new Plesk(new PleskClient('plesk.example.com', login: 'admin', password: $password)); $key = $bootstrap->auth()->createKey(description: 'provisioning worker')->data('key');
- Basic auth —
login/password, used when no API key is configured.
Missing credentials throw an AuthenticationException immediately, before any network request.
API Reference
Every method returns an ApiResponse and throws on failure (see Error Handling). Methods marked CLI go through the CLI bridge rather than a REST endpoint.
Server
$plesk->server()
| Method | Endpoint |
|---|---|
info() |
GET /server |
ips() |
GET /server/ips |
addIp(string $ip, string $netmask, string $interface, string $type = 'shared') |
POST /server/ips |
deleteIp(string $ip) |
DELETE /server/ips/{ip} |
serviceStatus() |
CLI service --status |
restartService(string $service) |
CLI service --restart |
Customers
$plesk->customers() — client accounts (the people who own subscriptions).
| Method | Endpoint |
|---|---|
list() / find(int $id) |
GET /clients, GET /clients/{id} |
create(string $name, string $login, string $password, string $email, array $options = []) |
POST /clients |
update(int $id, array $properties) |
PUT /clients/{id} |
remove(int $id) |
DELETE /clients/{id} |
domains(int $id) |
GET /clients/{id}/domains |
statistics(int $id) |
GET /clients/{id}/statistics |
suspend(string $login) / activate(string $login) |
CLI customer --off/--on |
$plesk->customers()->create('ACME Corp', 'acme', 'S3cret!', 'billing@acme.test'); $plesk->customers()->suspend('acme');
Domains
$plesk->domains()
| Method | Endpoint |
|---|---|
list(array $filters = []) / find(int $id) |
GET /domains, GET /domains/{id} |
create(string $name, array $options = []) |
POST /domains |
update(int $id, array $properties) |
PUT /domains/{id} |
remove(int $id) |
DELETE /domains/{id} |
owner(int $id) |
GET /domains/{id}/client |
status(int $id) |
GET /domains/{id}/status |
suspend(string $name) / activate(string $name) |
CLI domain --off/--on |
create() options follow the REST schema: hosting_type (virtual, standard_forwarding, frame_forwarding, none), hosting_settings, owner_client ({id} or {login}), plan ({name}), ipv4/ipv6.
$plesk->domains()->create('customer1.com', [ 'hosting_type' => 'virtual', 'hosting_settings' => ['ftp_login' => 'c1', 'ftp_password' => 'S3cret!'], 'owner_client' => ['login' => 'acme'], 'plan' => ['name' => 'Default Domain'], ]);
DNS
$plesk->dns()
| Method | Endpoint |
|---|---|
records(?string $domain = null) |
GET /dns/records |
addRecord(string $domain, string $type, string $host, string $value, ?int $ttl = null, ?string $opt = null) |
POST /dns/records |
deleteRecord(int $id) |
DELETE /dns/records/{id} |
enableZone(string $domain) / disableZone(string $domain) |
CLI dns --on/--off |
opt carries the type-specific extra (MX priority, SRV weight…). Records are addressed by numeric id — fetch them first.
Databases
$plesk->databases()
| Method | Endpoint |
|---|---|
list(?string $domain = null) |
GET /databases |
create(string $name, string $domain, string $type = 'mysql', ?int $serverId = null) |
POST /databases |
remove(int $id) |
DELETE /databases/{id} |
users(?int $databaseId = null) |
GET /dbusers |
createUser(string $login, string $password, int $databaseId) |
POST /dbusers |
removeUser(int $id) |
DELETE /dbusers/{id} |
FTP
$plesk->ftp() — additional FTP users: list(), create(name, password, domain, ?home), update(name, properties), remove(name) over GET|POST|PUT|DELETE /ftpusers.
Mail (CLI bridge)
$plesk->mail() — REST v2 has no mail endpoints, so this module drives the mail CLI utility.
| Method | CLI equivalent |
|---|---|
create(string $address, string $password, bool $mailbox = true) |
mail --create -passwd -mailbox |
remove(string $address) |
mail --remove |
info(string $address) |
mail --info |
changePassword(string $address, string $password) |
mail --update -passwd |
setMailboxQuota(string $address, string $quota) |
mail --update -mbox_quota (e.g. '1G', '500M') |
addAlias(...) / removeAlias(...) |
mail --update -aliases add:/del: |
enableForwarding(string $address, string $destination) / disableForwarding(...) |
mail --update -forwarding |
$plesk->mail()->create('support@example.com', 'S3cure!Pass'); $plesk->mail()->setMailboxQuota('support@example.com', '2G');
Extensions
$plesk->extensions() — list(), install(idOrUrl), uninstall(id) over /extensions; enable(id) / disable(id) via CLI extension.
API keys
$plesk->auth() — createKey(?ip, ?ttl, ?description, ?login) (POST /auth/keys, requires basic auth) and deleteKey(key).
CLI (generic bridge)
$plesk->cli() — the raw escape hatch to every plesk bin utility.
| Method | Endpoint |
|---|---|
commands() |
GET /cli/commands |
reference(string $command) |
GET /cli/{command}/ref |
call(string $command, array $params = [], array $env = []) |
POST /cli/{command}/call |
$plesk->cli()->call('subscription', ['--info', 'customer1.com']); $plesk->cli()->call('php_handler', ['--list']);
Responses
All calls return an immutable ApiResponse:
$response = $plesk->domains()->list(); $response->success; // bool — 2xx and, for CLI calls, exit code 0 $response->statusCode; // int $response->data; // mixed — decoded JSON body (null on 204) $response->data('id'); // keyed access with optional default $response->errors; // list<string> $response->raw; // complete decoded payload $response->stdout(); // CLI calls: trimmed standard output $response->stderr(); // CLI calls: trimmed standard error
Error Handling
All SDK exceptions implement PleskSdkExceptionInterface, so a single catch covers everything:
| Exception | Thrown when | Extras |
|---|---|---|
ApiException |
The API answered but reported a failure (module methods validate automatically) | getErrors(), getStatusCode(), getRaw() |
AuthenticationException |
Credentials are missing, or the server answered HTTP 401/403 | thrown before any request when credentials are empty |
TransportException |
Network error, TLS failure, timeout, or a non-JSON response body | wraps the underlying symfony/http-client exception |
use ChuckBartowski\PleskSdk\Exception\ApiException; use ChuckBartowski\PleskSdk\Exception\PleskSdkExceptionInterface; try { $plesk->domains()->create('example.com', ['hosting_type' => 'virtual']); } catch (ApiException $e) { $this->logger->warning('Plesk rejected the domain', ['errors' => $e->getErrors()]); } catch (PleskSdkExceptionInterface $e) { throw new ProvisioningUnavailableException(previous: $e); }
To inspect a failed response without exceptions, use the client directly — client-level methods return the response as-is:
$response = $plesk->client()->post('/domains', $payload); if (!$response->success) { // $response->statusCode, $response->errors, $response->raw }
Testing
The suite runs entirely offline against MockHttpClient:
composer install vendor/bin/phpunit
To test your own services, inject a PleskClient built with a mock:
use ChuckBartowski\PleskSdk\Client\PleskClient; use ChuckBartowski\PleskSdk\Plesk; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\JsonMockResponse; $http = new MockHttpClient(new JsonMockResponse(['id' => 1])); $plesk = new Plesk(new PleskClient('host', apiKey: 'key', httpClient: $http));
Security Notes
- Secrets (
apiKey,password) are passed with#[\SensitiveParameter], so they never appear in stack traces. - Prefer API keys over basic auth: they can be scoped to an IP (
createKey(ip: ...)), given a TTL, and revoked individually. - Keep credentials in
.env.localor your secret vault — never commit them. - The CLI bridge runs commands as root on the server — treat any code path that reaches
cli()with the same care as SSH access. - Leave
verify_ssl: truein production; the option exists solely for self-signed panels. remove()on domains, customers and databases is irreversible — gate destructive calls behind confirmation flows in your application.
WHMCS module
A ready-to-use WHMCS provisioning module ships in whmcs/modules/servers/plesksdk/. It provisions Plesk subscriptions through this SDK — create a domain with hosting, suspend/unsuspend, terminate, and change the web-user password.
Install
composer require chuckbartowski/plesk-sdkin your WHMCS root.- Copy the
plesksdkfolder into<whmcs>/modules/servers/. - Add a server with Type: Plesk (SDK), the Plesk hostname, and your Plesk API key in the Access Hash field (or username/password as fallback).
- Set the Service Plan config option to the Plesk plan name.
| Operation | Plesk API |
|---|---|
| Create | REST POST /domains (virtual hosting) |
| Suspend / Unsuspend | CLI domain --off / --on |
| Terminate | CLI domain --remove |
| Change password | CLI site --update-web-user |
License
MIT