zeevx / php-bachs
A framework-independent PHP client for the Bachs API.
Requires
- php: ^8.2
- ext-json: *
- guzzlehttp/guzzle: ^7.8
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- laravel/pint: ^1.24
- pestphp/pest: ^3.5 || ^4.0
- phpstan/phpstan: ^2.1
README
A framework-independent, typed PHP client for the Bachs API. It provides checkout sessions, customers, customer portal sessions, subscriptions, API exceptions, and timestamped HMAC webhook verification without coupling your application to a framework or database schema.
Requirements
- PHP 8.2 or newer
- JSON extension
- Guzzle 7.8 or newer
Installation
composer require zeevx/php-bachs
Laravel applications can use zeevx/laravel-bachs for container bindings, configuration, a facade, and verified webhook delivery.
Creating a client
use Zeevx\Bachs\Bachs; use Zeevx\Bachs\Config; $bachs = new Bachs(new Config( apiKey: 'your-api-key', baseUrl: 'https://api.bachs.io', timeout: 30, ));
Use https://sandbox-api.bachs.io while developing and https://api.bachs.io in production.
Checkout sessions
Create a checkout from a Bachs product cart:
$checkout = $bachs->checkoutSessions()->create([ 'product_cart' => [ ['product_id' => 'product_123', 'quantity' => 1], ], 'customer' => [ 'email' => 'ada@example.com', 'first_name' => 'Ada', 'last_name' => 'Lovelace', ], 'success_url' => 'https://example.com/billing?success=1', 'cancel_url' => 'https://example.com/billing', 'billing_currency' => 'USD', 'allowed_payment_method_types' => ['card'], 'reference' => 'order:123', 'metadata' => [ 'order_id' => '123', ], ]); $checkout->id; $checkout->url; $checkout->status; $checkout->raw;
Retrieve an existing checkout:
$checkout = $bachs->checkoutSessions()->retrieve('checkout_123');
Checkout requests require a customer and exactly one pricing source: product_cart or pricing. Arrays remain supported, or you can validate and pass an explicit request object:
use Zeevx\Bachs\Data\CheckoutSessionRequest; $request = CheckoutSessionRequest::from([ 'product_cart' => [ ['product_id' => 'product_123', 'quantity' => 1], ], 'customer' => [ 'email' => 'ada@example.com', 'first_name' => 'Ada', 'last_name' => 'Lovelace', ], ]); $checkout = $bachs->checkoutSessions()->create($request);
The SDK sends supported payload keys unchanged, so Bachs API payloads remain snake_case and new optional fields can be used without waiting for an SDK release. Your application remains responsible for choosing the currency, products, prices, metadata, and allowed payment methods.
Customers
Create a customer:
$customer = $bachs->customers()->create([ 'email' => 'ada@example.com', 'name' => 'Ada Lovelace', 'reference' => 'user:42', 'metadata' => ['user_id' => '42'], ]);
Retrieve or update a customer:
$customer = $bachs->customers()->retrieve('customer_123'); $customer = $bachs->customers()->update('customer_123', [ 'name' => 'Ada Byron', ]);
Customer responses expose id, email, and the complete raw response.
Customer portal
Create a short-lived customer portal session:
$portal = $bachs->customers()->createPortalSession('customer_123', [ 'return_url' => 'https://example.com/billing', ]); header('Location: '.$portal->url);
Portal responses expose id, url, and raw.
Subscriptions
Retrieve a subscription:
$subscription = $bachs->subscriptions()->retrieve('subscription_123');
Update its product or metadata:
$subscription = $bachs->subscriptions()->update('subscription_123', [ 'product_id' => 'product_pro', 'metadata' => ['plan' => 'pro'], ]);
Cancel a subscription:
$subscription = $bachs->subscriptions()->cancel('subscription_123');
Subscription responses expose id, status, customerId, and raw.
Webhook verification
Bachs signs timestamp.raw_body with HMAC SHA-256. Always verify the exact raw request body before decoding or processing it.
use Zeevx\Bachs\Webhooks\WebhookVerifier; $verifier = new WebhookVerifier( secret: 'your-webhook-secret', tolerance: 300, ); $event = $verifier->verify( payload: $rawRequestBody, timestamp: $requestHeaders['X-Bachs-Timestamp'], signature: $requestHeaders['X-Bachs-Signature'], ); $event->id; $event->type; $event->createdAt; $event->organizationId; $event->data; $event->payload;
Verification rejects missing or invalid signatures, non-numeric timestamps, stale requests outside the configured tolerance, malformed JSON, and event payloads without an id or type.
Common event types include:
checkout.completedcheckout.expiredcollection.succeededcollection.failedcollection.underpaidcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.createdinvoice.paidinvoice.payment_failedcustomer.createdcustomer.updated
Bachs webhooks are delivered at least once. Persist each event ID and make application-side processing idempotent.
Error handling
All SDK exceptions extend Zeevx\Bachs\Exceptions\BachsException.
use Zeevx\Bachs\Exceptions\ApiException; use Zeevx\Bachs\Exceptions\BachsException; use Zeevx\Bachs\Exceptions\InvalidRequestException; use Zeevx\Bachs\Exceptions\TransportException; try { $checkout = $bachs->checkoutSessions()->retrieve('checkout_123'); } catch (ApiException $exception) { $exception->statusCode; $exception->response; $exception->getMessage(); } catch (InvalidRequestException $exception) { // A required checkout field or pricing source is invalid. } catch (TransportException $exception) { // DNS, connection, timeout, or invalid response JSON. } catch (BachsException $exception) { // Any other SDK failure. }
| Exception | Meaning |
|---|---|
ConfigurationException |
Missing API key, invalid base URL, or invalid timeout |
InvalidRequestException |
A checkout request is missing a customer or has an invalid pricing source |
ApiException |
Bachs returned an HTTP error; includes status and decoded response |
TransportException |
The request failed before a valid API response was available |
WebhookException |
Webhook configuration, signature, timestamp, or payload is invalid |
Testing with a custom client
Pass a configured Guzzle client as the second constructor argument when using MockHandler or another custom handler:
use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; $handler = new MockHandler([ new Response(200, [], json_encode([ 'checkout_id' => 'checkout_test', 'checkout_url' => 'https://checkout.bachs.io/checkout_test', ])), ]); $client = new Client(['handler' => HandlerStack::create($handler)]); $bachs = new Bachs(new Config('test-key'), $client);
Development
composer install
composer test
composer analyse
composer format
The package uses Pest, PHPStan level 8, Laravel Pint, strict types, and GitHub Actions across PHP 8.2, 8.3, and 8.4.
License
The MIT License. See LICENSE.