baseUrl = self::normalizeBaseUrl($baseUrl); $this->token = trim($token); $this->timeoutSeconds = max(1, $timeoutSeconds); if ($this->baseUrl === '' || $this->token === '') { throw new RuntimeException('Coolify base URL and API token are required.'); } } public static function normalizeBaseUrl(string $baseUrl): string { $baseUrl = rtrim(trim($baseUrl), '/'); if ($baseUrl === '') { return ''; } if (preg_match('#/api/v[0-9]+$#i', $baseUrl) === 1) { return $baseUrl; } return $baseUrl . '/api/v1'; } public function healthcheck(): array { return $this->request('GET', '/health', null, false); } public function version(): array { return $this->request('GET', '/version'); } public function listServers(): array { return $this->request('GET', '/servers'); } public function listProjects(): array { return $this->request('GET', '/projects'); } public function listProjectEnvironments(string $projectUuid): array { return $this->request('GET', '/projects/' . rawurlencode($projectUuid) . '/environments'); } public function listServices(): array { return $this->request('GET', '/services'); } public function listApplications(): array { return $this->request('GET', '/applications'); } public function listGithubApps(): array { return $this->request('GET', '/github-apps'); } public function getService(string $uuid): array { return $this->request('GET', '/services/' . rawurlencode($uuid)); } public function createService(array $payload): array { return $this->request('POST', '/services', $payload); } public function createPrivateGithubAppApplication(array $payload): array { return $this->request('POST', '/applications/private-github-app', $payload); } public function getApplication(string $uuid): array { return $this->request('GET', '/applications/' . rawurlencode($uuid)); } public function updateApplication(string $uuid, array $payload): array { return $this->request('PATCH', '/applications/' . rawurlencode($uuid), $payload); } public function updateService(string $uuid, array $payload): array { return $this->request('PATCH', '/services/' . rawurlencode($uuid), $payload); } public function updateServiceEnvsBulk(string $uuid, array $env): array { if ($env === []) { return []; } return $this->request('PATCH', '/services/' . rawurlencode($uuid) . '/envs/bulk', [ 'data' => self::bulkEnvData($env), ]); } public function updateApplicationEnvsBulk(string $uuid, array $env): array { if ($env === []) { return []; } return $this->request('PATCH', '/applications/' . rawurlencode($uuid) . '/envs/bulk', [ 'data' => self::bulkEnvData($env), ]); } public function listApplicationEnvs(string $uuid): array { return $this->request('GET', '/applications/' . rawurlencode($uuid) . '/envs'); } public function deleteApplicationEnv(string $uuid, string $envUuid): array { return $this->request('DELETE', '/applications/' . rawurlencode($uuid) . '/envs/' . rawurlencode($envUuid)); } private static function bulkEnvData(array $env): array { $data = []; foreach ($env as $key => $value) { $data[] = [ 'key' => (string)$key, 'value' => (string)$value, 'is_preview' => false, 'is_literal' => true, 'is_multiline' => str_contains((string)$value, "\n"), 'is_shown_once' => false, ]; } return $data; } public function deployResource(string $uuid, bool $force = false): array { $path = '/deploy?uuid=' . rawurlencode($uuid) . '&force=' . ($force ? 'true' : 'false'); return $this->request('GET', $path); } public function startService(string $uuid): array { return $this->request('GET', '/services/' . rawurlencode($uuid) . '/start'); } public function restartService(string $uuid): array { return $this->request('GET', '/services/' . rawurlencode($uuid) . '/restart'); } public function restartApplication(string $uuid): array { return $this->request('GET', '/applications/' . rawurlencode($uuid) . '/restart'); } public function stopService(string $uuid): array { return $this->request('GET', '/services/' . rawurlencode($uuid) . '/stop'); } public function stopApplication(string $uuid): array { return $this->request('GET', '/applications/' . rawurlencode($uuid) . '/stop'); } public function deleteService(string $uuid): array { return $this->request('DELETE', '/services/' . rawurlencode($uuid)); } public function deleteApplication(string $uuid): array { return $this->request('DELETE', '/applications/' . rawurlencode($uuid)); } public function listDeployments(): array { return $this->request('GET', '/deployments'); } protected function request(string $method, string $path, ?array $payload = null, bool $versionedApi = true): array { $url = ($versionedApi ? $this->baseUrl : $this->apiRootUrl()) . '/' . ltrim($path, '/'); $curl = curl_init($url); if ($curl === false) { throw new RuntimeException('Could not initialize Coolify API request.'); } $headers = [ 'Accept: application/json', 'Authorization: Bearer ' . $this->token, ]; curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_CUSTOMREQUEST, strtoupper($method)); curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, min(2, $this->timeoutSeconds)); curl_setopt($curl, CURLOPT_TIMEOUT, $this->timeoutSeconds); curl_setopt($curl, CURLOPT_NOSIGNAL, true); curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); if ($payload !== null) { $body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($body === false) { throw new RuntimeException('Could not encode Coolify API payload.'); } $headers[] = 'Content-Type: application/json'; curl_setopt($curl, CURLOPT_POSTFIELDS, $body); } curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); $raw = curl_exec($curl); $error = curl_error($curl); $status = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl); if ($raw === false) { throw new RuntimeException('Coolify API request failed: ' . $error); } $decoded = null; if (trim((string)$raw) !== '') { $decoded = json_decode((string)$raw, true); if (!is_array($decoded)) { $decoded = ['raw' => (string)$raw]; } } if ($status < 200 || $status >= 300) { $message = is_array($decoded) ? (string)($decoded['message'] ?? $decoded['error'] ?? ('HTTP ' . $status)) : ('HTTP ' . $status); if (is_array($decoded)) { $details = self::validationErrorSummary($decoded); if ($details !== '') { $message .= ': ' . $details; } } throw new RuntimeException('Coolify API request failed: ' . $message); } return is_array($decoded) ? $decoded : []; } private static function validationErrorSummary(array $decoded): string { $errors = $decoded['errors'] ?? $decoded['data']['errors'] ?? null; if (!is_array($errors)) { return ''; } $parts = []; foreach ($errors as $field => $messages) { $fieldName = trim((string)$field); $fieldPrefix = $fieldName !== '' ? $fieldName . ': ' : ''; if (is_array($messages)) { $messages = implode(', ', array_filter(array_map(static fn(mixed $message): string => trim((string)$message), $messages))); } else { $messages = trim((string)$messages); } if ($messages !== '') { $parts[] = $fieldPrefix . $messages; } } return implode('; ', array_slice($parts, 0, 5)); } private function apiRootUrl(): string { return preg_replace('#/v[0-9]+$#i', '', $this->baseUrl) ?: $this->baseUrl; } }