beziworld / laravel-hestiacp
Laravel client for the Hestia Control Panel API: users, web domains, DNS, mail, databases, cron, backups and Let's Encrypt.
Requires
- php: ^8.3
- ext-json: *
- guzzlehttp/guzzle: ^7.8
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- phpstan/phpstan-strict-rules: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-10 21:15:34 UTC
README
Laravel client for the Hestia Control Panel API: users and packages, web domains with aliases, redirects and FTP accounts, Let's Encrypt and SSL settings, DNS zones and records, mail domains, accounts, aliases, forwards, catch-all and DKIM, databases, cron jobs, backups and server information, for one or many servers.
Features
- Access keys (
access_key+secret_key, orhash=ACCESS:SECRET) as recommended since Hestia 1.6, plus the deprecated API key and admin password methods for older servers - Exit code handling for both methods: with access keys the
Hestia-Exit-Codeheader, with the legacy methodsreturncode=yesfor changes and theError:prefix for reads, because those answer HTTP 200 for failed commands - Typed
CommandExceptionwith the exit code and its name (E_EXISTS,E_NOTEXIST,E_LIMIT, ...) and helpers such asalreadyExists()andnotFound() - Typed results (
User,WebDomain,DnsDomain,DnsRecord,MailDomain,MailAccount,Database,CronJob,Backup) built from the JSON output of thev-list-*commands, with dates in the server time zone - Read commands (
v-list-*,v-get-*,v-search-*) are repeated once after a connection error or HTTP 5xx; every other command is sent exactly once - Multiple named connections plus runtime connections with
HestiaCP::using([...]) query()andexecute()for any of the 400+v-*commands,php artisan hestiacp:check- PHP 8.3+, Laravel 12 and 13, PHPStan level 8
Why this package
The PHP clients on Packagist (gigabait/hestiacp-api, neto737/hestiacp-api, ponasromas/hestiacp-api and
forks) and the two Laravel packages (teaminfinitylk/laravel-hestiacp-sdk, 14 downloads, and
kitteam/hestia-api, 3 downloads, in November 2026) send commands but leave the different answers of the
authentication methods to you: with the deprecated methods a failed command is still HTTP 200. This package
follows the API source (web/api/index.php) and the official return code table, maps the keyed JSON output of the
list commands to data objects and adds Laravel configuration, Http::fake() support for your own tests and an
Artisan check.
Installation
composer require beziworld/laravel-hestiacp
php artisan vendor:publish --tag=hestiacp-config # optional
HESTIACP_URL=https://panel.example.com:8083 # /api/ is appended HESTIACP_ACCESS_KEY=AKIA0123456789ABCDEF HESTIACP_SECRET_KEY=your-40-character-secret-key HESTIACP_TIMEZONE=Europe/Warsaw # server time zone for DATE and TIME values HESTIACP_VERIFY_SSL=true
php artisan hestiacp:check
Create the access key as the admin user in the panel or with v-add-access-key admin '*' laravel json on the
server, limit it to the commands you need when possible, and add the address of your application to the allowed
IP addresses of the API in the server settings.
Usage examples
Provision a hosting account after an order is paid
use Beziworld\HestiaCP\Exceptions\CommandException; use Beziworld\HestiaCP\Facades\HestiaCP; class ProvisionHosting implements ShouldQueue { public function handle(): void { $user = $this->order->username; try { HestiaCP::users()->create($user, $password = Str::password(16), $this->order->email, package: 'business'); HestiaCP::domains()->create($user, $this->order->domain); HestiaCP::dns()->create($user, $this->order->domain, config('hosting.ip')); HestiaCP::email()->createDomain($user, $this->order->domain); HestiaCP::email()->createAccount($user, $this->order->domain, 'office', Str::password(16), quotaMb: 2048); HestiaCP::ssl()->letsEncrypt($user, $this->order->domain, ['www.' . $this->order->domain]); } catch (CommandException $e) { // e.g. "Hestia v-add-user failed (E_EXISTS): user john exists" $this->fail($e); return; } Mail::to($this->order->email)->send(new HostingReady($this->order, $password)); } }
Users
$users = HestiaCP::users()->all(); // list<User> $user = HestiaCP::users()->get('john'); // ?User, null when it does not exist HestiaCP::users()->suspend('john'); HestiaCP::users()->unsuspend('john'); HestiaCP::users()->changePackage('john', 'premium'); HestiaCP::users()->changePassword('john', $newPassword); HestiaCP::users()->packages(); // packages with their limits HestiaCP::users()->delete('john'); // with all domains, mail and databases
Web domains, FTP and Let's Encrypt
HestiaCP::domains()->create('john', 'example.com'); // default IP, www alias HestiaCP::domains()->addAlias('john', 'example.com', ['shop.example.com']); HestiaCP::domains()->changeBackendTemplate('john', 'example.com', 'PHP-8_3'); HestiaCP::domains()->redirect('john', 'example.net', 'https://example.com'); foreach (HestiaCP::domains()->all('john') as $domain) { // $domain->domain, $domain->aliases, $domain->ssl, $domain->letsEncrypt, $domain->diskUsedMb } HestiaCP::ftp()->create('john', 'example.com', 'deploy', $password, 'public_html'); // becomes john_deploy HestiaCP::ssl()->letsEncrypt('john', 'example.com', ['www.example.com']); HestiaCP::ssl()->letsEncrypt('john', 'example.com', mail: true); // mail.example.com HestiaCP::ssl()->forceHttps('john', 'example.com'); HestiaCP::ssl()->certificate('john', 'example.com'); // NOT_AFTER, ISSUER, ...
DNS
HestiaCP::dns()->create('john', 'example.com', '192.0.2.10', ['ns1.example.com', 'ns2.example.com']); HestiaCP::dns()->addRecord('john', 'example.com', 'www', 'A', '192.0.2.10'); HestiaCP::dns()->addRecord('john', 'example.com', '@', 'MX', 'mail.example.com.', priority: 10); HestiaCP::dns()->addRecord('john', 'example.com', '_dmarc', 'TXT', 'v=DMARC1; p=quarantine', ttl: 300); $mx = HestiaCP::dns()->findRecords('john', 'example.com', '@', 'MX'); HestiaCP::dns()->updateRecord('john', 'example.com', $mx[0]->id, '@', 'MX', 'mail2.example.com.', priority: 20); HestiaCP::dns()->deleteRecord('john', 'example.com', $mx[0]->id);
$mail = HestiaCP::email(); $mail->createDomain('john', 'example.com', antispam: true, antivirus: true, dkim: true); $mail->createAccount('john', 'example.com', 'office', $password, quotaMb: 2048); // null: unlimited $mail->changeQuota('john', 'example.com', 'office', 4096); $mail->addAlias('john', 'example.com', 'office', 'sales'); $mail->addForward('john', 'example.com', 'office', 'boss@example.net'); $mail->setCatchAll('john', 'example.com', 'office@example.com'); $mail->dkimRecord('john', 'example.com'); // ['name' => 'mail._domainkey', 'ttl' => 3600, 'value' => 'v=DKIM1; ...'] $mail->accounts('john', 'example.com'); // list<MailAccount>
Databases, cron and backups
HestiaCP::databases()->create('john', 'shop', 'shop', $password); // creates john_shop with user john_shop HestiaCP::databases()->changePassword('john', 'john_shop', $rotated); HestiaCP::cron()->create('john', '* * * * *', 'php /home/john/web/example.com/app/artisan schedule:run'); HestiaCP::backups()->schedule('john'); // queued, the user is notified by e-mail $latest = HestiaCP::backups()->latest('john'); // ?Backup HestiaCP::backups()->restore('john', $latest->file, web: 'example.com', dns: '', mail: '', databases: '*', cron: '', userDirectories: '');
Several servers
// config/hestiacp.php 'connections' => [ 'default' => [...], 'node2' => ['url' => env('HESTIA2_URL'), 'hash' => env('HESTIA2_HASH')], // ACCESS:SECRET ], HestiaCP::connection('node2')->users()->all(); HestiaCP::using(['url' => $server->url, 'hash' => decrypt($server->api_hash)])->system()->info();
Anything else
HestiaCP::query('v-list-user-ips', ['john']); // "json" is appended HestiaCP::execute('v-change-user-shell', ['john', 'bash']); HestiaCP::system()->stoppedServices();
Exceptions
All exceptions extend HestiaCPException:
| Exception | When |
|---|---|
AuthenticationException |
Credentials refused (exit code 9, E_PASSWORD, or HTTP 401 without an exit code). Never retried. Extends CommandException. |
CommandException |
A command failed or the API could not be reached: command(), exitCode(), errorName(), httpStatus(), output(), notFound(), alreadyExists(), forbidden(), limitReached() |
RateLimitException |
HTTP 429 from a proxy in front of the panel, retryAfter() in seconds |
InvalidConfiguration |
Unknown connection, missing URL / credentials or an invalid time zone |
Notes
- Commands run synchronously on the server and some (user creation, Let's Encrypt, backups) take several seconds: run them in queued jobs. The default timeout is 120 seconds.
- Hestia prefixes databases, database users and FTP accounts with the user name.
- Keys of non-admin users only work when the API is enabled for all users (
API_SYSTEM= 2) and only for their own objects.
Development
composer install
composer ci # pint --test, phpstan (level 8), pest
Author
Created and maintained by Kamil Ćwiertnia, beziworld.eu.
- E-mail: biuro@beziworld.eu
- Issues and feature requests: GitHub issues
- Security vulnerabilities: see SECURITY.md
Need an integration? Get in touch through beziworld.eu.
Other packages by the author
- beziworld/laravel-ispconfig: ISPConfig 3 remote API
- beziworld/laravel-mailcow: mailcow mail server
- beziworld/laravel-directadmin: DirectAdmin
License
MIT. See LICENSE.md. This package is not affiliated with the Hestia Control Panel project or its maintainers.