PHP
Framework-agnostic client with a bundled curl transport and an optional PSR-18 seam.
Install
PHP 8.1 or newer, with ext-curl and ext-json. No other hard runtime dependency: the PSR interfaces are there so you can swap the transport, not because a client is bundled.
Initialise
Named arguments throughout. The key falls back to the BILLKIT_API_KEY environment variable.
<?php
use BillKit\BillKitClient;
use BillKit\RetryPolicy;
$client = new BillKitClient(
apiKey: 'bk_test_...',
baseUrl: 'https://api.billkit.eu',
timeoutMs: 30_000,
retryPolicy: new RetryPolicy(maxAttempts: 4),
); Making calls
Every resource is a readonly property on the client: customers, products, prices, checkoutSessions, oneShotPayments, subscriptions, refunds, disputes, webhookEndpoints, events, tenant, coupons, taxRates, invoices, auditLogs, payments, billingPortalSessions.
Every method returns the decoded JSON body as a plain associative array. The SDK ships no model classes, so responses forward through your own data layer unchanged.
$customer = $client->customers->create([
'email' => 'ada@example.com',
'name' => 'Ada Lovelace',
]);
echo $customer['id']; Auto-pagination
List resources expose all() for one page and autoPagingIterator() for a Generator that walks every page over the has_more and starting_after cursors.
foreach ($client->customers->autoPagingIterator() as $customer) {
echo $customer['id'], "\n";
}
foreach ($client->events->autoPagingIterator(type: 'customer.created') as $event) {
handle($event);
} Idempotency and retries
Connection errors, 5xx and short Retry-After 429s are retried with jittered exponential backoff. Every mutating call is sent with an auto-generated Idempotency-Key, so a retry never doubles a charge. Pass your own to make that hold across process restarts.
$client->refunds->create([
'payment_id' => 'pay_9XKp2vQ1',
'idempotency_key' => 'refund-order-4711',
]); Errors
Non-2xx responses throw a subclass of BillKit\Exception\BillKitException, so you catch the case you care about instead of branching on status codes. Each carries errorType, requestId and retryAfter where the API sent one.
use BillKit\Exception\BillKitException;
use BillKit\Exception\RateLimitException;
use BillKit\Exception\ResourceMissingException;
try {
$client->customers->retrieve('cus_gone');
} catch (ResourceMissingException $e) {
return null;
} catch (RateLimitException $e) {
sleep((int) ceil($e->retryAfter ?? 1));
} catch (BillKitException $e) {
error_log($e->errorType . ': ' . $e->getMessage() . ' (request ' . $e->requestId . ')');
} The hierarchy is ApiConnectionException, AuthenticationException (401), PermissionException (403), ResourceMissingException (404), ConflictException (409), RateLimitException (429), InvalidRequestException (other 4xx) and ServerException (5xx).
Webhooks
<?php
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;
} Read the body raw. A framework that has already parsed and re-encoded it will not verify.
Bring your own HTTP client
The bundled curl transport is the default. For custom TLS, proxies or connection pooling, inject any PSR-18 client with its PSR-17 factories.
use BillKit\BillKitClient;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Psr7\HttpFactory;
$factory = new HttpFactory();
$client = new BillKitClient(
apiKey: 'bk_test_...',
httpClient: new GuzzleClient(),
requestFactory: $factory,
streamFactory: $factory,
); Logging
The SDK defaults to a NullLogger and writes nowhere. Inject any PSR-3 logger to opt in: you get one debug record per attempt and per response, and one warning per retry. Your API key, request and response bodies and the query string are never logged.
BillKit