mfadul24/kount-commerce
Composer 安装命令:
composer require mfadul24/kount-commerce
包简介
PHP SDK for the Kount 360 Commerce APIs (Payments Fraud 2.0) — PSR-18/PSR-17 based, OAuth2 client-credentials with optional PSR-6 token caching, plus a legacy RIS migration facade.
README 文档
README
Modern, PSR-first PHP SDK for the Kount 360 Commerce APIs (Payments Fraud 2.0) — evaluate orders for fraud risk and receive a typed Approve / Decline / Review decision.
- PSR-18 / PSR-17 transport — bring your own HTTP client; nothing concrete is forced on you.
php-http/discoveryfinds an installed client automatically. - OAuth2 client-credentials built in — with an optional PSR-6 cache so the ~20-minute bearer token is reused across requests and processes.
- Typed, generated-from-OpenAPI models hydrated by
symfony/serializer— enums, nested objects and dates, no raw arrays. - One exception marker interface (
Kount\Exception\KountException) covering every failure mode. - Legacy RIS migration facade for consumers of the old
Kount/kount-ris-php-sdk.
Installation
The SDK requires a PSR-18 client and PSR-17 factories at runtime (declared as virtual packages). If your project doesn't already ship one:
composer require mfadul24/kount-commerce symfony/http-client nyholm/psr7
Any other PSR-18/PSR-17 implementation (Guzzle 7, httplug adapters, ...) works exactly the same.
Quickstart
use Kount\KountCommerceApi; use Kount\Models\Components\OrderPostBody; use Kount\Models\Components\Item; $kount = KountCommerceApi::builder() ->setOAuth('your-client-id', 'your-client-secret') ->build(); $result = $kount->orders->evaluate( OrderPostBody::for('ORDER-4711') ->withDeviceSession($deviceSessionId) // unique for >= 30 days ->addItem(new Item(name: 'Widget', quantity: 2)), riskInquiry: true, ); $decision = $result->order?->riskInquiry?->decision; // enum: Approve | Decline | Review $omniscore = $result->order?->riskInquiry?->omniscore; // float 0-99
Available operations on $kount->orders:
| Method | Endpoint |
|---|---|
evaluate(OrderPostBody $body, ?bool $riskInquiry, ?bool $excludeDevice, ?bool $excludeFromPaymentsModel) |
POST /v2/orders |
get(string $orderId) |
GET /v2/orders/{orderId} |
update(string $orderId, OrderPatchBody $body) |
PATCH /v2/orders/{orderId} |
batchUpdateReversals(BatchUpdateReversalsRequest $request) |
PATCH /v2/orders:batchUpdateReversals |
Credentials
Kount issues the OAuth2 client-credentials secret in one of two shapes, and the builder has a method for each:
| You were given | Use | Header the SDK sends |
|---|---|---|
| a discrete client id + client secret | setOAuth($clientId, $clientSecret) |
Basic base64("$clientId:$clientSecret") |
a single pre-encoded API key (the ready-made base64 of clientId:clientSecret) |
setApiKey($apiKey) |
Basic $apiKey — used verbatim |
Some tenants — for example those onboarded via Digistore24 — never receive a separate id/secret pair, only the encoded API key. Pass it straight through; the SDK does not decode or split it:
$kount = KountCommerceApi::builder() ->setApiKey('bXktY2xpZW50LWlkOm15LWNsaWVudC1zZWNyZXQ=') ->build();
setApiKey() behaves exactly like setOAuth() otherwise — same token endpoint, the same 401 → refresh → retry, and the same optional PSR-6 cache and tokenUrl: override:
$kount = KountCommerceApi::builder() ->setApiKey($apiKey, tokenCache: new FilesystemAdapter()) ->useSandbox() ->build();
Under the hood both route to Kount\Auth\ClientCredentialsTokenProvider, which stores only the encoded Basic credential:
use Kount\Auth\ClientCredentialsTokenProvider; ClientCredentialsTokenProvider::fromApiKey($apiKey, $tokenUrl); // pre-encoded key ClientCredentialsTokenProvider::fromClientCredentials($id, $secret, $tokenUrl); // id + secret pair
Token caching (PSR-6)
Without a cache the bearer token is memoized per process only — under PHP-FPM that means one token exchange per worker. Inject any PSR-6 pool to share the token (TTL is expires_in − 60s safety skew). The example uses FilesystemAdapter, which requires composer require symfony/cache:
use Symfony\Component\Cache\Adapter\FilesystemAdapter; $kount = KountCommerceApi::builder() ->setOAuth('your-client-id', 'your-client-secret', tokenCache: new FilesystemAdapter()) ->build();
On a 401 the SDK invalidates the cached token, re-authenticates and retries the request exactly once; a second 401 throws AuthenticationException.
Bringing your own HTTP client
Retry and timeout policy intentionally belong to your client, not the SDK:
use Symfony\Component\HttpClient\Psr18Client; use Symfony\Component\HttpClient\HttpClient; $kount = KountCommerceApi::builder() ->setClient(new Psr18Client(HttpClient::create(['timeout' => 10]))) ->setOAuth('your-client-id', 'your-client-secret') ->build();
setRequestFactory() / setStreamFactory() accept custom PSR-17 factories; setLogger() accepts any PSR-3 logger.
Tracing outbound requests
Need per-request traceability — duration, request body, response body — for every external call the SDK makes? Decorate your PSR-18 client. Every outbound call flows through the one client you inject, so a single decorator captures both the OAuth token POST and the /v2/orders calls (tell them apart by URL). Timing and traceability policy belong to the transport layer, exactly like retries.
The SDK ships Kount\Http\SensitiveDataRedactor so your trace never records a live credential: it masks the whole Authorization header (Basic on the token call, Bearer on order calls) and any JSON body key whose name contains token (the token endpoint's access_token). Everything else — order payloads, error documents, the token request's grant_type/scope form body — is left verbatim.
use Kount\Http\SensitiveDataRedactor; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; final class TracingClient implements ClientInterface { public function __construct( private readonly ClientInterface $inner, private readonly YourTraceRecorder $recorder, private readonly SensitiveDataRedactor $redactor = new SensitiveDataRedactor(), ) {} public function sendRequest(RequestInterface $request): ResponseInterface { $requestBody = (string) $request->getBody(); // (string) rewinds a seekable stream $start = microtime(true); $response = $this->inner->sendRequest($request); $durationSeconds = microtime(true) - $start; $this->recorder->record( method: $request->getMethod(), uri: (string) $request->getUri(), requestHeaders: $this->redactor->redactHeaders($request->getHeaders()), requestBody: $this->redactor->redactBody($requestBody), statusCode: $response->getStatusCode(), responseBody: $this->redactor->redactBody((string) $response->getBody()), durationSeconds: $durationSeconds, ); return $response; } } $kount = KountCommerceApi::builder() ->setClient(new TracingClient($inner, $recorder)) ->setOAuth('your-client-id', 'your-client-secret') ->build();
Two things to keep in mind:
- Seekable streams. Reading a body consumes its stream. The
(string)cast above seeks back to the start first (PSR-7__toString), and the SDK re-reads the response the same way — so as long as your PSR-18 client returns seekable streams (guzzle and nyholm do), reading for the trace doesn't starve the SDK. With a non-seekable stream you must buffer the body yourself. - Redaction is yours to keep. Only the values above are masked. If you also want to keep order PII (email, payment fields) out of traces, extend the redactor:
new SensitiveDataRedactor(sensitiveKeySubstrings: ['token', 'email', 'pan']), or pass extra header names via the first argument.
Sandbox
$kount = KountCommerceApi::builder() ->setOAuth('sandbox-client-id', 'sandbox-client-secret') ->useSandbox() ->build();
Warning
The default production/sandbox hosts (https://api.kount.com/commerce, https://api-sandbox.kount.com/commerce) and the OAuth token URL are taken from the OpenAPI spec plus Kount's public documentation but have not yet been verified against live credentials. If your Kount onboarding material lists different endpoints, override them with setServerUrl() and the tokenUrl: argument of setOAuth() / useSandbox(). Running the gated live smoke test (below) settles this.
Error handling
Every exception the SDK throws implements Kount\Exception\KountException:
| Exception | Meaning |
|---|---|
AuthenticationException |
Credentials rejected by the token endpoint, or persistent 401 after a forced refresh |
TransportException |
The PSR-18 client failed (network, DNS, timeout) — including token endpoint calls; original exception in getPrevious() |
ApiException |
Non-2xx API answer; carries statusCode, raw body, response and the typed error document: errorDetails (POST 400/500) or rpcStatus (all other error bodies) |
MalformedResponseException |
2xx answer that could not be decoded into the expected model |
UnsupportedFeatureException |
Legacy RIS feature without a 2.0 equivalent (facade only) |
InvalidArgumentException |
Pre-send validation (empty order id, missing credentials) — never reaches the network; extends \InvalidArgumentException |
use Kount\Exception\ApiException; use Kount\Exception\KountException; try { $result = $kount->orders->evaluate($body); } catch (ApiException $e) { $log->warning('Kount rejected the call', [ 'status' => $e->statusCode, 'correlationId' => $e->errorDetails?->correlationId, 'rpcMessage' => $e->rpcStatus?->message, ]); } catch (KountException $e) { // transport / auth / decoding problems }
The SDK parses responses strictly against the OpenAPI contract and fails closed: an unknown enum value or a number where the spec promises a string (e.g. the uint64-as-string amount fields) raises MalformedResponseException rather than degrading silently.
Auditability
Kount's cross-system trace id arrives as the X-Correlation-Id response header and is not part of any success model. After a call, $kount->orders->lastCorrelationId() returns the header of the most recent response on that instance (null when absent). It refers to the last call only and is not concurrency-safe across a shared instance — read it right after the call whose trace you want.
Migrating from the legacy RIS SDK
Kount\Legacy\RisClient offers familiar Inquiry / Response semantics on top of the 2.0 pipeline — see docs/MIGRATION.md for the full field mapping.
Upgrading between versions of this SDK? See docs/UPGRADING.md for breaking changes and migration steps.
use Kount\Legacy\Inquiry; use Kount\Legacy\RisClient; $ris = new RisClient($kount); $response = $ris->process( (new Inquiry()) ->setSessionId($deviceSessionId) ->setOrderNumber('ORDER-4711') ->setEmail('customer@example.com') ->setTotal(1999) // cents, as in legacy RIS ->setCurrency('EUR'), ); $response->getDecision(); // 'A' | 'D' | 'R' $response->getOmniscore(); // float
Legacy-only features (Khash payment encoding, masked payment tokens) throw UnsupportedFeatureException instead of silently doing nothing.
Development
composer install composer lint # ECS code style composer stan # PHPStan, level max (zero baseline) composer test # unit + mocked integration tests
The live sandbox smoke test never runs in CI; it is gated on env vars:
KOUNT_SANDBOX_CLIENT_ID=... KOUNT_SANDBOX_CLIENT_SECRET=... composer test:live
Optional: KOUNT_SANDBOX_SERVER_URL / KOUNT_SANDBOX_TOKEN_URL to override the unverified default endpoints.
About this codebase
The typed models were originally bootstrapped from docs/swagger.json with a code generator; the SDK is hand-maintained since and has no generator toolchain dependency. docs/swagger.json remains as the API contract reference.
Sponsoring
If this SDK saves you time, consider supporting its maintenance by sponsoring @mfadul24 on GitHub. ❤️
License
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 2
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-07-08