BillKit/Docs Console
SDK

PHP

Framework-agnostic client with a bundled curl transport and an optional PSR-18 seam.

v0.4.0 PHP 8.1+ Beta Source

Install

composer require billkit-eu/billkit-php

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
<?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.

php
$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.

php
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.

php
$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.

php
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
<?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.

php
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.