Skip to main content
knoxcall/knoxcall-php is the official KnoxCall client for PHP. Requires PHP >= 8.1 with ext-curl + ext-json — no other runtime dependencies.

Install

Not yet published to Packagist — install from the monorepo via a Composer path repository or a git checkout.
{
  "repositories": [
    { "type": "path", "url": "../knoxcall/sdk/knoxcall-php" }
  ]
}
composer require knoxcall/knoxcall-php:@dev

Create a client

use KnoxCall\KnoxCall;

// Credentials inline — no extra imports; tenant auto-discovered
$client = new KnoxCall(['client_id' => 'tk_xxxxxxxx', 'client_secret' => '...']);

// Or zero-arg with the environment configured
// (KNOXCALL_CLIENT_ID, KNOXCALL_CLIENT_SECRET)
$client = new KnoxCall();

// A pre-acquired key also works (kc_… token or legacy tk_… / AKE… key)
$client = new KnoxCall(['api_key' => getenv('KNOXCALL_API_KEY')]);
For the Stripe-style isolated Test environment, pass 'sandbox' => true (uses https://sandbox.knoxcall.com + https://sandbox-{tenant}.knoxcall.com; requires a tk_test_… key). Conflicting explicit options (e.g. api_key + client_id) throw at construction.
Token caching under PHP-FPM: typical deployments are per-request processes, so under client_credentials each request mints one token. If that matters, mint out of band, cache it yourself (APCu/Redis), and construct with ['access_token' => $cached]. Long-running workers (CLI, queues, Octane) get in-process caching with refresh-ahead automatically. DPoP-bound OAuth clients are not supported — the SDK fails fast with a clear error; use a Bearer client or the Node.js / Python SDK.

Manage resources

Create a route, then list with pagination — single-object methods return data unwrapped; paginated list() methods return the {data, meta} envelope; iterate() walks all pages:
$route = $client->routes->create(['name' => 'stripe', 'target_base_url' => 'https://api.stripe.com']);

$page = $client->routes->list(['page' => 2, 'per_page' => 50]);
// $page['data'] = rows; $page['meta'] = ['total', 'page', 'per_page', 'total_pages', 'request_id']

foreach ($client->routes->iterate() as $route) {   // every page, transparently
    echo $route['slug'], "\n";
}
The same pattern covers every resource: secrets, webhooks, clients, oauthClients, environments, apiKeys, account, auditLogs, agents, crypto, pki, vaults, and dynamicDb.

Call routes through the proxy

call() proxies a request through a KnoxCall route to your upstream and returns the raw response (['status', 'headers', 'body']) — upstream HTTP errors are never thrown; they belong to you. Reference routes by slug (write-once, rename-proof); UUIDs also work.
$resp = $client->call('billing-stripe', [
    'method' => 'POST',
    'path' => '/v1/charges',
    'body' => ['amount' => 2000, 'currency' => 'usd'],
    'environment' => 'staging',
]);
$charge = json_decode($resp['body'], true);
Legacy (non-kc_) keys automatically travel as the x-knoxcall-key header instead of Authorization: Bearer.

Bound routes

State the route (and optional defaults) once, then use plain HTTP verbs:
$printnode = $client->route('printnode-api', ['environment' => 'production']);

$computers = json_decode($printnode->get('/computers')['body'], true);
$printnode->post('/printjobs', ['body' => ['printerId' => 1, 'title' => 'Invoice']]);
$printnode->request('DELETE', '/printjobs/42');
// per-call options still override the bound defaults:
$printnode->get('/computers', ['environment' => 'staging']);

Verify webhooks

constructEvent() verifies the delivery AND parses it in one step — use it in your webhook endpoint with the RAW request body:
use KnoxCall\KnoxCall;
use KnoxCall\WebhookSignatureVerificationException;

$rawBody = file_get_contents('php://input');

try {
    $event = KnoxCall::constructEvent($rawBody, getallheaders(), $endpointSecret, [
        'format' => 'stripe',          // must match the webhook's hmac_format (default 'legacy')
        'tolerance_seconds' => 300,    // replay window; null disables
    ]);
} catch (WebhookSignatureVerificationException $e) {
    http_response_code(400);
    exit;
}

match ($event['event']) {
    'request.server_error' => alert($event['data']['route_name'], $event['data']['response']['status']),
    'audit.event' => log($event['data']['action']),
    default => null, // the event-type list is open — unknown types still parse
};
Supported formats mirror the server exactly: legacy, stripe, github, slack, aws-sns, and custom (pass header_name). All are HMAC-SHA256 with constant-time comparison.

Handle errors

try {
    $client->routes->create(['name' => 'x', 'target_base_url' => 'y']);
} catch (\KnoxCall\RateLimitException $e) {
    sleep($e->retryAfter ?? 1);
} catch (\KnoxCall\ValidationException $e) {
    var_dump($e->fields);
}
Hierarchy: KnoxCallException (base) → ApiException (with ->statusCode, ->errorCode, ->requestId, ->responseHeaders, ->responseBody) → AuthenticationException (401), PermissionDeniedException (403), NotFoundException (404), ConflictException (409), ValidationException (422, with ->fields), RateLimitException (429, with ->retryAfter), ServerException (5xx), SignupException; plus ConnectionException / ConnectionTimeoutException (transport) and WebhookSignatureVerificationException. Every ApiException carries the server’s request_id — include it when contacting support.

Retries and idempotency

Management requests retry HTTP 408/429/500/502/503/504 (never 409) with half-jitter exponential backoff, honoring Retry-After up to 30s; a 401 purges the cached token and re-auths once. Every mutating request carries a ULID X-Idempotency-Key that stays stable across retries. Data-plane calls never replay a mutation that may have reached the upstream. Configure with retry_max_attempts, retry_base_delay_ms, retry_max_delay_ms, timeout_ms.

Full reference

The package README documents every resource method, the ephemeral proxy, crypto/PKI/vault operations, and credential-less KnoxCall::signup(): sdk/knoxcall-php/README.md in the monorepo.