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

Teamleader Laravel Package

madeitbelgium/teamleader

A Laravel/PHP integration for the Teamleader API that lets you query and manage CRM data such as contacts, companies, deals and invoices from your application through a simple client, so you can sync Teamleader records without writing raw HTTP calls yourself.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the SDK and publish its config in one shot:

composer require madeitbelgium/teamleader
php artisan vendor:publish --tag=teamleader

Add credentials to .env (register the app in the TeamLeader developer portal first — you need a client_id, client_secret, and a registered redirect URI):

TEAMLEADER_CLIENT_ID=
TEAMLEADER_CLIENT_SECRET=
TEAMLEADER_REDIRECT_URI="${APP_URL}/teamleader/callback"

The package is a thin Laravel wrapper over the TeamLeader Focus REST API, so the first thing to read is the published config/teamleader.php — it tells you which connector/base URL the service is bound to and how tokens are resolved. Then skim the package's src/ for the facades and request classes (or just composer show madeitbelgium/teamleader to see the autoloaded namespace). The README examples usually cover the auth dance and one listing call; treat them as the canonical "first use case."

Your first working endpoint should be a read-only listing — e.g. contacts or the current account — because it exercises auth end to end without any side effects. Build a tiny tinker session:

use MadeITBelgium\TeamLeader\Facades\TeamLeader;
TeamLeader::contacts()->list();

If that returns data, OAuth and configuration are correct and you can layer the write flows (deals, invoices, time tracking) on top.

Implementation Patterns

Wrap the SDK in a domain service. Calls like TeamLeader::invoices()->create() get ugly inline. Put them behind app(Services\TeamLeader\InvoiceService::class) so controllers stay thin, retries/tests live in one place, and you have a seam to swap in a fake.

Treat the OAuth callback as a real flow, not a one-off. TeamLeader uses authorization-code OAuth2. Store access_token, refresh_token, and expires_at per user (or per tenant if you're multi-company), then refresh proactively before expiry — a 401 mid-sync is the most common production failure mode. A small TeamLeaderToken model with a refresh_token_if_expired() helper pays for itself immediately.

Queue anything that iterates. Bulk exporting contacts, syncing invoices, or reconciling time entries should all be dispatched jobs, not live request work. Combine a queued job per page with a cursor in the payload so a 429 doesn't lose progress.

Paginate explicitly. TeamLeader list endpoints return paged envelopes. Write one helper that accepts a closure and loops page/size until exhausted:

public function each(string $resource, callable $cb, int $size = 100): void {
    for ($page = 1; ; $page++) {
        $data = $this->client->{$resource}()->list(['page' => $page, 'size' => $size]);
        if (empty($data->items())) return;
        foreach ($data->items() as $item) { $cb($item); }
    }
}

Map payloads to your own models rather than passing SDK response objects around. TeamLeader field names change between API versions and snake/camel casing is inconsistent — one fromTeamLeader(array $data): Contact mapper keeps the rest of the app insulated.

Test with HTTP fakes. Because the package is Saloon-based under the hood, you can usually stub the underlying Http client with Laravel's Http::fake() or Saloon's recording. Record one real fixture per endpoint and replay it in a test that asserts your DTO mapping — the SDK's own tests are your best source of expected payload shapes.

Gotchas and Tips

  • Token lifetime is the trap. TeamLeader access tokens are short-lived. If your cron sync starts failing after an hour of working fine, you're not refreshing. Always check expires_at before every call, not once per process.
  • Rate limits are aggressive on bulk endpoints. Expect 429s on backfills. Use exponential backoff in a queued job and cap concurrency at 1 for a given tenant — two parallel syncs will thrash.
  • Pagination size is capped server-side. Passing size=1000 doesn't error, it silently returns the max (usually 100). Trust the returned items count, not your requested size, as the loop terminator.
  • Date handling. TeamLeader returns dates as YYYY-MM-DD strings and timestamps with timezone offsets. Don't new DateTime() and assume UTC — normalise with Carbon::parse()->tz('UTC') before storing, or you'll get off-by-one-day invoice dates.
  • The teamleader config key is global; the client is not. If you support multiple TeamLeader accounts (e.g. multi-tenant SaaS), don't rely on the singleton from the facade — instantiate the connector per account, or rebind the singleton on each request in a service provider / middleware.
  • Versions drift silently. With 14 stars this is a solo-maintained package; pin an exact version in composer.json rather than ^ if you're shipping. The release cadence (latest 2025-05-16) means breaking API changes may arrive with little fanfare. Watch the TeamLeader API changelog yourself.
  • Debug the HTTP layer first. When a request fails inexplicably, dump the last request/response via the underlying HTTP client (Saloon exposes $connector->getLastResponse(), or use Laravel's Http::fake() with a dd). The SDK's exceptions often wrap a generic message and hide the 400 body — the body is where the actual validation error lives.
  • Extension point: custom requests. If the package doesn't cover an endpoint you need, extend the base request class rather than dropping to raw Guzzle — you keep auth, retries, and token refresh for free, and upgrades are one file to check instead of many.
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.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr