bluetools/moneybird-php-api
A PHP client for the Moneybird accounting API with a Laravel service provider and facade. Manage contacts, invoices, payments and other resources through a clean, fluent interface, with typed resources, pagination and authentication handled for you.
Install with Composer (check the composer.json on the repo — the package is archived, so you may need to pin a version that matches your Guzzle constraint):
composer require bluetools/moneybird-php-api
Grab a personal API token from MoneyBird: Settings → API tokens, and note the administration ID you want to work against (visible in the MoneyBird URL: .../administrations/<id>/...).
Wire the client up — the package is a thin wrapper around Guzzle, so instantiation is just "token in, client out":
use Moneybird\Api\Api;
$api = new Api\Api($token);
// or, depending on version:
// $api = new Api('token');
Look here first, in this order:
README.md — the only trustworthy source of method names, because the archived package predates several MoneyBird API changes.src/Api/ — the entry point class and how it wires services.src/Service/ (or src/Service/*Service.php) — the per-resource classes (Contacts, SalesInvoices, Invoices, Payments, Products, TimeEntries, Documents). These tell you exactly which endpoints are wrapped.src/Object/ / src/Entity/ — the typed wrappers around JSON responses.First use case: pull contacts and create a sales invoice. This is the 80% case for a Dutch SMB integration:
$contacts = $api->contacts()->all();
$invoices = $api->salesInvoices()->all();
If that works, you're past the hard part. If all() returns an entity collection rather than plain arrays, iterate and inspect the first item before writing any mapping code.
Bind the client as a singleton in the container. The token and administration context are per-request state; do not new it up in every controller.
// AppServiceProvider
$this->app->singleton(\Moneybird\Api\Api::class, fn () => new \Moneybird\Api\Api(
config('services.moneybird.token')
));
Wrap the library behind your own repository. Every call site should go through MoneybirdContactRepository, MoneybirdInvoiceRepository, etc. The reason is not abstraction purity — it is that this package is archived and you will eventually need to swap it for the first-party SDK or raw Guzzle calls. Confining the blast radius to three classes makes that a one-day job instead of a three-week one.
class MoneybirdInvoiceRepository
{
public function __construct(private \Moneybird\Api\Api $api) {}
public function find(string $id): array
{
return $this->api->salesInvoices()->get($id);
}
public function create(array $payload): array
{
return $this->api->salesInvoices()->create($payload);
}
}
Treat responses as DTOs, not arrays. The entity objects returned by the package expose attributes via magic getters or a toArray()-style method. Convert once at the boundary into your own DTOs so nothing downstream depends on the package's shape.
Use the package for the write path, raw HTTP for the read-heavy paths. Listing thousands of invoices via the typed wrapper is slow and memory-hungry; paginate with small per_page values, or fall back to a direct Guzzle call for reporting endpoints the wrapper does not cover (there are several — the package is incomplete by modern standards).
Synchronisation pattern for invoices → payments:
filter on state:open or updated_at where supported.$api->salesInvoices()->payments($id) (or the equivalent nested service) and reconcile against your local ledger.moneybird_id + updated_at pair locally as your idempotency key — MoneyBird will give you duplicate webhook/retry traffic.Queue every write. MoneyBird enforces rate limits and returns 429 under load. Wrap creates/updates in a job with ->backoff([10, 60, 300]) and ->tries(5). Do not let a customer-facing HTTP request block on a MoneyBird call.
Testing: point the underlying Guzzle handler at a MockHandler (or use Http::fake() if you have abstracted to Laravel's client). Do not hit the real sandbox in CI — MoneyBird sandboxes are shared and mutable, so tests will interfere with each other.
packages/ if you want to patch it, and budget for replacement. Treat it as scaffolding for a working integration, not a long-term dependency.setAdministration() call, or a path segment the wrapper assembles for you. Read src/Api/ before debugging a mysterious 404 Not Found.invoices vs salesInvoices vs purchaseInvoices. MoneyBird distinguishes these hard. Sending a purchase invoice payload to the sales endpoint produces a validation error that reads like a schema problem; it is not, it is the wrong endpoint.details_attributes), tax rates, and ledger accounts all need exact MoneyBird field names and, frequently, the ID of the tax rate/ledger, not the human-readable name. Cache these lookup lists locally — they rarely change and fetching them on every invoice creation is wasteful."123.45". Never (float) cast before arithmetic; use bcmath or an integer-cents value object.2024-01-15T10:30:00+01:00). Parse with Carbon::parse(), store as UTC, and be aware MoneyBird reports in the administration's timezone.\Exception-based, not granular. Catch broadly around every call and log the response body — the package does not expose response bodies cleanly, so you may need to catch the Guzzle exception underneath it.nextPage() / hasMore(), check the README for the exact idiom; guessing here costs an afternoon.Api instance and add your own method that builds the Guzzle request. Do not fork the package in-place unless you are prepared to maintain a vendored copy.429 rather than hammering through all().How can I help you explore Laravel packages today?