kinetistack-io / php-sdk
Framework-Agnostic PHP Client Library for KinetiStack API
Requires
- php: ^8.2
- ext-json: *
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.0 || ^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.60
- guzzlehttp/guzzle: ^7.8 || ^8.0
- guzzlehttp/psr7: ^2.6 || ^3.0
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^11.5
- symfony/http-client: ^6.4 || ^7.0 || ^8.0
- symfony/http-client-contracts: ^3.0
Suggests
- guzzlehttp/guzzle: To use Guzzle HTTP Client (^7.8 || ^8.0)
- symfony/http-client: To use Symfony's HTTP Client (^6.4 || ^7.0 || ^8.0)
Provides
None
Conflicts
None
Replaces
None
README
Framework-Agnostic PHP Client Library for KinetiStack API.
Installation
composer require kinetistack-io/php-sdk
Usage
Initialization
use KinetiStack\Sdk\KinetiClient; // Basic initialization $client = new KinetiClient( 'https://api.kinetistack.io', // Your API host 'sk_test_12345' // Your project API key ); // Advanced initialization with custom options and automatic retries $client = new KinetiClient( apiHost: 'https://api.kinetistack.io', apiKey: 'sk_test_12345', httpClient: null, // Optional: PSR-18 ClientInterface (e.g. Guzzle), Symfony HttpClientInterface, or auto-discovered options: [ 'timeout' => 30.0, // Request timeout in seconds (applied when using Symfony HttpClient) 'max_retries' => 3, // Automatically retry transient HTTP 429 and 503 errors 'use_http_query_method' => true, // Default: true (RFC 10008 HTTP QUERY). Set to false to force POST fallback ] );
HTTP Client Flexibility (PSR-18 & Symfony)
The SDK is strictly framework-agnostic. It does not force a concrete HTTP client implementation:
- Auto-Discovery: If
httpClientis omitted ornull,php-http/discoveryautomatically discovers any installed PSR-18 client (such as Guzzle) or Symfony's HTTP Client. - PSR-18 (Guzzle, etc.): Inject any
Psr\Http\Client\ClientInterfaceinstance directly (e.g.new KinetiClient($host, $key, $guzzleClient)). Note that PSR-18ClientInterface::sendRequest()does not support per-request timeout options; configure timeout settings directly on your PSR-18 client instance (e.g.new GuzzleHttp\Client(['timeout' => 30.0])). - Symfony HTTP Client: Existing Symfony projects can inject
Symfony\Contracts\HttpClient\HttpClientInterfacefor full backward compatibility. The'timeout'option in$optionsis applied directly to requests when using the Symfony HTTP client.
Transient Error Retries & Backoff
When max_retries is configured in $options, the SDK automatically retries transient HTTP errors:
- HTTP 429 (Too Many Requests): Automatically delays subsequent retries by respecting the server's
Retry-Afterresponse header (or falls back to exponential backoff). - HTTP 503 (Service Unavailable): Automatically retries using exponential backoff.
- Non-transient errors (such as HTTP 400, 401, 403, 404, 422, or 500) fail immediately without retrying.
If all retry attempts are exhausted, the corresponding typed exception (e.g., RateLimitExceededException or ServiceUnavailableException) is thrown.
Image Analysis
use KinetiStack\Sdk\Dto\ContextHintsDto; // From a URL (returns an async JobDto) $job = $client->analyzeImage( 'https://example.com/image.jpg', new ContextHintsDto(pageTitle: 'Renewable Energy') ); // Wait for job completion and hydrate the typed vision response $completedJob = $client->waitForJob($job->jobId); $vision = $completedJob->toVisionResponse(); echo $vision?->altText; if ($vision?->tokensConsumed !== null) { echo "Tokens consumed: {$vision->tokensConsumed}\n"; } // From a stream resource (memory-efficient for large files & remote stream wrappers) $stream = fopen('/path/to/image.jpg', 'rb'); $job = $client->analyzeImageStream($stream, 'image.jpg'); fclose($stream); // From a local file (uses streams internally) $job = $client->analyzeImageBinary('/path/to/image.jpg'); // From binary string content $job = $client->analyzeImageContent($binaryData, 'image.jpg');
Batch Processing
use KinetiStack\Sdk\Dto\ImageInputDto; use KinetiStack\Sdk\Dto\JobDto; use KinetiStack\Sdk\Exception\JobTimeoutException; use KinetiStack\Sdk\KinetiClient; // Submit a batch of images (URL or Base64 / Data URI) $batch = $client->submitBatchJob([ new ImageInputDto('media:1', 'https://example.com/1.jpg'), new ImageInputDto('media:2', imageBase64: 'data:image/jpeg;base64,...'), ]); echo $batch->jobId; // E.g., '123e4567-e89b-12d3-a456-426614174000' // Poll for completion with exponential backoff & jitter try { $completedBatch = $client->waitForJob( jobId: $batch->jobId, timeoutSeconds: KinetiClient::DEFAULT_TIMEOUT_SECONDS, // Default: 60s pollIntervalSeconds: KinetiClient::DEFAULT_POLL_INTERVAL_SECONDS, // Default: 2s (initial interval) onProgress: function (JobDto $job): void { echo "Current status: {$job->status->value}\n"; }, maxPollIntervalSeconds: KinetiClient::DEFAULT_MAX_POLL_INTERVAL_SECONDS // Default: 10s (capped interval) ); if ($completedBatch->tokensConsumed !== null) { echo "Tokens consumed: {$completedBatch->tokensConsumed}\n"; } foreach ($completedBatch->results as $result) { echo $result->externalId . ': ' . $result->altText . "\n"; } } catch (JobTimeoutException $e) { echo "Job timed out. Last known status: " . $e->latestJob->status->value; }
Exponential Backoff & Polling Constants
waitForJob() implements an adaptive polling strategy with jitter:
- Exponential progression: Polling begins at
$pollIntervalSeconds(default:2s) and doubles on each subsequent check (e.g.,2s -> 4s -> 8s -> 10s). - Interval cap: The sleep interval between checks never exceeds
$maxPollIntervalSeconds(default:10s). - Randomized jitter: A randomized jitter between 0 and 500ms is added to every sleep duration to prevent synchronized polling spikes across distributed workers.
- Progress monitoring: The optional
$onProgresscallback receives the updatedJobDtoon each poll check.
Default constants exposed on KinetiClient:
KinetiClient::DEFAULT_POLL_INTERVAL_SECONDS=2KinetiClient::DEFAULT_MAX_POLL_INTERVAL_SECONDS=10KinetiClient::DEFAULT_TIMEOUT_SECONDS=60
Semantic Search & RAG Streaming (SDK v2)
In v2, search and RAG operations utilize the RFC 10008 HTTP QUERY method by default, providing clean, safe, idempotent query semantics with a JSON request payload. If your application operates behind legacy corporate middleboxes, firewalls, or reverse proxies that reject non-standard HTTP methods, pass ['use_http_query_method' => false] in $options or configure Configuration to fall back to POST.
The SDK provides dedicated sub-clients via $client->search() and $client->rag(), while preserving full backward compatibility when calling $client->search($query) directly.
1. Search Operations (SearchClient)
use KinetiStack\Sdk\Dto\SearchQueryDto; // Direct execution (backward-compatible) $response = $client->search('solar panels'); // Access dedicated SearchClient $search = $client->search(); // Standard search query (dispatches RFC 10008 HTTP QUERY to /api/v1/search/query) $query = SearchQueryDto::create('How does solar net metering work?') ->withLimit(5) ->withSynthesis(true); $response = $search->query($query); echo "Found {$response->total} results.\n"; if ($response->tokensConsumed !== null) { echo "Tokens consumed: {$response->tokensConsumed}\n"; } if ($response->hasSynthesis()) { echo "Synthesized Answer: " . $response->synthesis->answer . "\n"; if ($response->synthesis->tokensConsumed !== null) { echo "Synthesis Tokens: " . $response->synthesis->tokensConsumed . "\n"; } } // Automatic pagination across all matching results foreach ($search->all('solar energy') as $item) { echo "{$item->title} (score: {$item->score})\n"; } // Streamed RAG synthesis via SSE generator (dispatches HTTP QUERY to /api/v1/search/rag) foreach ($search->stream('Explain commercial solar financing options') as $chunk) { echo $chunk->text; // Streams partial tokens/words in real-time if ($chunk->tokensConsumed !== null) { echo "\nTokens consumed: {$chunk->tokensConsumed}\n"; } flush(); }
2. Retrieval Augmented Generation (RagClient)
use KinetiStack\Sdk\Dto\RagSynthesisDto; $rag = $client->rag(); // Synchronous RAG query (dispatches HTTP QUERY to /api/v1/rag/query) $result = $rag->query('What are the key advantages of perovskite solar cells?'); if ($result instanceof RagSynthesisDto) { echo "Answer: {$result->answer}\n"; echo "Citations: " . implode(', ', $result->citations) . "\n"; } // Streaming RAG query foreach ($rag->stream('Explain solar ROI calculations') as $chunk) { echo $chunk->text; flush(); }
3. RFC 10008 HTTP QUERY & POST Fallback Configuration
use KinetiStack\Sdk\Configuration; use KinetiStack\Sdk\KinetiClient; // Option A: Via constructor array options $client = new KinetiClient( 'https://api.kinetistack.io', 'sk_test_12345', options: ['use_http_query_method' => false] // Forces POST for search & RAG ); // Option B: Via typed Configuration instance $config = new Configuration(['use_http_query_method' => false]); $client = new KinetiClient('https://api.kinetistack.io', 'sk_test_12345', options: $config); // Option C: Immutable reconfiguration $postClient = $client->withConfiguration(['use_http_query_method' => false]); $queryClient = $postClient->withConfiguration(['use_http_query_method' => true]);
API Key Scopes
RAG synthesis and semantic search enforce granular API key scopes:
- RAG synthesis (
$client->rag()->query(),$client->rag()->stream(),$client->search()->stream(), and$client->searchStream()calling/api/v1/rag/queryor/api/v1/search/rag) requires an API key scoped toragorall. - Standard vector search (
$client->search()->query(),$client->search()->all(),$client->search($query), and$client->searchAll()calling/api/v1/search/query) requiressearchorall.
Calling RAG endpoints with a search-only key returns an HTTP 403 Forbidden response. Because searchStream(), $client->search()->stream(), and $client->rag()->stream() return a lazy PHP generator, the HTTP request executes on the first stream iteration and throws an AuthorizationException (code 403) before any chunks are yielded:
use KinetiStack\Sdk\Exception\AuthorizationException; try { $stream = $client->searchStream('Explain commercial solar financing options'); foreach ($stream as $chunk) { echo $chunk->text; flush(); } } catch (AuthorizationException $e) { // Thrown on first iteration when API key lacks 'rag' or 'all' scope (HTTP 403) echo "Authorization error (HTTP {$e->getCode()}): {$e->getMessage()}\n"; }
Upgrading from v1.x to v2.0
When upgrading from kinetistack-io/php-sdk ^1.0 to ^2.0:
- Minimum PHP Version (
>= 8.2): PHP 8.1 support has been dropped. Ensure your runtime environment runs PHP 8.2, 8.3, 8.4, or 8.5. - RFC 10008 HTTP
QUERYDefault: Vector search (/api/v1/search/query) and RAG (/api/v1/rag/query,/api/v1/search/rag) now dispatch HTTPQUERYrequests by default instead ofPOST. If your network passes through legacy proxies or middleboxes that blockQUERY, pass['use_http_query_method' => false]in$optionsor viaConfiguration. - Sub-Client Accessors (
$client->search()&$client->rag()):- Calling
$client->search($query)with aSearchQueryDtoorstringcontinues to execute the search directly and returnSearchResponseDto(backward-compatible). - Calling
$client->search()without arguments returnsSearchClientInterface(query(),search(),all(),stream()). - Calling
$client->rag()returnsRagClientInterface(query(),stream()). - Custom implementations of
KinetiClientInterfacemust update theirsearch()signature tosearch(SearchQueryDto|string|null $query = null): SearchClientInterface|SearchResponseDtoand implementrag(),getConfiguration(), andwithConfiguration().
- Calling
- Removed Deprecated v1 Symbols:
- Use
SearchQueryDto::withOffset(),setOffset(), andgetOffset()instead of the removed$page,withPage(),setPage(), andgetPage(). - Catch
RateLimitExceededExceptioninstead of the removedRateLimitException. - Import
KinetiClientInterface,AdminClientInterface,AnalyticsClientInterface, andModuleClientInterfacefrom the rootKinetiStack\Sdknamespace.
- Use
Document ACL & Role Assertions
When executing searches across protected documents, integrating servers assert end-user roles via withUserRoles() or SearchFilterDto::withPermissions():
use KinetiStack\Sdk\Dto\SearchQueryDto; use KinetiStack\Sdk\Exception\AuthorizationException; $query = SearchQueryDto::create('Internal quarterly review') ->withUserRoles(['editor', 'finance']); try { $results = $client->search($query); } catch (AuthorizationException $e) { // Thrown if the API key has an allowed_asserted_roles whitelist and one of the asserted roles is unauthorized (HTTP 403) echo "Unauthorized role assertion: {$e->getMessage()}\n"; }
Webhook Verification
Verify incoming HMAC-SHA256 signatures for webhook events from the Kineti-Signature header (v1={timestamp}.{hmac}), with replay protection:
use KinetiStack\Sdk\WebhookVerifier; $payload = file_get_contents('php://input'); // Modern Kineti-Signature (v1={timestamp}.{hmac}) with replay protection $signatureHeader = $_SERVER['HTTP_KINETI_SIGNATURE'] ?? ''; $webhookSecret = 'whsec_your_secret_key'; // Verifies HMAC and validates timestamp within 300s replay window (default) $isValid = WebhookVerifier::verify($payload, $signatureHeader, $webhookSecret); // Or customize tolerance window in seconds (pass 0 to disable replay check) // $isValid = WebhookVerifier::verify($payload, $signatureHeader, $webhookSecret, toleranceSeconds: 600); if (!$isValid) { http_response_code(401); exit('Invalid or expired signature'); }
Secret Rotation and Dual-Secret Verification
During secret rotation, the backend generates a new secret while webhooks in flight may still be signed with the prior secret. WebhookVerifier::verify() accepts a list of secrets (string|list<string>), enabling zero-downtime rolling verification against either the current or prior secret:
// Rotate secret via AdminClient $secretDto = $admin->rotateProjectWebhookSecret($projectId); $newSecret = $secretDto->webhookSecret; // Verify incoming webhooks against either secret during rotation rollout: $isValid = WebhookVerifier::verify( $payload, $signatureHeader, [$newSecret, $priorSecret] );
Event Deduplication with Delivery ID
Each webhook delivery includes a unique UUID in the Kineti-Delivery-Id header to detect and prevent duplicate processing across retries:
$deliveryId = $_SERVER['HTTP_KINETI_DELIVERY_ID'] ?? ''; if ($deliveryId !== '' && $idempotencyStore->has($deliveryId)) { // Event was already processed; acknowledge immediately http_response_code(200); exit('Already processed'); }
Available Constants
WebhookVerifier::HEADER_SIGNATURE:'Kineti-Signature'WebhookVerifier::HEADER_DELIVERY_ID:'Kineti-Delivery-Id'WebhookVerifier::DEFAULT_TOLERANCE_SECONDS:300
Error Handling
The SDK maps HTTP error responses to typed exceptions (RFC 9457 compliant):
use KinetiStack\Sdk\Exception\AuthenticationException; use KinetiStack\Sdk\Exception\AuthorizationException; use KinetiStack\Sdk\Exception\KinetiException; use KinetiStack\Sdk\Exception\NotFoundException; use KinetiStack\Sdk\Exception\PayloadTooLargeException; use KinetiStack\Sdk\Exception\RateLimitExceededException; use KinetiStack\Sdk\Exception\ServerException; use KinetiStack\Sdk\Exception\ServiceUnavailableException; use KinetiStack\Sdk\Exception\ValidationException; try { $client->analyzeImage('invalid-url'); } catch (ValidationException $e) { print_r($e->violations); } catch (RateLimitExceededException $e) { // Thrown if max_retries is not configured or all retry attempts are exhausted echo "Rate limited. Retry after: " . $e->retryAfter . " seconds."; } catch (ServiceUnavailableException $e) { // Thrown if max_retries is not configured or all retry attempts are exhausted echo "Service temporarily unavailable (503): " . $e->getMessage(); } catch (KinetiException $e) { echo "API Error: " . $e->getMessage(); }
Note on Transient Errors: When
max_retriesis specified in client options (e.g.,['max_retries' => 3]), transient HTTP errors (429 Too Many RequestsrespectingRetry-Afterheader, and503 Service Unavailable) are automatically retried with exponential backoff before throwingRateLimitExceededExceptionorServiceUnavailableException.
Admin API (AdminClient)
The SDK provides AdminClient as a dedicated sibling client for administrative operations under /api/v1/admin/*, authenticated via LexikJWT Bearer tokens (Authorization: Bearer <jwt>).
Initialization & Authentication
use KinetiStack\Sdk\AdminClient; // 1. Initial login without token $admin = new AdminClient('https://api.kinetistack.io'); $auth = $admin->login('admin@agency.com', 'SuperSecret123!'); // 2. Obtain an authenticated client instance with the JWT token $authenticatedAdmin = $admin->withToken($auth->token); // Or initialize directly if you already hold a valid token: // $authenticatedAdmin = new AdminClient('https://api.kinetistack.io', $jwtToken); // 3. Revoke session server-side (logout) $authenticatedAdmin->logout();
Organizations & Projects
// Organizations are initialized during registration (POST /api/v1/admin/register). // Once registered, manage your organization via item operations: $orgId = 'org-uuid-123'; // Obtained from registration response or user profile $currentOrg = $admin->getOrganization($orgId); $updatedOrg = $admin->updateOrganization($orgId, ['name' => 'Acme Global Agency']); // Projects (supports optional defaultDocumentVisibility: 'restricted' | 'public') $project = $admin->createProject('Client Portal', 'portal.example.com', 'https://webhook.example.com/events', defaultDocumentVisibility: 'restricted'); $projects = $admin->listProjects(); $projectDetails = $admin->getProject($project->id); $admin->updateProject($project->id, ['settings' => ['theme' => 'dark'], 'defaultDocumentVisibility' => 'public']); $secretDto = $admin->rotateProjectWebhookSecret($project->id); // Rotates project webhook signing secret $admin->deleteProject($project->id); // Soft-delete and revokes associated API keys
API Key Management
// Create API key with optional allowed asserted roles whitelist (returns ApiKeyCreatedDto with plaintext token) $newKey = $admin->createApiKey( $project->id, 'Production Drupal Key', 'all', rateLimitPerMinute: 120, allowedAssertedRoles: ['authenticated', 'editor'] ); echo $newKey->token; // Secret plaintext token — save this now! echo $newKey->tokenSuffix; // e.g. '1234' print_r($newKey->allowedAssertedRoles); // ['authenticated', 'editor'] (or null for unrestricted key) // List API keys (returns ApiKeyDto[] without plaintext token, with optional usage metrics and allowedAssertedRoles) $keys = $admin->listApiKeys($project->id); foreach ($keys as $key) { $tokens = $key->tokensConsumedToday ?? 0; $requests = $key->requestsToday ?? 0; $roles = $key->allowedAssertedRoles !== null ? implode(', ', $key->allowedAssertedRoles) : 'unrestricted'; echo "{$key->name}: {$tokens} tokens consumed today across {$requests} requests (roles: {$roles})\n"; } // Rotate API key (old key continues working during grace period) $rotated = $admin->rotateApiKey($project->id, $newKey->id); echo $rotated->token; // New secret token // Revoke API key $admin->revokeApiKey($project->id, $newKey->id);
Usage & Analytics
use KinetiStack\Sdk\AnalyticsClient; use KinetiStack\Sdk\Enum\AnalyticsGrouping; // Summary report across services (supports optional apiKeyId filter) $usage = $admin->getUsage(from: '2026-09-01', to: '2026-09-05', projectId: $project->id, apiKeyId: $newKey->id); foreach ($usage as $summary) { echo "{$summary->projectName} - {$summary->service}: {$summary->totalTokens} tokens ({$summary->requestCount} requests)\n"; } // Time-bucketed analytics with typed grouping (DAY, WEEK, MONTH) via AdminClient $dailyAnalytics = $admin->getAnalytics(grouping: AnalyticsGrouping::DAY, from: '2026-09-01', to: '2026-09-07'); $weeklyAnalytics = $admin->getAnalytics(grouping: AnalyticsGrouping::WEEK, from: '2026-09-01', to: '2026-09-30'); $monthlyAnalytics = $admin->getAnalytics(grouping: AnalyticsGrouping::MONTH); print_r($weeklyAnalytics->data); // Or via dedicated AnalyticsClient $analyticsClient = $admin->analytics(); // Or standalone: new AnalyticsClient('https://api.kinetistack.io', $jwtToken) $trends = $analyticsClient->getAnalytics(AnalyticsGrouping::WEEK);
Development
No local PHP or Composer installation is required. Everything runs in one-off Docker containers via docker compose and make.
Prerequisites
- Docker & Docker Compose
make
Common Commands
# Install dependencies make install # Run PHPUnit tests make test # Run tests across PHP matrix (8.2, 8.3, 8.4, 8.5) make test-all # Static analysis (PHPStan) make phpstan # Code style fixer (PHP CS Fixer) make cs-fix make cs-check # Run all checks (cs-check, phpstan, test) make check # Run arbitrary composer command make composer cmd="require symfony/yaml" # Prepare and publish a release (runs verification checks, validates composer strictly, tags, pushes) make release VERSION=2.0.0 # Open container shell make shell
CI/CD & Release Workflow
The SDK uses automated CI/CD workflows for testing, static analysis, and releases:
- CI Pipeline (
.github/workflows/ci.yml): Runs on pushes and pull requests targetingmain. Executes test matrices across PHP 8.2, 8.3, 8.4, and 8.5, followed by PHP CS Fixer and PHPStan static analysis. - Release Pipeline (
.github/workflows/release.yml): Triggered automatically when a version tag matchingv*.*.*(e.g.,v2.0.0) is pushed to the repository.- Validation (
validate): Checks platform requirements viacomposer check-platform-reqsand runs PHPUnit across PHP 8.2, 8.3, 8.4, and 8.5. - Quality Checks (
quality-checks): Ensures code conforms to PSR-12 and passes PHPStan Level 9 static analysis. - Release (
release): Requires bothvalidateandquality-checksto pass. Publishes a GitHub Release (with auto-generated release notes and automatic pre-release detection) and triggers a synchronization webhook to the Packagist API via authenticated cURL.
- Validation (
Required Repository Secrets
The release workflow requires the following repository secrets to be configured in GitHub:
| Secret Name | Description | Required By |
|---|---|---|
PACKAGIST_USER |
Packagist account username authorized to manage kinetistack-io/php-sdk |
Packagist API update hook |
PACKAGIST_TOKEN |
Packagist API token with package update permissions | Packagist API update hook |
The workflow also requires GitHub Actions default GITHUB_TOKEN with contents: write permissions (configured automatically in release.yml) to publish releases. Fork runs automatically skip the release job if secrets are not configured.
Contributing
Contributions are welcome! Please refer to CONTRIBUTING.md for detailed local development, testing, and release guidelines.
Before opening a pull request, ensure that:
- Code adheres to PSR-12 standards (
make cs-check/make cs-fix). - Static analysis passes at PHPStan Level 9 (
make phpstan). - Unit tests pass across all supported PHP versions (
make test-all). - All checks pass locally via
make checkbefore opening a pull request.
Security
If you discover a security vulnerability within this SDK, please send an email to security@kinetistack.io or use GitHub Private Vulnerability Reporting. All security vulnerabilities will be promptly addressed.
License
The KinetiStack PHP SDK is open-sourced software licensed under the MIT license.