Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Bexio Laravel Package

codebar-ag/laravel-bexio

Laravel Bexio by codebar AG offers a fluent, Laravel-friendly integration with the Bexio Swiss business software API, letting you manage contacts, invoices, orders, and accounting data through a clean, expressive interface backed by Eloquent-style models.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Confirm the support matrix before anything else. The package is Laravel-native but the release you get is v14 of the package, not of Laravel. Read composer.json on the tag you intend to install and check require for laravel/framework, php, and guzzlehttp/guzzle. If your application is not inside that matrix, stop — this is a framework upgrade, not an integration.

2. Install and publish.

composer require codebar-ag/laravel-bexio
php artisan vendor:publish --tag=bexio-config   # confirm the tag in the provider
php artisan config:clear

Vendor publishing tags in this package family are usually bexio-config (and sometimes bexio-migrations); if the tag is wrong the command fails loudly, which is the good failure mode. Confirm the actual tag by reading src/*ServiceProvider.php in vendor/codebar-ag/laravel-bexio.

3. Read four files, in this order. This package has thin public documentation, so the source is the documentation:

  • config/bexio.php — the credentials shape, base URL, and any timeout/retry keys. This is your integration contract.
  • src/*ServiceProvider.php — what is bound in the container, what is aliased, whether there is a facade.
  • The client class (src/Client.php or equivalent) — how the HTTP layer is constructed, whether it takes a token, a config array, or a token store.
  • src/Resources/ (or src/Endpoints/) — the resource-oriented surface the description advertises. This directory is your API map; anything not in it you will write yourself.

4. First use case: read-only contact lookup. Do not open with an invoice write. Start with a read that proves auth, base URL, and response hydration in one shot:

use CodebarAg\Bexio\Facades\Bexio; // verify exact namespace/alias

$contacts = Bexio::contacts()->all(); // indicative; check resource method names

If the facade does not exist, resolve the client from the container instead:

$bexio = app(\CodebarAg\Bexio\BexioClient::class);

Getting this read working end-to-end on a sandbox token — before touching any write endpoint — is the single most valuable half-hour you will spend on this package.

Implementation Patterns

Inject the contract, not the facade. Facades are convenient in controllers and tinker, but for anything that is queued or unit-tested, bind and inject the client (or the resource class) through the constructor. Facades resolved at static call sites are painful to substitute in tests and hide the dependency from static analysis. If the package exposes a contract interface, alias it in a service provider and inject that:

// AppServiceProvider::register()
$this->app->bind(BexioClientContract::class, fn ($app) => $app->make(BexioClient::class));

Keep a thin local adapter between the package and your domain. The highest-leverage pattern with any third-party API client is a one-class-wide boundary:

final class BexioContactGateway
{
    public function __construct(private BexioClient $client) {}

    public function findOrCreateByEmail(string $email): ContactDto
    {
        // package calls live here, nowhere else
    }
}

This means a v15 bump, a base-URL change, or an endpoint rename touches one file. Given the package ships major bumps with empty release notes, this boundary is not architectural purity — it is change insurance.

Resource-per-domain is the intended shape. Rather than dropping to raw HTTP, use the resource accessors (contacts(), invoices(), items(), accounts() — verify the exact list in src/Resources). Reserve raw requests for endpoints the package has not covered. Most such clients expose the underlying PendingRequest or Guzzle client; that is your extension point.

Paginate with an explicit loop, not a ->all(). Bexio's list endpoints are paginated and the client will usually expose limit/offset (or page) options. Wrap it once:

$offset = 0;
do {
    $page = $bexio->contacts()->all(['limit' => 500, 'offset' => $offset]);
    foreach ($page as $contact) { /* … */ }
    $offset += 500;
} while (count($page) === 500);

Never assume a bare call returns everything. A silently truncated sync is the classic integration bug here.

Push writes into jobs with a rate-limit middleware. Any bulk create/update — importing 2,000 contacts, pushing 300 invoices — belongs in a queued job with RateLimited middleware, not a synchronous controller loop. Bexio enforces per-minute request limits; a 429 mid-import with no retry strategy leaves you half-synced.

public function middleware(): array
{
    return [(new RateLimited('bexio'))->releaseAfter(60)];
}

Define the limiter in a service provider to match the plan on your Bexio tenant — do not guess the number.

