PHPackages                             billkit-eu/billkit-php - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. billkit-eu/billkit-php

ActiveLibrary[Payment Processing](/categories/payments)

billkit-eu/billkit-php
======================

Official PHP SDK for BillKit: a Stripe-Billing-shape multi-tenant SaaS API on Mollie.

v0.1.0(today)02↑2900%1Apache-2.0PHPPHP &gt;=8.1CI passing

Since Aug 25Pushed todayCompare

[ Source](https://github.com/billkit-eu/billkit-php)[ Packagist](https://packagist.org/packages/billkit-eu/billkit-php)[ Docs](https://billkit.eu)[ RSS](/packages/billkit-eu-billkit-php/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (7)Versions (2)Used By (1)

BillKit PHP SDK
===============

[](#billkit-php-sdk)

Official PHP SDK for [BillKit](https://billkit.eu), a Stripe-Billing-shape, multi-tenant SaaS billing API running on Mollie.

- **PHP 8.1+**, PSR-4, no hard runtime dependencies beyond `ext-curl` / `ext-json`.
- Full resource coverage, typed exception hierarchy, automatic retries with idempotency, cursor auto-pagination, and webhook signature verification.
- Bring-your-own PSR-18 HTTP client (Guzzle, Symfony HttpClient, ...) when you need custom transport behaviour.

Install
-------

[](#install)

```
composer require billkit-eu/billkit-php
```

Quick start
-----------

[](#quick-start)

```
use BillKit\BillKitClient;

$client = new BillKitClient('sk_test_...'); // or set BILLKIT_API_KEY

$customer = $client->customers->create([
    'email' => 'ada@example.com',
    'name'  => 'Ada Lovelace',
]);

$product = $client->products->create(['name' => 'Pro']);

$price = $client->prices->create([
    'product_id'   => $product['id'],
    'amount_cents' => 999,
    'currency'     => 'EUR',
    'interval'     => 'month',
]);

$session = $client->checkoutSessions->create([
    'customer_id' => $customer['id'],
    'price_id'    => $price['id'],
    'success_url' => 'https://app.example.com/done',
    'cancel_url'  => 'https://app.example.com/pricing',
]);
```

Every method returns the decoded JSON body as a plain associative `array`. The SDK deliberately ships no model classes so responses forward through your own data layer unchanged.

One-shot payments
-----------------

[](#one-shot-payments)

Charge a customer a single time without creating a mandate: no subscription, no renewal. Create the payment, redirect the shopper to `redirect_url`, then (optionally) refund it later. `refund_window_days` sets how long the charge stays refundable: `0` disables refunds, the default is `30`, the max is `365`.

```
$payment = $client->oneShotPayments->create([
    'customer_id'  => $customer['id'],
    'amount_cents' => 1999,
    'currency'     => 'EUR',
    'method'       => 'ideal',
    'success_url'  => 'https://app.example.com/done',
    'cancel_url'   => 'https://app.example.com/cart',
]);

header('Location: ' . $payment['redirect_url']); // send the shopper to pay

// The payment settles via the one_shot_payment.succeeded / .failed webhooks.
// Refund a settled one-shot payment (omit amount_cents for a full refund):
$client->refunds->create(['one_shot_payment_id' => $payment['id']]);
// ...or refund part of it. A charge can carry several partials:
$client->refunds->create(['one_shot_payment_id' => $payment['id'], 'amount_cents' => 500]);
```

Configuration
-------------

[](#configuration)

```
use BillKit\BillKitClient;
use BillKit\RetryPolicy;

$client = new BillKitClient(
    apiKey: 'sk_test_...',
    baseUrl: 'https://api.billkit.eu',          // override for self-hosted
    timeoutMs: 30_000,
    retryPolicy: new RetryPolicy(maxAttempts: 4),
    logger: $psrLogger,                          // opt-in; omitted = silent
);
```

The API key resolves from the constructor argument, falling back to the `BILLKIT_API_KEY` environment variable.

Auto-pagination
---------------

[](#auto-pagination)

List endpoints expose `all()` (one page) and `autoPagingIterator()` (a `Generator` that walks every page via the `has_more` + `starting_after`cursor protocol):

```
foreach ($client->customers->autoPagingIterator() as $customer) {
    echo $customer['id'], "\n";
}

// Server-side filters are first-class where the API supports them:
foreach ($client->events->autoPagingIterator(type: 'customer.created') as $event) {
    // ...
}
```

Error handling
--------------

[](#error-handling)

Non-2xx responses raise a typed subclass of `BillKit\Exception\BillKitException`, so you catch the case you care about instead of branching on status codes:

```
use BillKit\Exception\ResourceMissingException;
use BillKit\Exception\RateLimitException;
use BillKit\Exception\BillKitException;

try {
    $client->customers->retrieve('cus_missing');
} catch (ResourceMissingException $e) {
    // 404
} catch (RateLimitException $e) {
    sleep((int) ceil($e->retryAfter ?? 1));
} catch (BillKitException $e) {
    error_log($e->errorType . ': ' . $e->getMessage() . ' (request ' . $e->requestId . ')');
}
```

Hierarchy: `ApiConnectionException`, `AuthenticationException` (401), `PermissionException` (403), `ResourceMissingException` (404), `ConflictException` (409), `RateLimitException` (429), `InvalidRequestException`(4xx), `ServerException` (5xx), all extending `BillKitException`.

Retries &amp; idempotency
-------------------------

[](#retries--idempotency)

Transient failures (connection errors, 5xx, and 429 with a short `Retry-After`) are retried with jittered exponential backoff. Every mutating call is sent with an auto-generated `Idempotency-Key`, so a retried request never double-charges. Supply your own to coalesce retries across process restarts:

```
$client->refunds->create([
    'payment_id'      => 'pay_1',
    'idempotency_key' => 'refund-order-4711',
]);
```

Webhooks
--------

[](#webhooks)

Verify the `BillKit-Signature` header before trusting a webhook body:

```
use BillKit\Webhooks;
use BillKit\Exception\WebhookVerificationException;

try {
    $event = Webhooks::verifySignature(
        payload: file_get_contents('php://input'),
        signatureHeader: $_SERVER['HTTP_BILLKIT_SIGNATURE'] ?? null,
        secret: getenv('BILLKIT_WEBHOOK_SECRET'),
    );
} catch (WebhookVerificationException $e) {
    http_response_code(400);
    exit;
}

// $event is the decoded, verified payload.
```

Custom HTTP client (PSR-18)
---------------------------

[](#custom-http-client-psr-18)

By default the SDK uses a bundled curl transport. To route requests through your own PSR-18 client (for custom TLS, proxies, or connection pooling), inject it alongside PSR-17 factories:

```
use BillKit\BillKitClient;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Psr7\HttpFactory;

$factory = new HttpFactory();
$client = new BillKitClient(
    apiKey: 'sk_test_...',
    httpClient: new GuzzleClient(),
    requestFactory: $factory,
    streamFactory: $factory,
);
```

Logging (PSR-3)
---------------

[](#logging-psr-3)

The SDK is **silent by default**: it defaults to a `NullLogger` and writes nowhere, so it can't take over your application's logging. Inject any PSR-3 logger to opt in:

```
use BillKit\BillKitClient;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;

$log = new Logger('billkit');
$log->pushHandler(new StreamHandler('php://stderr', Logger::DEBUG));

$client = new BillKitClient(
    apiKey: 'sk_test_...',
    logger: $log,
);
```

```
billkit.DEBUG: BillKit request {"method":"POST","url":"https://api.billkit.eu/v1/customers","attempt":1,"max_attempts":3}
billkit.DEBUG: BillKit response {"method":"POST","url":".../v1/customers","status":503,"duration_ms":84,"request_id":"req_9f2a"}
billkit.WARNING: BillKit retrying {"method":"POST","url":".../v1/customers","reason":"HTTP 503","attempt":1,"delay_ms":500}
billkit.DEBUG: BillKit response {"method":"POST","url":".../v1/customers","status":200,"duration_ms":91,"request_id":"req_9f2b"}

```

- **debug**: one record per attempt, one per response (`status`, `duration_ms`, `request_id`; quote that id to support).
- **warning**: one record per retry, with the reason and the delay before the next attempt.

**Never logged:** your API key or the `Authorization` header; request and response **bodies** (they carry customer PII); the **query string** (list filters carry values like `email=`); only the path is logged. The final failure isn't logged either: it's thrown as a typed `BillKitException` carrying the status, request id and retry-after, and logging it here too would hand you a duplicate you can't suppress.

Using Laravel? The [`billkit-eu/billkit-laravel`](../laravel) package wires a log channel for you via `config/billkit.php`.

API surface
-----------

[](#api-surface)

Every resource is a property on the client. List resources expose `all()` (one page) and `autoPagingIterator()` (walk all pages).

`$client->...`Methods`customers`create, retrieve, update, delete, all, autoPagingIterator, setVatNumber, purge`products`create, retrieve, update, delete, all, autoPagingIterator`prices`create, retrieve, all, autoPagingIterator`checkoutSessions`create, retrieve`oneShotPayments`create, retrieve`subscriptions`retrieve, all, autoPagingIterator, cancel, pause, resume, reactivate, previewUpdate, update, reauthorizePaymentMethod`refunds`create, retrieve, all, autoPagingIterator`webhookEndpoints`create, retrieve, update, delete, rotateSecret, all, autoPagingIterator, allDeliveries, autoPagingIteratorDeliveries, retrieveDelivery, redeliver`events`retrieve, all, autoPagingIterator`tenant`capabilities, portalBranding, setPortalBranding, rotateProviderCredential`coupons`create, retrieve, update, delete, validate, all, autoPagingIterator`taxRates`create, retrieve, update, delete, all, autoPagingIterator`invoices`retrieve, all, autoPagingIterator`auditLogs`retrieve, all, autoPagingIterator`payments`retrieve, all, autoPagingIterator`billingPortalSessions`create, revokePlus `BillKit\Webhooks::verifySignature(...)` (static) for inbound webhooks.

Development
-----------

[](#development)

```
composer install
composer test      # PHPUnit
composer analyse   # PHPStan (level max)
composer cs        # php-cs-fixer (apply)
composer cs:check  # php-cs-fixer (dry-run)
```

License
-------

[](#license)

Apache-2.0

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance100

Actively maintained with recent releases

Popularity3

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity32

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/291105088?v=4)[BillKit EU](/maintainers/billkit-eu)[@billkit-eu](https://github.com/billkit-eu)

---

Top Contributors

[![nicholasamorim](https://avatars.githubusercontent.com/u/2200260?v=4)](https://github.com/nicholasamorim "nicholasamorim (2 commits)")

---

Tags

stripebillingmolliesubscriptionssaaseubillkit

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/billkit-eu-billkit-php/health.svg)

```
[![Health](https://phpackages.com/badges/billkit-eu-billkit-php/health.svg)](https://phpackages.com/packages/billkit-eu-billkit-php)
```

###  Alternatives

[tempest/framework

The PHP framework that gets out of your way.

2.3k37.6k21](/packages/tempest-framework)[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

86337.5k](/packages/flow-php-flow)[cakephp/cakephp

The CakePHP framework

8.9k20.0M1.9k](/packages/cakephp-cakephp)[drupal/core-recommended

Locked core dependencies; require this project INSTEAD OF drupal/core.

6943.5M450](/packages/drupal-core-recommended)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)[typo3/cms-core

TYPO3 CMS Core

3313.6M5.7k](/packages/typo3-cms-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
