beckonbilling / api-client
Official PHP client for the Beckon Billing REST API (v1).
Requires
- php: >=8.2
- ext-json: *
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.8
- nyholm/psr7: ^1.8
- phpunit/phpunit: ^10.5 || ^11.0
Suggests
- guzzlehttp/guzzle: A PSR-18 HTTP client, auto-discovered by php-http/discovery.
- nyholm/psr7: A lightweight PSR-7/PSR-17 message and factory implementation, auto-discovered.
- symfony/http-client: An alternative PSR-18 HTTP client, auto-discovered by php-http/discovery.
This package is auto-updated.
Last update: 2026-07-25 01:39:08 UTC
README
The official PHP client for the Beckon Billing
REST API (v1) - a small, typed, idiomatic wrapper over the public
/api/v1 surface: customers, article categories, articles, quotes, outbound
invoices and recurring invoices, plus user-token authentication.
- PSR-4 / PHP
>=8.2, no framework required. - Bring your own HTTP client - any PSR-18 client works and is auto-discovered.
- Typed exceptions you can branch on with a stable
error.key. - The canonical API contract lives in
openapi.yaml(import it into Postman/Insomnia). Building AI tooling on top? SeeAGENTS.md.
This is a thin, open-source HTTP wrapper - it holds no secrets and no server code. You still need a valid API token to do anything.
Installation
composer require beckonbilling/api-client
You also need any PSR-18 HTTP client + PSR-17 factories. If you don't already have one, add Guzzle (auto-discovered):
composer require guzzlehttp/guzzle
Quick start
require 'vendor/autoload.php'; use BeckonBilling\ApiClient\Client; $client = new Client([ 'token' => 'bbp_your_token', // organisation or user token 'base_uri' => 'https://portal.beckonbilling.com', // your portal host 'organisation' => 'organisation-uuid', // optional default scope ]); // List open invoices $page = $client->outboundInvoices->list(['status' => 'issued', 'paid' => 0]); foreach ($page as $invoice) { echo $invoice->public_index, ' - ', $invoice->gross_total, "\n"; } // Create a customer $customer = $client->customers->create([ 'label' => 'Musterfirma AG', 'customer_info' => "Musterfirma AG\nIndustriestraße 45\n1220 Wien", ]); // Download an invoice PDF file_put_contents('invoice.pdf', $client->outboundInvoices->pdf($invoice->id()));
Authentication
Every request sends Authorization: Bearer <token>. There are two token
flavours - both authorise through the same per-feature model:
Organisation tokens
Created in the portal under Firma -> API-Zugriff. Bound to one organisation;
carry an explicit permission grant + the send/bank capabilities. Pass the
bbp_... token as token.
User tokens (app sign-in)
Mint a token from email + password - no portal visit:
$client = new Client(['base_uri' => 'https://portal.beckonbilling.com']); // no token yet $session = $client->auth->login('user@example.com', 'secret'); // $session = ['token' => 'bbp_...', 'expires_at' => ..., 'user' => ..., 'organisations' => [...]] $authed = new Client([ 'token' => $session['token'], 'base_uri' => 'https://portal.beckonbilling.com', 'organisation' => $session['organisations'][0]['id'], ]);
If two-factor authentication is enabled, login() throws an
AuthenticationException whose getErrorKey() is tfa_required until you pass
a code:
$session = $client->auth->login('user@example.com', 'secret', [ 'tfa_code' => '123456', 'remember_device' => true, // returns a 30-day device_token ]);
Organisation scoping
Set a default organisation in the config, or override per call:
$client->customers->list([], ['organisation' => 'another-org-uuid']); $client->customers->list([], ['organisation' => null]); // send no organisation param
A user token with exactly one organisation may omit it entirely.
Resources
| Property | Entity | Feature |
|---|---|---|
$client->customers |
Customers | customers |
$client->articleCategories |
Article categories | articles |
$client->articles |
Articles | articles |
$client->quotes |
Quotes | quotes |
$client->outboundInvoices |
Outbound invoices | outbound_invoices |
$client->recurringInvoices |
Recurring invoices | recurring_invoices |
$client->auth |
User-token auth | - |
Each entity resource offers list(), autoPaging(), get(), create(),
update(), delete(). Quotes and invoices add lifecycle actions
(issue, send, convert/cancel, setPaid, pdf).
Pagination
Lists return a Collection (data, total, limit, offset; iterable and
countable):
$page = $client->customers->list(['limit' => 50, 'offset' => 0]); $page->total; // full server-side count $page->hasMore(); // more pages available?
Or iterate everything lazily:
foreach ($client->customers->autoPaging() as $customer) { // ... }
Error handling
Non-2xx responses throw a typed exception. Every one exposes
getStatusCode(), getErrorKey() (the stable slug) and getResponse().
use BeckonBilling\ApiClient\Exception\ApiException; use BeckonBilling\ApiClient\Exception\PermissionException; try { $client->outboundInvoices->issue($id); } catch (PermissionException $e) { if ($e->getErrorKey() === 'send_not_permitted') { // token lacks the `send` capability } } catch (ApiException $e) { // any other API error error_log($e->getStatusCode() . ' ' . $e->getErrorKey() . ': ' . $e->getMessage()); }
| Exception | Status |
|---|---|
AuthenticationException |
401 |
PermissionException |
403 (incl. send_not_permitted / bank_not_permitted) |
NotFoundException |
404 |
ConflictException |
409 |
ValidationException |
400 / 422 |
RateLimitException |
429 |
ServerException |
5xx |
TransportException |
network failure (no response) |
All extend ApiException, so a single catch (ApiException $e) covers everything.
Custom HTTP client
Inject any PSR-18 client / PSR-17 factories (for a preconfigured Guzzle, retries, proxies, timeouts, test doubles ...):
use GuzzleHttp\Client as Guzzle; use Nyholm\Psr7\Factory\Psr17Factory; $psr17 = new Psr17Factory(); $client = new Client([ 'token' => 'bbp_...', 'base_uri' => 'https://portal.beckonbilling.com', 'http_client' => new Guzzle(['timeout' => 10]), 'request_factory' => $psr17, 'stream_factory' => $psr17, ]);
Working models
Resource objects wrap the JSON payload: read documented fields as properties or array keys, and any field the API adds later stays reachable without a client upgrade.
$customer->label; // property access $customer['label']; // array access $customer->id(); // the UUID $customer->toArray(); // the full payload $invoice->isPaid(); // a few typed helpers on quotes/invoices
Contributing / development
composer install composer test # PHPUnit, fully mocked - no network
The API contract in openapi.yaml and the agent guide in AGENTS.md are part
of the release and are kept in sync with the API surface.
License
MIT - see LICENSE.