Cache reference data hard. Tax rates, chart-of-accounts entries, currencies, units — these change rarely and are fetched constantly during invoice assembly. A Cache::remember('bexio.accounts', now()->addDay(), ...) around them removes most of your API volume. Invalidate on a schedule or a manual command, not on every request.

Test with Http::fake(), not against the live API. Because the package is Guzzle/HTTP-facade based, you can assert the outgoing request shape without a token:

Http::fake(['*/contact*' => Http::response([['id' => 1, 'name_1' => 'Acme']], 200)]);

$result = app(BexioContactGateway::class)->findOrCreateByEmail('[email protected]');

Http::assertSent(fn ($req) => str_contains($req->url(), '/contact'));

Fake at the HTTP layer rather than the facade — it exercises the package's own hydration and error mapping, which is exactly the code you do not control and therefore most need covered.

Model the token as an application concern. The client needs a valid access token; token storage, refresh, and rotation are your application's job (a bexio_tokens table, a settings row, or a cache entry keyed by tenant). Resolve the client per-tenant in the container rather than as a singleton if you serve multiple Bexio organisations.

Gotchas and Tips

The v14 release notes are empty — treat the upgrade as undocumented. Pin the version explicitly in composer.json ("codebar-ag/laravel-bexio": "14.*"), and before adopting a new major, diff the tags in vendor/ or on GitHub. Budget half a day for that diff review; it is the only reliable change summary you will get. Do not let a composer update move this package's major version as a side effect of unrelated work.

Config keys that rename will not throw. Laravel merge-config semantics mean a renamed key in config/bexio.php silently falls back to the package default. If the default base URL points at production while you expected sandbox, you find out by writing real invoices. After publishing, re-read the config file against the package's shipped defaults and diff them.

config:cache will freeze your credentials. If you read credentials from env() inside your own code rather than the config file, deployments that cache config will silently produce null tokens. Always go through config('bexio.…'), and remember to re-run php artisan config:cache after any credential change — this bites everyone once.

Money is not a float. Bexio amounts are decimals with currency-specific scale. Round only at the boundary, use brick/math or string arithmetic, and never (float) a total on the way in or out. Hydration through the package's DTOs may give you strings or floats depending on the version — assert the type in a test rather than assuming.

Dates and timezones. Bexio is a Swiss product and its date fields are date-only strings in local business time. Converting to Carbon with an app timezone and back can shift an invoice date by a day. Keep date-only fields as strings until the point you actually need date math.

429s and 5xxs need differentiated handling. A 429 is retryable after a delay; a 422 is a validation error you should surface to the user, not retry; a 401 means your refresh logic failed. Map the package's exception types once in your adapter and log the response body — the body is where Bexio puts the actual reason ({"error": {"message": ...}}), and it is the first thing you will want when a write fails.

Partial updates use PUT with a full payload in many Bexio endpoints. Sending only the changed field can null out other fields. Read-modify-write, and do it inside a job so a concurrent edit does not clobber. If the resource exposes PATCH, verify per-endpoint — the two behave differently across Bexio's API generations.

Suppress the echo on bidirectional sync. If you both push and pull records, your own writes will come back as "changes" on the next poll. Stamp a source or an updated_by on your side and skip records you just wrote, or you will build a feedback loop that inflates API usage until the rate limit stops it.

Log the request, never the token. Enable Guzzle request logging behind a debug flag, but add a middleware that redacts Authorization and any refresh token in the body. A stray Log::debug($response) on an auth endpoint is a credential leak in your log aggregator.

Debugging checklist when something looks wrong but nothing throws: dump the resolved base URL (config('bexio.base_url') or equivalent) — wrong environment is the most common cause; then Http::fake() in a test to confirm the URL and payload shape you think you are sending; then check the raw response body for a 200 carrying an error object.

Extension point: drop to the underlying client. For endpoints the package has not wrapped, most Bexio Laravel clients expose ->client() or accept a raw path. Use that rather than adding a second HTTP client with its own auth — one token, one retry policy, one place to instrument.

No published support window means plan for annual review. Put a calendar reminder to check the package's releases and Laravel support matrix once a year. An unowned minor dependency with silent majors is how you end up doing a two-major jump under deadline. composer outdated --direct in CI, with this package explicitly flagged, is cheap protection.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels