PHP SDK
PHP SDK
cipi/sdk is the PHP client for the Cipi panel API. From a Laravel app, a Symfony service, or a plain script, one Sanctum token calls the same REST surface as cipi-cli and the control panel: apps, deploys, domains, databases, and the server.
The SDK runs on your machine. It does not SSH into the VPS and it does not shell out to cipi. The server still needs the API package.
cipi api), then create a token with cipi api token create. Grant only the abilities this application needs.
Install
composer require cipi/sdk
Requires PHP 8.2+ with ext-curl and ext-json. Laravel is optional. Source: github.com/cipi-sh/sdk (MIT).
PHP SDK
Client
Pass the panel origin and the token. Both https://api.example.com and https://api.example.com/api work. The client always calls /api/*.
use Cipi\Sdk\Cipi; $cipi = Cipi::connect( baseUrl: 'https://api.example.com', token: getenv('CIPI_TOKEN') ?: '', ); $apps = $cipi->apps()->list();
Reads return the JSON body as an associative array. Creates, edits, deploys, SSL, and most database writes return 202 with job_id. Poll that id, or let wait() do it.
$job = $cipi->deploys()->start('shop'); $result = $cipi->wait($job);
Plain http:// is rejected. For a panel on your laptop, opt in:
$cipi = Cipi::connect('http://127.0.0.1:8080', $token, [ 'allow_http' => true, ]);
PHP SDK
Laravel
The service provider is discovered automatically on Laravel 11, 12, and 13. Publish the config if you want it in your app:
php artisan vendor:publish --tag=cipi-config
CIPI_BASE_URL=https://api.example.com CIPI_TOKEN= CIPI_TIMEOUT=30 CIPI_ALLOW_HTTP=false
use Cipi\Sdk\Laravel\Facades\Cipi; $apps = Cipi::apps()->list();
You can also type-hint Cipi\Sdk\Cipi in a controller or job. The container holds one server. For a second server, call Cipi::connect() with that server's URL and token — do not reuse the facade.
Symfony, Slim, and other frameworks use the same Cipi::connect() call and register the instance in their own container. There is no extra bundle.
PHP SDK
Apps
App creates accept the same fields as POST /api/apps: user and domain are required. Add repository and branch for Git, php, custom, octane, engine, or the Node fields node, framework, node_version, build, start, output, and health_path.
$cipi->apps()->list(); $cipi->apps()->get('shop'); $cipi->wait($cipi->apps()->create([ 'user' => 'shop', 'domain' => 'shop.example.com', 'repository' => 'git@github.com:acme/shop.git', 'branch' => 'main', 'php' => '8.4', ])); $cipi->apps()->update('shop', ['php' => '8.4']); $cipi->apps()->suspend('shop'); $cipi->apps()->unsuspend('shop'); $cipi->apps()->fixPermissions('shop'); $cipi->apps()->recreateWebhook('shop', rotateSecret: true); $cipi->apps()->delete('shop');
HTTP Basic Auth, Artisan, and whitelisted app commands:
$cipi->basicAuth()->enable('shop', 'admin'); $cipi->basicAuth()->disable('shop'); $cipi->wait($cipi->artisan()->run('shop', 'migrate --force')); $cipi->run()->commands(); $cipi->wait($cipi->run()->execute('shop', 'composer install --no-dev'));
artisan() and run() are async jobs. The panel rejects tinker and interactive programs. logs()->get($app, $type, $page, $perPage) reads nginx, php, worker, deploy, or laravel (or omit the type for all).
PHP SDK
Deploy and jobs
$cipi->wait($cipi->deploys()->start('shop')); $cipi->deploys()->rollback('shop'); $cipi->deploys()->unlock('shop'); $cipi->deploys()->audit('shop', days: 30); $cipi->deployConfig()->get('shop'); $cipi->deployConfig()->update('shop', [ 'keep_releases' => 5, 'migrate' => true, ]);
wait() polls until completed or failed. The default timeout is 300 seconds. A failed job throws Cipi\Sdk\Exception\JobFailedException and keeps the job payload on $e->job. Pass a job id or the 202 body:
$cipi->jobs()->get('9f1c…'); $cipi->jobs()->wait($job, timeoutSeconds: 120, intervalSeconds: 1.0);
Deploy config edits the structured Deployer options and regenerates deploy.php. It does not upload a raw PHP file. The audit call is synchronous and reads the hash-chained ledger (Cipi 5.4.0+).
PHP SDK
Domains and SSL
$cipi->aliases()->add('shop', 'www.shop.example.com'); $cipi->www()->forceToRoot('shop'); $cipi->redirects()->set('shop', 'https://example.com', 301, keepPath: true); $cipi->redirects()->add('shop', '/blog/', 'https://blog.example.com', 301, true); $cipi->redirects()->remove('shop', '/blog/'); $cipi->proxies()->add( 'shop', prefix: '/api/', upstream: 'http://127.0.0.1:3000', stripPrefix: true, ); $cipi->ssl()->install('shop'); $cipi->ssl()->force('shop');
Redirect and proxy writes are synchronous on the panel: Nginx is tested and the previous vhost is restored if the test fails. The proxy loopback guard cannot be bypassed from the API. Node apps use node()->runtimes(), node()->status($app), and node()->restart($app) (restart is a job).
PHP SDK
Env, auth, and databases
env() merges keys. It does not replace the file. authJson() is the shared Composer auth.json, not HTTP Basic Auth. Both need Cipi 5.0.3+ and the matching token ability (apps-env, apps-auth).
$cipi->env()->update('shop', [ 'APP_ENV' => 'production', ], ['TELESCOPE_ENABLED']); $cipi->authJson()->create('shop'); $cipi->authJson()->replace('shop', [ 'http-basic' => [ 'repo.example.com' => ['username' => 'token', 'password' => $secret], ], ]);
$cipi->databases()->engines(); $cipi->databases()->list('pgsql'); $cipi->wait($cipi->databases()->create('shop', 'pgsql')); $cipi->databases()->backup('shop'); $cipi->databases()->restore('shop', '/home/cipi/backups/shop.sql.gz'); $cipi->databases()->password('shop');
engine is mariadb or pgsql. Omit it to use the server default. Backup, restore, and password regeneration are jobs. The restore path has to be a .sql.gz file already on the server.
PHP SDK
Server
$cipi->server()->status(); $cipi->php()->list(); $cipi->php()->install('8.4'); $cipi->ssh()->add($publicKey); $cipi->ssh()->remove(2); $cipi->services()->list(); $cipi->services()->restart('php8.4-fpm'); $cipi->smtp()->get(); $cipi->health()->update('shop', 'https://shop.example.com/health', 200); $cipi->health()->check('shop'); $cipi->search()->enable('shop'); $cipi->packages()->list(); $cipi->monitor()->list(); $cipi->zeroTrust()->status();
Packages, the system monitor, and Cloudflare Zero Trust are read-only here, matching the panel API. Installing those pieces stays on the host CLI.
The API allowlist defaults to *. Restrict it without locking yourself out — ensure_client_ip: true keeps the caller on the list:
$cipi->ipWhitelist()->replace(['203.0.113.10'], ensureClientIp: true); $cipi->ipWhitelist()->add('203.0.113.11'); $cipi->ipWhitelist()->allowAll();
An endpoint this release does not wrap yet is still reachable:
$cipi->request('GET', '/apps/shop');
PHP SDK
Security
- HTTPS and certificate verification are on. Redirects are not followed, so the Bearer token stays on the host you configured.
- The token is sent only in the
Authorizationheader. A value that contains whitespace or control characters is rejected, which blocks header injection. - The token is stripped from exception messages, decoded bodies, and
var_dump/print_routput. GETmay retry429and503whenRetry-Afteris at most 10 seconds. Writes are never retried.- Responses larger than 16 MiB are refused. Timeouts default to 30 seconds (10 seconds to connect).
The third argument of Cipi::connect() changes those limits. On Laravel the package config reads CIPI_TIMEOUT, CIPI_CONNECT_TIMEOUT, CIPI_MAX_RETRIES, and CIPI_ALLOW_HTTP. A value out of range throws before any request is sent.
$cipi = Cipi::connect($url, $token, [ 'timeout' => 30, // 1–300 seconds 'connect_timeout' => 10, // 1–60 seconds 'max_retries' => 2, // 0–5, reads only 'max_response_bytes' => 16777216, // 1 KiB–64 MiB 'allow_http' => false, ]);
Keep CIPI_TOKEN in the environment of the PHP host. Do not commit it. Create one token per application, with the smallest ability set that still works (apps-view, deploy-manage, dbs-create, …). A missing ability is HTTP 403, not a silent no-op.
PHP SDK
Errors
HTTP failures throw Cipi\Sdk\Exception\CipiException or a subclass. The status, the JSON body, the method, and the path are on the exception. The token is not.
| Status | Exception |
|---|---|
| 401 | AuthenticationException |
| 403 | AuthorizationException — missing ability, or IP whitelist |
| 404 | NotFoundException |
| 409 | ConflictException |
| 422 | ValidationException — errors() when the panel returned a Laravel bag |
| 429 | RateLimitException |
| 5xx | ServerException |
Without an HTTP status: TransportException when the connection or the TLS handshake fails, or a response is over the size limit; ConfigurationException for an invalid URL, token, path, or option, before anything is sent. wait() adds JobFailedException when the job ends failed and TimeoutException when it is still running at the timeout. RateLimitException carries retryAfter in seconds when the panel sent it.
use Cipi\Sdk\Exception\AuthorizationException; use Cipi\Sdk\Exception\JobFailedException; try { $cipi->wait($cipi->deploys()->start('shop')); } catch (JobFailedException $e) { $job = $e->job; } catch (AuthorizationException $e) { // token is missing deploy-manage }