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

Sellsy Api Laravel Package

ferjul17/sellsy-api

A Laravel/PHP client for the Sellsy API that simplifies authentication and requests to Sellsy CRM and billing services, making it easier to integrate contacts, companies, invoices, and other Sellsy resources into your application.

Deep Wiki
Context7

Getting Started

This is a plain PHP client for Sellsy's legacy v1 API, not a Laravel package — there is no service provider, no facade, and no config file. Treat it as a transport layer you wrap yourself. Because the repo is archived and the package predates Sellsy's API v2 migration, confirm viability before you invest.

1. Install and inspect

composer require ferjul17/sellsy-api

The repo name is unlisted in your metadata, so before writing a line of code:

  • Open composer.json in vendor/ferjul17/sellsy-api/ and note the autoload.psr-4 namespace — the examples below use Ferjul17\SellsyApi\SellsyApi, but confirm the actual class name and constructor signature from the source or README. Archived packages often drift from their docs.
  • Check the required PHP version and whether ext-curl / ext-json are declared. If the package pins php: ^5.6 or ^7.0, test it against your runtime before committing.
  • Skim src/ for the request/response methods. Most Sellsy v1 wrappers expose a small set of generic verbs (get, create, update, delete) plus resource-specific helpers rather than a full typed SDK.

2. Store credentials outside the repo

Sellsy v1 authenticates with four values (consumer key/secret, user token/secret), typically passed to the constructor:

// config/services.php
'sellsy' => [
    'consumer_key'    => env('SELLSY_CONSUMER_KEY'),
    'consumer_secret' => env('SELLSY_CONSUMER_SECRET'),
    'user_token'      => env('SELLSY_USER_TOKEN'),
    'user_secret'     => env('SELLSY_USER_SECRET'),
],

3. First use case: read something you can eyeball

Start with a read, not a write. Fetch a single company/client by ID and dd() the response so you learn the envelope shape (Sellsy v1 responses are nested under response and often double-encoded):

use Ferjul17\SellsyApi\SellsyApi; // verify namespace

$client = new SellsyApi(
    config('services.sellsy.consumer_key'),
    config('services.sellsy.consumer_secret'),
    config('services.sellsy.user_token'),
    config('services.sellsy.user_secret'),
);

$response = $client->get('Company.getById', ['companyid' => 12345]);
ray($response);

Once that round-trips, you have proven credentials, endpoint URL, and response parsing in one shot. Only then move to create/update.


Implementation Patterns

Bind it once, inject everywhere. Register a singleton in a service provider so you are not re-authenticating per call and so tests can swap the binding:

// app/Providers/SellsyServiceProvider.php
public function register(): void
{
    $this->app->singleton(SellsyApi::class, fn () => new SellsyApi(
        config('services.sellsy.consumer_key'),
        config('services.sellsy.consumer_secret'),
        config('services.sellsy.user_token'),
        config('services.sellsy.user_secret'),
    ));
}

Wrap the raw client in your own gateway. This is the single highest-value pattern with an archived dependency: your application code should never reference the vendor class directly. Define a SellsyGateway interface in your domain with the handful of operations you actually need (findCompany, createInvoice, syncContact), and implement it with the vendor client. When you migrate to Sellsy API v2 — or to a maintained fork — you rewrite one class instead of grepping the whole codebase.

interface SellsyGateway
{
    public function findCompany(string $sellsyId): ?CompanyData;
    public function createInvoice(InvoiceData $invoice): string;
}

final class LegacySellsyGateway implements SellsyGateway
{
    public function __construct(private SellsyApi $client) {}

    public function findCompany(string $sellsyId): ?CompanyData
    {
        $raw = $this->client->get('Company.getById', ['companyid' => $sellsyId]);

        return blank($raw['response'] ?? null)
            ? null
            : CompanyData::fromSellsy($raw['response']);
    }
}

Map responses into DTOs immediately. Sellsy v1 returns deeply nested associative arrays with snake_case-ish keys, numeric strings for IDs, and occasionally a {"0": ...} array wrapper when there is exactly one result. A fromSellsy(array $raw): self static constructor on a DTO isolates all that noise at the boundary and makes the rest of your app typed.

