Search by

cipi / sdk

cipi

PHP SDK for the Cipi panel API. Call every cipi.sh REST endpoint from PHP, Laravel, and other frameworks with a Sanctum token.

1.2.0 2026-10-15 08:30 UTC

This package is auto-updated.

Last update: 2026-10-15 08:30:37 UTC


README

PHP client for the Cipi panel API. One Sanctum token is enough to manage apps, deploys, domains, databases, and the server from PHP, Laravel, Symfony, or any other framework.

The package talks only to the REST API (/api/*). It does not open SSH and it does not shell out to cipi on the machine where your code runs.

Documentation: cipi.sh/docs/php-sdk.

Requirements

  • PHP 8.2+
  • ext-curl and ext-json
  • A Cipi server with cipi/api installed and a token from cipi api token create

Laravel 11, 12, and 13 can use the service provider and facade. They are optional.

Install

composer require cipi/sdk

Create a token on the server and grant only the abilities this application needs (apps-view, deploy-manage, and so on). The panel returns 403 when the token is missing an ability.

Quick start

use Cipi\Sdk\Cipi;

$cipi = Cipi::connect(
    baseUrl: 'https://api.example.com',
    token: getenv('CIPI_TOKEN') ?: '',
);

$apps = $cipi->apps()->list();

$job = $cipi->deploys()->start('shop');
$result = $cipi->wait($job);

https://api.example.com and https://api.example.com/api are both accepted. Plain http:// is rejected unless you pass allow_http: true for a local panel.

Responses are the JSON body as an associative array. Writes that the panel queues return job_id and status: pending. wait() polls GET /api/jobs/{id} until the job is completed or failed.

Laravel

The service provider is discovered automatically.

CIPI_BASE_URL=https://api.example.com
CIPI_TOKEN=
CIPI_TIMEOUT=30
CIPI_ALLOW_HTTP=false
php artisan vendor:publish --tag=cipi-config
use Cipi\Sdk\Laravel\Facades\Cipi;

$apps = Cipi::apps()->list();

Or inject the client:

public function __construct(private Cipi\Sdk\Cipi $cipi) {}

One container binding talks to one server. For several servers, call Cipi::connect() with each endpoint and token.

Other frameworks

Symfony, Slim, WordPress, or a script: construct the client yourself and pass it through your container. There is no framework-specific bundle to install.

$cipi = Cipi::connect($_ENV['CIPI_BASE_URL'], $_ENV['CIPI_TOKEN']);

What you can call

Client Panel
apps() list, get, create, update, delete, suspend, unsuspend, fix permissions, recreate webhook
limits() FPM, memory, workers, and the soft disk limit
basicAuth() HTTP Basic Auth status, enable, disable
env() read and merge .env keys
authJson() shared Composer auth.json (not Basic Auth)
artisan() Artisan as an async job (tinker is rejected by the panel)
run() whitelisted non-interactive commands
deployConfig() structured Deployer options
logs() nginx, php, worker, deploy, laravel
aliases(), www(), redirects(), proxies() domains, apex redirects, path redirects, prefix proxies
node() Node runtimes, app status, blue/green restart
deploys() deploy, rollback, unlock, audit ledger
ssl() install a certificate (HTTP-01 or Cloudflare DNS-01), force HTTPS, DNS accounts
databases() engines, create, backup, restore, password
php(), ssh(), services(), smtp() PHP versions, cipi user keys, services, SMTP
health(), search() HTTP healthchecks, Meilisearch / Scout
packages(), monitor(), zeroTrust(), disk() read-only host insights
server()->status() same snapshot as cipi status
ipWhitelist() API client allowlist
jobs()->get() / wait() async job status
request() any /api path this release does not wrap yet

A few examples:

$cipi->apps()->create([
    'user' => 'shop',
    'domain' => 'shop.example.com',
    'repository' => 'git@github.com:acme/shop.git',
    'branch' => 'main',
    'php' => '8.4',
    'node_build' => 'npm ci && npm run build',
]);

$cipi->limits()->update('shop', ['fpm_max_children' => 10, 'disk_limit_gb' => 5]);

$cipi->env()->update('shop', ['APP_ENV' => 'production'], ['TELESCOPE_ENABLED']);

$cipi->databases()->create('shop', 'pgsql');

$cipi->redirects()->add('shop', '/blog/', 'https://blog.example.com', 301, true);

$cipi->ssl()->install('shop', dns: 'cloudflare', account: 'prod', wildcard: true);

$cipi->disk()->usage();

$cipi->logs()->get('shop', type: 'laravel', perPage: 100);

node_build (Laravel apps only) is the asset build Cipi runs inside every deploy, before the release goes live. It starts with npm, npx, yarn, pnpm, bun, or node, allows && and ;, refuses pipes, redirects, backticks, and $( ), and is at most 200 characters. The panel validates it and answers 422 (ValidationException) on a bad command or on a custom or Node app (Node apps take build). Needs cipi/api 1.34.0+ and Cipi CLI 5.6.0+.

The same versions report whether the app's deploy key can read its repository. The app-create job result, and the app-edit result when the repository or branch changed, carry repository_access; app-create also carries build. The check never fails the job, so look at it after wait():

$job = $cipi->wait($cipi->apps()->create([/* ... */]));
$access = $job['data']['result']['repository_access'] ?? null;

