pirabyte/laravel-lexware-office
Laravel integration for the Lexware Office (lexoffice) API. Manage invoices, contacts, vouchers, and articles with an expressive, fluent interface, plus webhook support and configurable authentication for rapid accounting automation in PHP apps.
pirabyte/laravel-lexware-office is a Laravel wrapper around the Lexware Office (formerly lexoffice) REST API — the accounting/invoicing product used by most German small businesses. You reach for it when your app needs to push contacts, invoices, quotations, credit notes, articles or files into a tenant's bookkeeping, or receive webhooks when something changes there.
v0.4.0 is a compatibility release: the only change is Laravel 13 support (PR #46). No new endpoints, no deprecations, no breaking changes signalled — so if you're already on v0.3.9 the bump is safe, and if you were blocked on Laravel 13 this unblocks you. Note the package is still pre-1.0, so pin it ("pirabyte/laravel-lexware-office": "^0.4") and read CHANGELOG.md rather than the release notes before every minor bump.
composer require pirabyte/laravel-lexware-office
php artisan vendor:publish --tag="lexware-office-config"
LEXWARE_OFFICE_API_KEY=your-per-organisation-key
Get the key from Lexware Office under Settings → Public API — it is scoped to a single organisation, not to a user.
Where to look first: config/lexware-office.php for the API key/base URL wiring, and the resource classes under src/ — they map roughly one-to-one onto API domains (contacts, vouchers, files, event subscriptions). The README is the source of truth for the facade alias and method names; treat the class list as the real documentation.
First use case: create a contact, then a draft invoice against it. Drafts are the safe way to prove the integration end-to-end, because a draft voucher can be edited or discarded, whereas a finalised one is permanent (see Gotchas).
$contact = LexwareOffice::contacts()->create([
'company' => ['name' => 'Acme GmbH'],
'roles' => ['customer' => []],
]);
Wrap the package in your own service, don't sprinkle it through controllers. The package is a thin HTTP client; you want a LexwareBillingService (or similar) that your app depends on, so tests can fake it and so a future API change lands in one file.
Respect the voucher lifecycle. Lexware distinguishes draft → open → paid → voided, and the transition to finalised is one-way. Model it explicitly:
POST a draft voucher with type: invoice, taxType: net|gross|taxfree, line items and a shipping/paymentConditions block.GET .../document/{fileId}) if a human needs to see it.Keep the Lexware voucher ID on your own model (lexware_voucher_id, lexware_contact_id) so you can reconcile webhooks and re-fetch later.
Sync contacts idempotently. Search by email/company.name before creating, and store the ID. Lexware will happily create duplicate contacts otherwise, and merging contacts after the fact is manual work in their UI.
Handle webhooks as the primary update path, not polling. Register an event subscription for voucher.changed, contact.changed, etc.; the payload is thin (an ID and event type), so the job should re-fetch the resource and upsert. Verify the signature, dispatch to a queue, and make the handler idempotent — Lexware retries.
Cache reference data. Countries, tax rates, payment conditions and posting categories change rarely. Cache them for hours to a day; they're requested on every form render if you're not careful.
Queue everything that writes. Wrap creates/updates in jobs with $tries = 5 and exponential backoff — a 429 or a 502 during a bulk import should not fail a user's request.
File attachments are a two-step: upload bytes to the files endpoint, then reference the returned fileId on the voucher. PDF only.
Http::fake()) for tests. Check whether the config exposes an overridable base URL before you assume it does.composer why-not if the update refuses, and re-run your test suite — a framework major is where subtle container/HTTP-client behaviour shifts show up, even when the package diff is one line.Stornorechnung) — which is itself a voucher. Build a "cancel" path in your domain model early, or you'll be doing it in the Lexware UI under pressure.taxType (net/gross/taxfree), 19%/7% rates, paymentConditions as {paymentTermDuration, paymentTermLabel}, and mandatory contact roles. If you're building for non-DE customers, this is the wrong package.->middleware('throttle') on jobs, or RateLimited job middleware) rather than retrying blindly..env in the repo; rotate on staff departure.Http::globalRequestMiddleware) enabled only in non-production. Errors come back as JSON with a Message/IssueList shape — log the raw body, not just the exception message.How can I help you explore Laravel packages today?