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

Moneybird Php Client Laravel Package

picqer/moneybird-php-client

A PHP client for the Moneybird API that simplifies access to invoices, contacts, products, financial data, and administration resources. It handles authentication and API requests, making Moneybird integration straightforward for Laravel and PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and configure:

composer require picqer/moneybird-php-client

You need three credentials before you can do anything: client_id, client_secret, and a redirect_url. These come from a Moneybird app you register at moneybird.com/api-applications — the app defines your OAuth redirect URI once, and every administration you later want to touch has to grant it access.

First use case: pull the administrations the token can see.

use Picqer\Financials\Moneybird\Connection;
use Picqer\Financials\Moneybird\Moneybird;

$connection = new Connection();
$connection->setRedirectUrl(config('services.moneybird.redirect'));
$connection->setClientId(config('services.moneybird.client_id'));
$connection->setClientSecret(config('services.moneybird.client_secret'));

// Step 1 – send the user here
header('Location: ' . $connection->getAuthUrl());
exit;

// Step 2 – in your callback route
$connection->parseRedirectUrl();          // reads $_GET['code'] + state
$connection->requestAccessToken();        // exchanges code for token
$token = $connection->getAccessToken();   // persist this + refresh token

$moneybird = new Moneybird($connection);
$administrations = $moneybird->administration()->get();

Where to look first — in order:

  1. Connection — OAuth, token storage, HTTP transport, administration scoping.
  2. Moneybird — the facade that returns entity instances (contact, salesInvoice, product, ledgerAccount, taxRate, etc.).
  3. Entities/ — every model class; read the $fillable and $supportedMethods properties to know what a model can actually do.
  4. Entities/SalesInvoice — the deepest entity; read it first if you need to create invoices, not just read data.

Once you can list administrations, the very next step is $connection->setAdministrationId($id) — nothing else works until that's set.

Implementation Patterns

Wrap the connection in a service. Moneybird is one of those APIs where "get a token" is a 3-step dance that happens once per administration, not once per user. A small singleton is worth it:

class MoneybirdService
{
    public function forAdministration(string $moneybirdId): Moneybird
    {
        $account = MoneybirdAccount::where('administration_id', $moneybirdId)->firstOrFail();

        $connection = new Connection();
        $connection->setClientId(config('services.moneybird.client_id'));
        $connection->setClientSecret(config('services.moneybird.client_secret'));
        $connection->setRedirectUrl(config('services.moneybird.redirect'));
        $connection->setAccessToken($account->access_token);
        $connection->setAdministrationId($moneybirdId);

        return new Moneybird($connection);
    }
}

Store tokens per administration, not per user. A single user can link multiple administrations, and each grant has its own access token. Model this as moneybird_accounts(administration_id, access_token, refresh_token, expires_at, user_id) and key on the administration ID.

Refresh before every call. Moneybird access tokens expire — a setAccessToken() from yesterday silently 401s. Either:

if ($account->expires_at->isPast()) {
    $connection->setRefreshToken($account->refresh_token);
    $connection->refreshAccessToken();
    $account->update([...]);
}

…or catch the exception, refresh, and retry. The retry approach is more robust to clock drift.

Reading and creating — same fluent API:

$moneybird = $this->forAdministration($id);

// Create a contact
$contact = $moneybird->contact();
$contact->fill([
    'company_name' => 'Acme BV',
    'firstname'    => 'Jan',
    'lastname'     => 'Jansen',
    'email'        => '[email protected]',
    'country'      => 'NL',
]);
$contact->save();               // ID is written back to the model
echo $contact->id;

// Query with filters + pagination
$contacts = $moneybird->contact()
    ->filter(['query' => 'acme'])      // supported per-entity
    ->page(1)
    ->perPage(100)
    ->get();

Building a sales invoice is where most projects spend their time. You need a tax_rate_id from the administration's own tax rate table, and line items are nested arrays, not entities:

$rates = $moneybird->taxRate()->get();           // cache per administration
$vat21 = collect($rates)->firstWhere('percentage', 21);
$product = $moneybird->product()->filter(['query' => 'SKU-1'])->get()->first();

