affiliatecom / zeroclick-sdk-sellers-php
Unofficial, third-party PHP seller SDK for ZeroClick.ai. Not affiliated with or endorsed by ZeroClick.
Package info
github.com/affiliatecom/zeroclick-sdk-sellers-php
pkg:composer/affiliatecom/zeroclick-sdk-sellers-php
Requires
- php: ^8.3
- ext-hash: *
- ext-json: *
- php-http/discovery: ^1.19
- psr/clock: ^1.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
Requires (Dev)
- laravel/framework: ^13.0
- laravel/pint: ^1.18
- nyholm/psr7: ^1.8
- nyholm/psr7-server: ^1.1
- phpstan/phpstan: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^11.5 || ^12.0
- psr/log: ^3.0
- spomky-labs/aes-key-wrap: ^7.0
- symfony/config: ^7.2 || ^8.1
- symfony/dependency-injection: ^7.2 || ^8.1
- symfony/http-foundation: ^7.2 || ^8.1
- symfony/http-kernel: ^7.2 || ^8.1
- web-token/jwt-framework: ^4.1
Suggests
- illuminate/http: For the Laravel bridge: the request type its raw-target extraction reads.
- illuminate/routing: For the Laravel bridge's route middleware alias.
- illuminate/support: For the Laravel bridge: service provider, publishable config, and the `zeroclick` route middleware alias. Never required — install the package into a non-Laravel app and none of it loads.
- nyholm/psr7-server: For Slim and plain PSR-15: builds a server request whose target is the raw REQUEST_URI, which is what verification needs. No adapter required.
- psr/http-client-implementation: Any PSR-18 client (guzzlehttp/guzzle, symfony/http-client, php-http/curl-client). Auto-discovered when present; inject your own to control timeouts and instrumentation.
- psr/http-factory-implementation: Any PSR-17 factory set (nyholm/psr7, guzzlehttp/psr7, laminas/laminas-diactoros).
- spomky-labs/aes-key-wrap: For JWE body encryption only, and easy to miss: the ECDH-ES+A256KW suite cannot work without it, but web-token only *suggests* it, so installing the JOSE package alone leaves encryption broken.
- symfony/config: For the Symfony bridge's configuration tree, so a typo in zeroclick.yaml fails at container build.
- symfony/dependency-injection: For the Symfony bridge: wires SellerClient as a container service.
- symfony/http-foundation: For the Symfony bridge: the request type its raw-target extraction reads. Also enough on its own, without the bundle, if you only want correct target extraction.
- symfony/http-kernel: For the Symfony bridge's bundle.
- web-token/jwt-framework: For JWE body encryption only. Required to construct AffiliateCom\ZeroClick\Sellers\Encryption\BodyEncryptionService; nothing else in the package touches it. Install alongside spomky-labs/aes-key-wrap. web-token/jwt-library is a leaner alternative carrying the same Jose\Component classes.
README
Accept agent traffic forwarded by ZeroClick in a PHP API: verify that the traffic is genuine, check that the buyer's plan covers the work, do the work, and settle what was consumed.
⚠️ Unofficial package
This is an unofficial, third-party SDK published by Affiliate.com. It is not affiliated with or endorsed by ZeroClick, and ZeroClick does not produce, review, or support it. Nothing here is a ZeroClick asset or wordmark. If you need a vendor-supported client, ask ZeroClick for one — they ship TypeScript, Python, and Go.
What it does
An agent calls your API through ZeroClick. Four things have to happen, in this order, and this package does each of them:
| Step | What it means | How |
|---|---|---|
| Verify | The request really came from ZeroClick and reached you unaltered | HMAC over six canonical fields, locally, with no network call |
| Check | The buyer's plan covers the work you are about to do | POST /v1/usage/check, before anything billable starts |
| Serve | Your handler does the work | Nothing of ours in the way |
| Settle | Report what was actually consumed | On the response, or asynchronously afterwards |
A refused request is a decision, not an exception: the guard hands back a ready-to-return
401, 402, or 503, so your code reads as a guard clause rather than a try/catch
around ordinary traffic.
Requirements
- PHP 8.3 or later (tested on 8.3, 8.4, and 8.5)
- Any PSR-18 HTTP client and PSR-17 factories — yours, or auto-discovered
- No framework required. PSR-15 middleware, a Laravel bridge, and a Symfony bundle are included, and each is optional.
Install
composer require affiliatecom/zeroclick-sdk-sellers-php
If you have no PSR-18 client or PSR-17 factories installed yet, add one of each — for example:
composer require guzzlehttp/guzzle nyholm/psr7
Sixty-second quickstart
Configure the credentials ZeroClick issued you:
# Two values, not one: the key id, a colon, then the signing secret. ZEROCLICK_SIGNING_SECRETS=<key-id>:<signing-secret> ZEROCLICK_API_KEY=zc_live_your_api_key
The key id is the second half of the signing credential, issued beside the secret and shown with it in the ZeroClick dashboard. Pasting the secret in on its own is the most common setup mistake — see configuration.
Then wrap the billable route in the guard. One line of wiring performs the whole sequence in the order that matters, so no route can get that order wrong or forget the last step:
use AffiliateCom\ZeroClick\Sellers\Http\GuardMiddleware; use AffiliateCom\ZeroClick\Sellers\Usage\UsageItem; // $productWatch is your own PSR-15 handler; $client is the SellerClient. $guard = new GuardMiddleware($client, 'product-watch', [UsageItem::of('requests', 1)]); $response = $guard->process($request, $productWatch);
A denied request comes back as the refusal, the handler never runs, and nothing is billed.
A served one comes back carrying zc-usage, which is what turns the work into revenue.
The full wiring — building the client, the identity guard for free endpoints, ceilings, and asynchronous reports — is in the quickstart.
Three things that cost money quietly
Each of these is a silent, expensive failure rather than an error you will see in a log.
The default serves work you may never bill. When the allowance API gives no usable answer, this package serves the request anyway (
OutagePolicy::Allow), matching every reference SDK. That keeps a ZeroClick outage from becoming your outage, and the cost is traffic authorised by nobody that may never be paid for. Watch for it withonAllowanceUnavailable, or chooseOutagePolicy::Denyif refusing customers costs you less than serving them for free.
An unreported ceiling settles at zero. A
maxQuantityauthorises up to a limit and charges whatever amount you settle. Deliver the response without settling and that amount is zero — free for the buyer, unbilled for you, with no error anywhere. The report is what turns the work into revenue. Use a fixed quantity whenever the size is known before the work starts.
A normalised request target fails verification, and a proxy can normalise it before PHP sees it. ZeroClick signs the percent-encoded path and query exactly as it sent them. A framework — or an ingress — that decodes or reorders them breaks verification for those URLs only, in production only, presenting as an unexplained
401. This package detects the framework case and faults loudly. The proxy case no PHP change can fix: nginx and several managed load balancers normalise%2Fby default, and this can appear on a working deployment with no application change at all. See the raw request target for what to check.
Documentation
| Document | What is in it |
|---|---|
| Quickstart | Verify, check, serve, settle — end to end |
| Configuration | Every option: type, default, environment fallback, when to change it |
| API reference | Every public class and method, with signatures and examples |
| Errors | The fault hierarchy, decisions versus faults, every error code |
| Middleware | PSR-15, Laravel, and Symfony wiring |
| The raw request target | The highest-cost failure mode, and what to check |
| Read-only catalog | Sellers, services, meters, plans — unverified surface |
| Read-only analytics | Overview, revenue, transactions — unverified surface |
| Examples | Runnable PSR-15, Laravel, and Symfony integrations |
Limitations
Stated plainly, because the alternative is that you find out later.
- The catalog and analytics surfaces are unverified against a running server. Their paths, trailing slashes, and response shapes come from ZeroClick's OpenAPI document and nothing else; no shipped client exercises them. Everything else in this package is corroborated by at least two sources. Confirm them against a live sandbox before you depend on them.
- The body-encryption parameters were recovered from reference SDK source, not from a specification. ZeroClick documents none of that surface — the suite, the curve, and the custom reply-key header were read out of shipped code. The suite is pinned rather than negotiated, and the whole feature sits behind an optional dependency you install on purpose.
- Neither of those surfaces exists in any other ZeroClick SDK. Their presence here is a deliberate addition, not parity, and must not be read as a claim that a TypeScript, Python, or Go equivalent exists.
- Upstream is pre-1.0, and the reference SDKs already disagree with each other. The TypeScript, Python, and Go clients differ on environment-variable names, on where the service slug lives, and on how error codes are spelled. In places ZeroClick documents two incompatible conventions at once. Where they conflict this package follows the shipped code, and says so at the point it matters — in configuration, the API reference, and errors. Expect upstream to move.
- Management API writes, organizations, API-key management, Stripe Connect, sandbox control, and the agent-facing pay-URL surface are out of scope. Sellers never call them.
Versioning
Semantic Versioning. What the commitment covers — and what it deliberately does not — is stated in the changelog. Read that before pinning: the upstream protocol is outside our control, and we do not pretend otherwise.
Contributing
One command per task, and ci runs locally exactly what CI runs:
composer test # the PHPUnit suite composer analyse # PHPStan, level 10, strict rules, no baseline composer lint # formatting check composer fix # apply formatting composer ci # everything CI runs, in the same order
The suite is fully offline — every outbound call goes through a recording PSR-18 fake — so nothing here depends on an external service.
Documentation is executed, not reviewed. Every PHP sample on every page in docs/, and
in this README, is extracted and run against that same fake, so a signature change breaks
the build instead of quietly rotting the docs. Test coverage is measured by an actual run in
CI (composer test:coverage); no coverage figure is written here, because a number typed
into a file is not a measurement.
Licence
Apache-2.0. See LICENSE.