Push writes onto the queue. Writes are the slow, failure-prone half of any Sellsy integration. Wrap them in a job with retries and a unique key:

final class PushInvoiceToSellsy implements ShouldQueue
{
    use InteractsWithQueue, Queueable;

    public int $tries = 5;
    public array $backoff = [10, 60, 300, 900];

    public function __construct(public int $invoiceId) {}

    public function handle(SellsyGateway $sellsy): void
    {
        $invoice = Invoice::findOrFail($this->invoiceId);

        if ($invoice->sellsy_id) {
            return; // idempotency guard
        }

        $invoice->update(['sellsy_id' => $sellsy->createInvoice(
            InvoiceData::fromModel($invoice)
        )]);
    }

    public function uniqueId(): string
    {
        return 'sellsy-invoice-'.$this->invoiceId;
    }
}

Two-way sync: store the linkage first. Persist sellsy_id on your local models with a unique index before you attempt any update path. Every subsequent sync operation keys off that column, and uniqueness prevents the classic "duplicate invoice created by a retried job" bug.

Testing: fake the gateway, not the HTTP. Since SellsyApi is a concrete vendor class, mocking it with Mockery usually works but is brittle. Test your domain logic against the SellsyGateway interface and reserve one narrow integration test (behind an env flag) that hits a sandbox account for the real wiring.


Gotchas and Tips

The API version is the headline risk. Sellsy v1 (the apifeed.sellsy.com endpoints) was deprecated in favour of API v2, which uses OAuth2 and REST resources. An archived package will not follow that migration. Before building anything substantial, confirm with Sellsy whether your account still has v1 access, and treat this library as a bridge with a planned exit — not a foundation.

Archived means no security patches. Pin the version exactly in composer.json ("ferjul17/sellsy-api": "1.x.x" not "^1.0"), and vendor-review any transitive dependencies it drags in. If composer audit flags anything, you cannot upstream a fix.

Responses are inconsistently shaped. Sellsy v1 returns {"status":"success","response":...} where response may be a string, an object, or an array-of-one depending on cardinality. Normalise defensively:

$data = data_get($response, 'response', []);
$data = array_is_list($data) ? ($data[0] ?? null) : $data;

Errors often arrive as HTTP 200. Sellsy v1 signals some failures in the body ("status":"error", "error" keys) rather than via status code. Wrap every call in a helper that checks both the HTTP status and the body shape, then throws a single SellsyException your jobs can catch. Without this, failed writes silently appear successful.

IDs are strings, and they matter. companyid, invoiceid, etc. arrive as numeric strings. Casting to int is usually safe, but avoid loose comparisons (==) in lookups — "123" == "123abc" style surprises are real when Sellsy returns padded values. Store IDs as string columns.

Dates are French-locale and slash-formatted. Expect dd/mm/yyyy in several fields, not ISO 8601. Parse with Carbon::createFromFormat('d/m/Y', $value) explicitly; Carbon::parse() will silently misinterpret ambiguous values. Convert to ISO on the way in only if the field's own docs say so — often it does not.

Rate limits are undocumented and low. There is no reliable published quota for v1. Build in a throttle (a RateLimiter in your job middleware) and batch reads where the API supports it. During bulk imports, prefer chunked queued jobs over a tight foreach loop; a naive backfill of a few thousand records is the fastest way to get your account throttled or blocked.

Do not cache the client in a long-running process without re-auth checks. If you run Octane or a queue worker, a stale credential or expired session surfaces only at call time. Catch auth-shaped failures and let the job retry with a freshly resolved singleton rather than failing permanently.

Config caching will bite you. If you read credentials via env() outside a config file, php artisan config:cache returns null in production. Always route through config('services.sellsy.*').

Extension point of last resort: fork it. The class is small. If you need v2 support, a proxy setting, or better error handling, forking into app/Support/Sellsy (or a private VCS repo) is more realistic than patching vendor/. Keep the SellsyGateway interface stable and the rest is replaceable.

Log the raw request and response for the first month. With an unmaintained client and an undocumented API, structured logs (Log::debug('sellsy', ['method' => ..., 'payload' => ..., 'response' => ...]), with secrets redacted) are your only reliable debugging tool when a support ticket lands.

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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route