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

# PHP

> Guard a PHP route with the community ZeroClick seller SDK: install, wire the client, and settle usage.

<Warning>
  **Community SDK.** [`affiliatecom/zeroclick-sdk-sellers-php`](https://packagist.org/packages/affiliatecom/zeroclick-sdk-sellers-php)
  is built and maintained by [Affiliate.com](https://github.com/affiliatecom/zeroclick-sdk-sellers-php),
  not by ZeroClick. We list it because PHP sellers asked for one and this one is
  good — we verified its signature handling against our own conformance vectors
  (see [below](#what-we-verified)) — but it is outside our release process, and
  we do not review its updates. Treat version upgrades as you would any
  third-party dependency.

  This page gets you integrated. For the full API, the maintainer's
  [documentation](https://github.com/affiliatecom/zeroclick-sdk-sellers-php/tree/main/docs)
  is the authority, and issues belong on their tracker.
</Warning>

## Install

```bash theme={null}
composer require affiliatecom/zeroclick-sdk-sellers-php
```

Requires PHP 8.3+. It needs a PSR-18 HTTP client and PSR-17 factories; if you
have none yet, add a pair:

```bash theme={null}
composer require guzzlehttp/guzzle nyholm/psr7
```

## Configure

Two values, and the first is two values in one. The signing credential is a key
id and a secret joined by a colon — pasting in the secret alone is the common
setup mistake.

```bash theme={null}
ZEROCLICK_SIGNING_SECRETS=<key-id>:<signing-secret>
ZEROCLICK_API_KEY=zc_live_...
```

Both come from your seller settings. [Keys and secrets](/integrate/keys-and-secrets)
covers rotation, which the format supports: comma-separate the pairs and both
verify during the overlap.

## Wire the client, once

```php theme={null}
use AffiliateCom\ZeroClick\Sellers\Config\Credentials;
use AffiliateCom\ZeroClick\Sellers\Http\RefusalFactory;
use AffiliateCom\ZeroClick\Sellers\Http\ServerRequestVerifier;
use AffiliateCom\ZeroClick\Sellers\Http\Transport;
use AffiliateCom\ZeroClick\Sellers\SellerClient;
use AffiliateCom\ZeroClick\Sellers\Signature\SignatureVerifier;
use AffiliateCom\ZeroClick\Sellers\Signature\SystemClock;
use Nyholm\Psr7\Factory\Psr17Factory;

$psr17 = new Psr17Factory();
$credentials = Credentials::fromEnvironment(getenv());
$refusals = new RefusalFactory($psr17, $psr17);

$client = new SellerClient(
    credentials: $credentials,
    transport: Transport::discover(),
    verifier: new ServerRequestVerifier(
        verifier: new SignatureVerifier(
            secrets: $credentials->signingSecrets,
            refusals: $refusals,
            clock: new SystemClock(),
        ),
        refusals: $refusals,
    ),
    refusals: $refusals,
);
```

On Laravel or Symfony you write none of that — the package ships a service
provider and a bundle that assemble it from config. See the maintainer's
[middleware guide](https://github.com/affiliatecom/zeroclick-sdk-sellers-php/blob/main/docs/middleware.md).

## Guard a paid route

The PSR-15 middleware performs the whole sequence in the order that matters, so
no route can get it wrong or forget the last step:

```php theme={null}
use AffiliateCom\ZeroClick\Sellers\Http\GuardMiddleware;
use AffiliateCom\ZeroClick\Sellers\Usage\UsageItem;

$guard = new GuardMiddleware($client, 'product-watch', [UsageItem::of('requests', 1)]);

$response = $guard->process($request, $yourHandler);
```

A denied request comes back as the refusal, your handler never runs, and nothing
is billed. A served one carries `zc-usage`, which is what turns the work into
revenue.

## When the cost is not known upfront

Declare a ceiling, then settle the real amount. ZeroClick authorises up to the
ceiling and bills what you settle, so a ceiling never overcharges:

```php theme={null}
use AffiliateCom\ZeroClick\Sellers\Usage\SyncUsageItem;
use AffiliateCom\ZeroClick\Sellers\Usage\UsageItem;

$result = $client->guard($request, 'product-watch', [UsageItem::upTo('rows', 500)]);
if ($result->isDenied()) {
    return $result->response();
}

$rows = doTheWork($request);

return $client->withUsage($yourResponse, [
    SyncUsageItem::of('product-watch', 'rows', count($rows)),
]);
```

A ceiling does **gate the buyer's authorisation**, though: the reserve is the
ceiling, so a buyer whose per-call cap is below it is refused before any work
happens. Keep it near a realistic worst case rather than as high as possible.

## Free endpoints that still need a caller

```php theme={null}
$result = $client->guardIdentity($request, 'product-watch');
```

No allowance call and no `zc-usage`. See [free and identity endpoints](/integrate/free-and-identity-endpoints).

## Three things that cost money quietly

The maintainer documents these prominently, and they are worth repeating because
each is a silent failure rather than an error in a log:

* **The default serves work you may never bill.** When the allowance API gives
  no usable answer, the package serves the request anyway — matching every
  official SDK, so a ZeroClick outage does not become your outage. Watch it with
  `onAllowanceUnavailable`, or choose `OutagePolicy::Deny` if refusing costs you
  less than serving for free.
* **An unreported ceiling settles at zero.** `upTo()` authorises up to a limit
  and charges what you settle. Deliver without settling and that is zero — free
  for the buyer, unbilled for you, no error anywhere.
* **A normalised request target fails verification.** ZeroClick signs the
  percent-encoded path and query exactly as sent. A framework or an ingress that
  decodes or reorders them breaks verification for those URLs only, in
  production only, as an unexplained `401`. The package faults loudly on the
  framework case; the proxy case is a deployment concern — nginx and several
  managed load balancers normalise `%2F` by default. See the maintainer's
  [raw request target](https://github.com/affiliatecom/zeroclick-sdk-sellers-php/blob/main/docs/raw-request-target.md).

## What we verified

We ran our shared signing conformance vectors — the same 29 cases every official
SDK is checked against, including malformed-header and tampered-body cases —
against this package, and **all 29 agreed**. That covers signature verification:
the part where a mistake means serving forged traffic.

Two gaps to plan around, neither a defect:

* **No stateful account helpers.** If you [sell accounts and API keys](/integrate/stateful-sellers)
  rather than metering per call, implement those two routes yourself against the
  [reference](/resources/stateful-access-reference).
* **It carries runtime dependencies** — PSR HTTP interfaces, `php-http/discovery`,
  `psr/clock` — where the official SDKs deliberately carry none. They land in
  your `composer.json` beside your own.

The maintainer also marks their catalog and analytics surfaces as unverified
against a live server. Those are additions beyond what any official SDK offers;
confirm them against a sandbox before depending on them.

## If you would rather not take a dependency

The [REST walkthrough](/integrate/rest-walkthrough) implements the same contract
with plain HTTP and your standard library. The whole guard is one HMAC check and
one HTTPS call.
