> ## Documentation Index
> Fetch the complete documentation index at: https://docs.knoxcall.com/llms.txt
> Use this file to discover all available pages before exploring further.

# PHP SDK

> knoxcall/knoxcall-php — the official KnoxCall client for PHP 8.1+. OAuth 2.1 under the hood; ext-curl + ext-json only.

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

## Install

<Note>
  Not yet published to Packagist — install from the monorepo via a Composer [path repository](https://getcomposer.org/doc/05-repositories.md#path) or a git checkout.
</Note>

```json theme={"dark"}
{
  "repositories": [
    { "type": "path", "url": "../knoxcall/sdk/knoxcall-php" }
  ]
}
```

```bash theme={"dark"}
composer require knoxcall/knoxcall-php:@dev
```

## Create a client

```php theme={"dark"}
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.

<Note>
  **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.
</Note>

## 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:

```php theme={"dark"}
$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.

```php theme={"dark"}
$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:

```php theme={"dark"}
$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:

```php theme={"dark"}
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

```php theme={"dark"}
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`](https://github.com/knoxcall/knoxcall/tree/main/sdk/knoxcall-php) in the monorepo.
