codearachnid / check-commerce-laravel-sdk
Laravel integration for the Check Commerce (OBP Link) API — a service provider, publishable config, facade, cache-backed token store and testing fake around the codearachnid/check-commerce-php-sdk client.
Package info
github.com/codearachnid/check-commerce-laravel-sdk
pkg:composer/codearachnid/check-commerce-laravel-sdk
Fund package maintenance!
Requires
- php: ^8.3
- codearachnid/check-commerce-php-sdk: ^0.1
- illuminate/cache: ^12.0||^13.0
- illuminate/contracts: ^12.0||^13.0
- illuminate/support: ^12.0||^13.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.8
- larastan/larastan: ^3.9
- laravel/pao: ^1.0
- laravel/pint: ^1.29
- orchestra/testbench: ^10.0||^11.0
- pestphp/pest: ^4.6
- pestphp/pest-plugin-laravel: ^4.1
- pestphp/pest-plugin-type-coverage: ^4.0
- phpstan/extension-installer: ^1.4
Suggests
- guzzlehttp/guzzle: The recommended PSR-18 HTTP client for the Check Commerce SDK, auto-discovered when installed
This package is auto-updated.
Last update: 2026-08-01 19:16:14 UTC
README
Check Commerce for Laravel
Laravel integration for the Check Commerce (OBP Link) API — ACH, RTP, paper check and IAT payments, stored consumers, recurring subscriptions, hosted payment pages, batch processing and merchant boarding.
Everything that talks to the API lives in codearachnid/check-commerce-php-sdk. This package is the Laravel half of it:
- Configured like the rest of your app — a publishable config file mapping
CHECK_COMMERCE_*environment variables onto every SDK option. - Resolved from the container —
CheckCommerceClientis a singleton you can inject, plus aCheckCommercefacade. - Tokens shared across processes — bearer tokens live in Laravel's cache and expire with the token itself, so workers stop re-authenticating one at a time.
- Transport you can replace — bind your own PSR-18 client and PSR-17 factories and the SDK uses them.
- Testable without HTTP mocks —
CheckCommerce::fake()queues responses and records requests in feature tests.
Requirements
- PHP 8.3+
- Laravel 12 or 13
- A PSR-18 HTTP client and PSR-17 factories (Guzzle is recommended and auto-discovered)
Installation
composer require codearachnid/check-commerce-laravel-sdk guzzlehttp/guzzle
The core SDK is pulled in as a dependency; you never install it separately. Omit guzzlehttp/guzzle if your application already ships a PSR-18 client — the SDK discovers whatever implementation is installed.
Publish the config file:
php artisan vendor:publish --tag="check-commerce-laravel-sdk-config"
Then add your credentials:
CHECK_COMMERCE_API_KEY=your-api-key CHECK_COMMERCE_MERCHANT_NUMBER=999997 CHECK_COMMERCE_ENVIRONMENT=sandbox # Optional CHECK_COMMERCE_BASE_URL= CHECK_COMMERCE_API_VERSION=1.0 CHECK_COMMERCE_TIMEOUT=30 CHECK_COMMERCE_CONNECT_TIMEOUT=10 CHECK_COMMERCE_MAX_RETRIES=2 CHECK_COMMERCE_TOKEN_EXPIRY_MARGIN_SECONDS=60 CHECK_COMMERCE_TOKEN_CACHE_STORE=redis CHECK_COMMERCE_TOKEN_CACHE_PREFIX=check-commerce
Every key in config/check-commerce.php is documented in place, including scopes, retry backoff and the HTTP client toggle.
Usage
Inject the client anywhere the container builds your class:
use CheckCommerce\CheckCommerceClient; class CheckoutController { public function __construct(private CheckCommerceClient $checkCommerce) {} public function store(Request $request) { $result = $this->checkCommerce->transactions->debit([...]); } }
Or reach for the facade:
use CheckCommerce\Laravel\Facades\CheckCommerce; CheckCommerce::transactions()->debit([...]);
The facade exposes the SDK's services as methods — transactions(), consumers(), subscriptions(), batches(), hostedPages(), boarding() — plus authenticate(). Injected clients read them as properties ($client->transactions); both return the same objects.
Credentials are validated the first time the client is resolved, not during boot, so an application missing CHECK_COMMERCE_API_KEY still runs its migrations and queue workers.
Transactions
use CheckCommerce\Enums\PaymentType; use CheckCommerce\Laravel\Facades\CheckCommerce; $result = CheckCommerce::transactions()->debit([ 'merchantNumber' => config('check-commerce.merchant_number'), 'amount' => 42.50, 'referenceNumber' => 'INV-1001', 'consumerInfo' => [ 'name' => 'Jane Doe', 'bankAccountNumber' => '1234567890', 'bankRoutingNumber' => 121000248, ], ]); $result->transactionId; // 123456789 $result->status?->value; // "Processed" // Credits, voids and refunds work the same way: $merchantNumber = config('check-commerce.merchant_number'); CheckCommerce::transactions()->credit([...]); CheckCommerce::transactions()->void(['merchantNumber' => $merchantNumber, 'originalTransaction' => ['transactionId' => 123456789]]); CheckCommerce::transactions()->refund(['merchantNumber' => $merchantNumber, 'originalTransaction' => ['referenceNumber' => 'INV-1001']]); // Any rail, any transaction type: CheckCommerce::transactions()->create([...], PaymentType::Rtp); // Status lookups: $status = CheckCommerce::transactions()->status(transactionId: 123456789); if ($status->isDeclined()) { report(new RuntimeException($status->processingFailure?->detail ?? 'Declined')); }
Consumers
$created = CheckCommerce::consumers()->create([ 'name' => 'Jane Doe', 'email' => 'jane@example.com', 'bankAccountNumber' => '1234567890', 'bankRoutingNumber' => 121000248, ]); $consumer = CheckCommerce::consumers()->retrieve($created->consumerId); CheckCommerce::consumers()->update($created->consumerId, ['phoneNumber' => '5125551234']); // Charge a stored consumer: CheckCommerce::transactions()->debit([ 'merchantNumber' => config('check-commerce.merchant_number'), 'amount' => 42.50, 'consumerInfo' => ['consumerId' => $created->consumerId], ]); // List endpoints page lazily: foreach (CheckCommerce::consumers()->list(['city' => 'Austin'])->autoPagingIterator() as $consumer) { // ... }
Subscriptions
use CheckCommerce\Enums\SubscriptionEndCode; use CheckCommerce\Enums\SubscriptionStatus; use CheckCommerce\Enums\TransactionType; $created = CheckCommerce::subscriptions()->create([ 'startTime' => now()->addMonth()->startOfMonth()->toDateTimeImmutable(), 'amount' => 25.00, 'schCode' => 'Monthly:1', 'endCode' => SubscriptionEndCode::Indefinite, 'transactionType' => TransactionType::Debit, 'status' => SubscriptionStatus::Active, 'consumerInfo' => ['consumerId' => $consumerId], ]); CheckCommerce::subscriptions()->update($created->subscriptionId, ['amount' => 30.00]);
Hosted payment pages
$link = CheckCommerce::hostedPages()->createLink([ 'customer' => ['name' => 'Jane Doe', 'email' => 'jane@example.com'], 'order' => [ 'subTotal' => 89.95, 'tax' => 10.00, 'total' => 99.95, 'returnURL' => route('checkout.thanks'), ], 'orderItems' => [ ['name' => 'Widget', 'quantity' => 1, 'price' => 89.95], ], ]); return redirect()->away($link->url);
Batches
use CheckCommerce\Enums\FileDelimiter; $merchantNumber = config('check-commerce.merchant_number'); $batch = CheckCommerce::batches()->submit([ ['merchantNumber' => $merchantNumber, 'transactionType' => 'Debit', 'amount' => 42.50, 'consumerInfo' => [...]], ['merchantNumber' => $merchantNumber, 'transactionType' => 'Debit', 'amount' => 19.99, 'consumerInfo' => [...]], ]); // Or upload a file: $batch = CheckCommerce::batches()->uploadFile(storage_path('app/batches/today.csv'), FileDelimiter::Comma); CheckCommerce::batches()->status($batch->batchId)->status?->value; // "Pending" | "Processing" | "Processed" | "Declined"
Merchant boarding
$result = CheckCommerce::boarding()->board(['merchants' => [/* boarding records */]]); foreach ($result->boardingFailures as $failure) { Log::warning('Boarding failed', [ 'company' => $failure->companyName, 'detail' => $failure->processingFailure?->detail, ]); }
Errors
Every API failure is a typed exception from the core SDK — ValidationException, AuthenticationException, AuthorizationException, NotFoundException, RateLimitException, ServerException, ApiException and TransportException, all implementing CheckCommerce\Exception\CheckCommerceException:
use CheckCommerce\Exception\ApiException; use CheckCommerce\Exception\ValidationException; try { CheckCommerce::transactions()->debit([...]); } catch (ValidationException $e) { return back()->withErrors(collect($e->getValidationErrors()) ->mapWithKeys(fn ($error) => [$error->property => $error->detail]) ->all()); } catch (ApiException $e) { Log::error($e->getMessage(), [ 'status' => $e->getStatusCode(), 'code' => $e->getErrorCode(), 'correlation_id' => $e->getCorrelationId(), ]); throw $e; }
Token caching
The SDK requests a bearer token on the first API call and refreshes it before expiry. This package stores that token in Laravel's cache instead of in process memory, and the cache entry expires exactly when the token does.
That matters as soon as your application runs more than one PHP process — web workers, queue workers, scheduled commands. With per-process storage each of them authenticates separately, and keeps re-authenticating after every deploy or worker restart. With a shared store they issue one token between them.
'token_cache' => [ 'store' => env('CHECK_COMMERCE_TOKEN_CACHE_STORE'), // null uses your default cache store 'prefix' => env('CHECK_COMMERCE_TOKEN_CACHE_PREFIX', 'check-commerce'), ],
Use redis, memcached or database in production. The file store works on a single server; array is per-request and effectively disables sharing. The cache key is derived from your base URL, merchant number, API key and scopes, so multiple merchants in one application never collide.
Warm the token at deploy time — and fail loudly on bad credentials — with:
CheckCommerce::authenticate();
Testing
CheckCommerce::fake() swaps the transport for a recording PSR-18 client. Everything else — your config, the container binding, the token store — stays exactly as it is in production:
use CheckCommerce\Laravel\Facades\CheckCommerce; use Psr\Http\Message\RequestInterface; it('charges the customer at checkout', function () { $checkCommerce = CheckCommerce::fake(); $checkCommerce->queueJson(200, [ 'transactionId' => 123456789, 'status' => 'Processed', ]); $this->post('/checkout', ['amount' => 42.50])->assertRedirect(); $checkCommerce->assertSentCount(1); $checkCommerce->assertSent(fn (RequestInterface $request): bool => json_decode( (string) $request->getBody(), true )['request']['amount'] === 42.50); });
- Responses are replayed in the order they are queued;
queue()also accepts aThrowableto simulate a transport failure. - Queue a non-2xx response to exercise your error handling — the SDK still maps it to its typed exception.
- The fake answers the SDK's authentication call itself, so the queue and the recorded requests only ever hold the calls your application made. Call
withoutAuthentication()when the authentication request is what you are testing. - Credentials are stubbed when your testing environment has none, so
phpunit.xmlneeds no Check Commerce variables. - Assertions:
assertSent(),assertNotSent(),assertSentCount(),assertNothingSent(). Raw requests are on$checkCommerce->requests, withlastRequest()andrequestCount()as shortcuts.
Replacing the HTTP client
Bind a PSR-18 client (or PSR-17 factories) in the container and the SDK uses it — handy for proxies, logging middleware or a shared connection pool:
$this->app->bind(\Psr\Http\Client\ClientInterface::class, fn () => new \GuzzleHttp\Client([ 'handler' => $stackWithYourMiddleware, ]));
Set check-commerce.http_client.from_container to false to ignore container bindings and let the SDK discover and build its own client.
Contributing
composer install composer test # static analysis, formatting, type coverage and tests composer lint # apply formatting composer build # build the Testbench workbench composer serve # serve the workbench app
The package develops against Orchestra Testbench. composer serve boots a throwaway Laravel application from workbench/ with the package already registered, which is the quickest way to try a change against a real app.
See our contributing guide to get started.
Changelog
Please see CHANGELOG for more information on what has changed recently.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
Check Commerce for Laravel is open-sourced software licensed under the MIT license.