if ($access !== null && $access['status'] !== 'ok') {
    // 'nobranch': readable, but no such branch. 'denied': the key cannot read it.
    // $access['error'] is the git line, $access['hint'] the CLI fix Cipi suggests,
    // e.g. "cipi git github-token <token> && cipi git refresh shop".
}

status is ok, nobranch, or denied; branch, error, and hint are optional. On older servers, and on apps without a repository, the key is absent.

limits()->update() is an async job. Pass disk_limit_gb: null to clear the soft limit. DNS-01 and the disk limit need Cipi CLI 5.5.0+. disk() needs CLI 5.5.2 (older hosts return 503). ssl()->setDnsAccount() is synchronous, so the Cloudflare token is not queued, and the panel never returns it.

Installing host packages, changing monitor thresholds, and mutating Cloudflare Zero Trust stay on the server CLI. The API exposes those as read-only, and so does this SDK. Disk usage is read-only too; the per-app limit is limits().

Errors

Failed HTTP responses throw Cipi\Sdk\Exception\CipiException (or a subclass):

Status Exception
401 AuthenticationException
403 AuthorizationException
404 NotFoundException
409 ConflictException
422 ValidationException (errors() when Laravel returned a bag)
429 RateLimitException
5xx ServerException

wait() throws JobFailedException when the job finishes with failed, and TimeoutException if it is still running when the timeout elapses (default 300 seconds).

use Cipi\Sdk\Exception\JobFailedException;
use Cipi\Sdk\Exception\AuthorizationException;

try {
    $cipi->wait($cipi->deploys()->start('shop'));
} catch (JobFailedException $e) {
    $job = $e->job;
} catch (AuthorizationException $e) {
    // token is missing deploy-manage
}

Security

  • HTTPS and certificate verification are on by default. Redirects are not followed, so the Bearer token cannot be sent to another host.
  • The token is sent only in the Authorization header. A token that contains whitespace or control characters is rejected, so it cannot inject a second header.
  • The token is removed from exception messages, response arrays, and var_dump / print_r output.
  • Reads (GET) may retry 429 and 503 when Retry-After is at most 10 seconds. Writes are never retried.
  • Response bodies are capped (16 MiB by default).

Store CIPI_TOKEN in the environment of the machine that runs PHP. Do not commit it, and do not log the client. Create a separate token per application, with the smallest ability set that works.

$cipi = Cipi::connect($url, $token, [
    'timeout' => 30,
    'connect_timeout' => 10,
    'max_retries' => 2,
    'allow_http' => false,
]);

Tests

composer test

License

MIT. Copyright (c) 2026 Andrea Pollastri.