zeevx/php-bachs

A framework-independent PHP client for the Bachs API.

Maintainers

Package info

github.com/zeevx/php-bachs

pkg:composer/zeevx/php-bachs

Transparency log

Statistics

Installs: 14

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

0.1.0 2026-08-02 11:06 UTC

This package is auto-updated.

Last update: 2026-08-02 11:15:48 UTC


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.completed
  • checkout.expired
  • collection.succeeded
  • collection.failed
  • collection.underpaid
  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted
  • invoice.created
  • invoice.paid
  • invoice.payment_failed
  • customer.created
  • customer.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.