exlo89/laravel-sevdesk-api
Laravel wrapper for the SevDesk API, giving you a clean, fluent interface to manage contacts, invoices, orders, and other accounting data. It handles authentication and requests so you can focus on building your app instead of raw HTTP calls.
This is the first tagged release (v0.1.0) of a Laravel client for the sevdesk API. Do not treat it as production-hardened: the surface area is currently a basic API handler plus a contact resource, and the maintainer has explicitly signalled this is an initial, exploratory cut.
Minimal path from zero to a working call:
composer require the package (it is a v0.x release — expect the API to move under you, so pin an exact version).php artisan vendor:publish --tag=...) and check what it expects: typically a base URL and an API token..env, not in a committed config file, and reference it via config() inside the package config. Never read env() directly from your own service classes — config caching will silently return null.app(...)) rather than instantiating it — that is the extension point for faking and for swapping base URLs per environment.The first real use case is almost always the same: sync your users/customers table to sevdesk contacts so that invoices can later be issued against them. Start with a one-way, read-only reconciliation — pull contacts once and log what comes back — before you write a single create() call. sevdesk contacts double as customers and suppliers depending on category, and getting that wrong in a live account is painful to unwind.
Look first at: the service provider (what is bound, what is singleton-scoped), the config file (base URL, token, timeout, retry), and the contact resource class (which methods exist and what they return — array, DTO, or a fluent builder). Those three files tell you 90% of how the package wants to be used.
Wrap the package, don't sprinkle it. Put the package behind your own thin service — App\Services\Sevdesk\ContactSync — that takes the package resource in its constructor. v0.1.0 will change; your call sites should not.
final class ContactSync
{
public function __construct(private ContactResource $contacts) {}
public function upsert(User $user): int
{
return $this->contacts->create([
'surename' => $user->last_name, // yes, sevdesk really spells it this way
'familyname' => $user->first_name,
'customerNumber' => (string) $user->id,
'category' => ['id' => config('sevdesk.category.customer'), 'objectName' => 'Category'],
])['id'];
}
}
Queue the writes. Anything that creates or updates remote records belongs in a job with $tries, backoff(), and ShouldBeUnique keyed on the local model ID. sevdesk is an accounting system: a duplicated contact from a retried job is a real mess for whoever does the books.
Persist the remote ID locally. Add a sevdesk_contact_id column (nullable, indexed, unique). Every subsequent operation should look up by that ID rather than by name-matching, which fails the moment two customers share a surname.
Normalise the response at the boundary. Whatever the handler returns, convert it to a typed value object or a plain array with a fixed shape before it hits your domain. When the package's return type changes in v0.2, you fix one class.
Test with Http::fake(). Because the handler presumably sits on Laravel's HTTP client, you can fake by URL pattern and assert the outgoing payload — which is the only way to be confident about field names like surename, familyname, and customerNumber without touching a live account.
Use a separate sevdesk account for staging if you can, and a token scoped to it. There is no meaningful sandbox for most of these endpoints; your test data lands in real bookkeeping.
Cache read-heavy lookups. Contact categories, currency lists, and tax rules change rarely. Cache them for the process or an hour — this also cuts your request count against the rate limit.
0.x may break on minor bumps. Pin "vendor/package": "0.1.*" and read the diff before bumping. Given the release notes describe the API handler as "basic", assume it will be refactored.200-adjacent shape containing a messages array while objects is null. Validate that objects is present and non-empty before indexing into it — a naive $response['objects'][0]['id'] will throw an unhelpful Undefined array key deep in your sync job.depth is a footgun. Requesting deep embedding to save round-trips can blow up the payload size and slow responses. Ask for depth=1 and fetch related objects explicitly when you actually need them.limit/offset until a short page comes back, and don't assume limit=1000 is honoured.Bearer). If you get 401s with a token you know is valid, check the config for how the handler builds the header before you start rotating keys.401 with the token logged into your exception tracker is a credential leak. Wrap the handler's exceptions in your own exception type so you can add context (endpoint, payload keys) without dumping the whole body.Http::fake() is your only seam, and that constrains your testing strategy.main before working around it — and if you patch, upstream it; the maintainer is clearly at the start of the journey and early feedback is cheap.How can I help you explore Laravel packages today?