Skip to content

PHP SDK

A production-ready PHP client using Guzzle 7+. Handles Bearer authentication, rate-limit headers, automatic retries on 429 / 5xx, and structured error responses.

GhostFlowClient.php
<?php
declare(strict_types=1);
namespace GhostFlow;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Response;
class GhostFlowError extends \RuntimeException
{
public string $errorCode;
public int $httpStatus;
public function __construct(string $code, string $message, int $status)
{
parent::__construct($message, $status);
$this->errorCode = $code;
$this->httpStatus = $status;
}
}
class RateLimitInfo
{
public function __construct(
public readonly int $limit,
public readonly int $remaining,
public readonly int $reset,
) {}
public static function fromResponse(Response $response): self
{
return new self(
limit: (int) ($response->getHeaderLine('X-RateLimit-Limit') ?: 0),
remaining: (int) ($response->getHeaderLine('X-RateLimit-Remaining') ?: 0),
reset: (int) ($response->getHeaderLine('X-RateLimit-Reset') ?: 0),
);
}
}
class GhostFlowClient
{
private Client $http;
private int $maxRetries;
public function __construct(
?string $apiKey = null,
string $baseUrl = 'https://devcore.getghostflow.io/api/v1',
int $maxRetries = 2,
float $timeout = 30.0,
) {
$apiKey ??= getenv('GF_API_KEY') ?: throw new \RuntimeException('GF_API_KEY not set');
$this->maxRetries = $maxRetries;
$this->http = new Client([
'base_uri' => rtrim($baseUrl, '/') . '/',
'timeout' => $timeout,
'headers' => [
'Authorization' => "Bearer {$apiKey}",
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]);
}
// ── Core request method ────────────────────────────────
/**
* @return array{0: mixed, 1: RateLimitInfo}
*/
public function request(string $method, string $path, array $options = []): array
{
$lastError = null;
for ($attempt = 0; $attempt <= $this->maxRetries; $attempt++) {
try {
$response = $this->http->request($method, ltrim($path, '/'), $options);
$rateLimit = RateLimitInfo::fromResponse($response);
$data = json_decode((string) $response->getBody(), true);
return [$data, $rateLimit];
} catch (RequestException $e) {
$response = $e->getResponse();
if ($response === null) {
throw $e;
}
$body = json_decode((string) $response->getBody(), true) ?? [];
$status = $response->getStatusCode();
$lastError = new GhostFlowError(
$body['code'] ?? 'UNKNOWN',
$body['message'] ?? $e->getMessage(),
$status,
);
if (in_array($status, [429, 500, 502, 503], true) && $attempt < $this->maxRetries) {
$retryAfter = (int) ($response->getHeaderLine('Retry-After') ?: 1);
sleep($retryAfter);
continue;
}
}
}
throw $lastError;
}
// ── Convenience shortcuts ──────────────────────────────
public function get(string $path, array $query = []): array
{
return $this->request('GET', $path, ['query' => $query]);
}
public function post(string $path, array $json = []): array
{
return $this->request('POST', $path, ['json' => $json]);
}
public function put(string $path, array $json = []): array
{
return $this->request('PUT', $path, ['json' => $json]);
}
public function delete(string $path): array
{
return $this->request('DELETE', $path);
}
}
<?php
require_once __DIR__ . '/vendor/autoload.php';
use GhostFlow\GhostFlowClient;
use GhostFlow\GhostFlowError;
$gf = new GhostFlowClient(); // picks up GF_API_KEY from env
// List campaigns
[$campaigns, $rl] = $gf->get('campaigns');
echo "Found " . count($campaigns) . " campaigns, {$rl->remaining} requests left\n";
// Create a campaign
[$campaign, $_] = $gf->post('campaigns', [
'name' => 'Summer Promo',
'url' => 'https://example.com/promo',
]);
echo "Created campaign {$campaign['id']}\n";
// Dashboard stats
[$stats, $_] = $gf->get('reports/dashboard', [
'from' => '2025-01-01',
'to' => '2025-01-31',
]);
<?php
try {
$gf->get('campaigns/non-existent-id');
} catch (GhostFlowError $e) {
match ($e->errorCode) {
'NOT_FOUND' => echo "Campaign does not exist\n",
'AUTH_INVALID_API_KEY' => echo "Check your API key\n",
'RATE_LIMITED' => echo "Slow down — retry after backoff\n",
default => echo "API error {$e->httpStatus}: {$e->getMessage()}\n",
};
}