$invoice = $moneybird->salesInvoice();
$invoice->fill([
    'contact_id'         => $contact->id,
    'reference'          => 'ORDER-1042',
    'invoice_date'       => now()->format('Y-m-d'),
    'currency_id'        => 'EUR',
    'prices_are_incl_tax'=> false,
    'details' => [[
        'description'  => 'Consultancy',
        'amount'       => '1',
        'price'        => '100.00',
        'tax_rate_id'  => $vat21->id,
        'ledger_account_id' => $product->ledger_account_id,
    ]],
]);
$invoice->save();

Sending is a two-step async flow. save() creates a draft. Sending the invoice fires a background job on Moneybird's side; you cannot assume the PDF is ready when send() returns. Poll salesInvoice()->find($invoiceId) and check state === 'sent', or register a webhook and listen for invoice_state_changed.

Sync pattern — pull, diff, push. For dashboards, treat Moneybird as the source of truth for issued invoices but keep a local read model. Use ->filter(['updated_since' => $lastSync]) and store updated_at per entity. Do full reads, not incremental diffs, for contacts — Moneybird deduplicates softly and you will get dupes back if you delete locally.

Webhooks. Register them via $moneybird->webhook()->save() with a URL; verify the payload's signature server-side before trusting anything.

Gotchas and Tips

  • The administration scope is mandatory. A fresh Moneybird instance without setAdministrationId() returns 404 on almost every entity, and the error message is not obvious. Make it impossible to construct a client without one.
  • Tokens are not portable between administrations. If you store one token per user, you'll get 401s the moment a client with two administrations tries to switch. Key everything on administration_id.
  • fill() silently ignores unknown keys. If a field you "set" isn't in the entity's $fillable, nothing happens and no error is raised. When an invoice saves but a field is empty, check $fillable first.
  • Nested line items bypass fill(). details for invoices (and custom_fields for contacts) are arrays of arrays; each line item must have its own tax_rate_id. A 400 with "Invalid tax rate" almost always means you pulled VAT IDs from a different administration's cache.
  • save() mutates the model. After a successful save the entity has an id, and subsequent fill() + save() calls will update rather than insert. Keep a fresh instance per record in loops, or you'll overwrite the previous row.
  • delete() requires a persisted entity. Calling it on an unsaved model returns false without throwing.
  • Moneybird rate-limits aggressively (roughly 100 requests / 5 minutes per administration in the public tier — check the current docs, it changes). Wrap bulk syncs in RateLimiter or a small usleep; a naive contact loop on a 5k-customer administration will 429 on you.
  • Date and decimal formatting matters. Dates are Y-m-d strings, never Carbon. Prices must be strings with 2 decimals ("100.00", not 100 or 100.0). Cast them before fill().
  • Async sends need webhooks, not sleep(). The PDF generation is not synchronous and there is no documented polling interval that's safe to rely on. Register invoice_state_changed and gate downstream actions on it.
  • toArray() on an entity returns the last-known payload, not a fresh fetch. After a webhook fires, call ->find($id) if you need current state — the webhook body is often partial.
  • Testing is painful without a fake. The package has no built-in test doubles; either mock the Connection HTTP layer or point setBaseUrl() at a local mock server. Do this early — the OAuth dance alone makes integration testing expensive.
  • Connection is stateful and not thread-safe. Don't reuse one across administrations in the same request; construct a new one per administration ID. It's cheap.
  • Extension point when an entity is missing: subclass Picqer\Financials\Moneybird\Model and set $namespace, $endpoint, and $fillable following the pattern of existing entities. You can then access it via $moneybird->getConnection()->... manually — the Moneybird facade won't know about it, but the underlying Connection::get() / post() work fine.
  • Laravel users: don't reach for a first-party SDK. There isn't one, and community wrappers lag behind. The package is small and framework-agnostic — a ServiceProvider that binds MoneybirdService is ~20 lines and far easier to debug than a wrapper.
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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource