composer require spinen/laravel-clickup — but check the composer.json constraints first. The last release was 2021‑05‑25 and it sits on top of spinen/laravel-integration, so on Laravel 9+ you will likely need --ignore-platform-reqs or a fork. Verify before you build anything on it.php artisan vendor:publish --provider="Spinen\ClickUp\ClickUpServiceProvider" (or --tag=config). You get config/clickup.php; the one value that matters is the API token. Put it in .env as CLICKUP_API_TOKEN and never commit it — ClickUp personal tokens grant full workspace access.vendor/spinen/laravel-clickup/src/ is small. Look at the Facades/ directory for the entry point (usually Spinen\ClickUp\Facades\ClickUp), then the resource/model classes to see which endpoints are actually wrapped. Cross-reference with spinen/laravel-integration — that base package owns the fluent get()/create()/update()/delete() mechanics and the HTTP client, so most "how do I call X" questions are answered there.use Spinen\ClickUp\Facades\ClickUp;
$tasks = ClickUp::tasks()->where('list_id', $listId)->get();
Treat that snippet as a shape to verify, not gospel: the exact method names and where list_id belongs (query param vs. path segment) differ across versions of the base package. dd() the result on day one.
Wrap the facade, never call it from controllers. The facade is a static-ish service locator; sprinkle it through your app and you lose testability and the ability to swap in a fake. Put it behind one class:
final class ClickUpClient
{
public function tasksForList(string $listId): Collection
{
return ClickUp::tasks()
->where('list_id', $listId)
->where('archived', false)
->get();
}
}
Everything downstream depends on ClickUpClient, so you can bind a stub in tests and in local dev.
Sync into Eloquent, then read from Eloquent. ClickUp is slow (network + rate limits) and is not your query engine. Pull on a schedule, upsert into a clickup_tasks table keyed by ClickUp's task id, store the raw payload in a json column. Your UI then joins, filters, and paginates locally. This also gives you a replayable history when ClickUp's API shape shifts under a stale package.
Queue every write and every bulk read. A SyncListTasks job per list, SyncWorkspace as a batch of those. Use ->withoutOverlapping() on the scheduled command and a per-token rate limiter so a large workspace doesn't trip ClickUp's 100 requests/minute/token ceiling:
RateLimiter::for('clickup', fn () => Limit::perMinute(90));
Webhooks are the real-time path — but verify them yourself. Register the webhook via the API (or the ClickUp UI), point it at a route, and compute the HMAC SHA256 of the raw request body using the webhook secret to check the signature field. Do not trust $request->all() for hashing — $request->getContent() only. Then dispatch a job that re-fetches the task by ID; the webhook payload is a hint, not a source of truth.
Resolve IDs out of band. Lists, folders, spaces, and custom field IDs are opaque. Keep a config/clickup.php section (or a small clickup_resources table) mapping human names to IDs so nobody is pasting raw IDs into code.
Testing. Because the package sits on Guzzle via laravel-integration, Http::fake() may not intercept it. Use Guzzle's MockHandler/HandlerStack if the client is injectable, otherwise bind a fake ClickUpClient in the container and reserve one narrow integration test against a sandbox workspace for the HTTP layer.
"repositories": [{"type": "vcs", ...}]) and patch the client yourself. The package is thin — forking is cheap.composer require fails on Laravel 9/10/11, it's spinen/laravel-integration holding it back, not the ClickUp code. Check both before concluding it's unmaintainable.config:cache will bite you. Any token read at service-provider boot time is frozen into the cached config. Read config('clickup.*') lazily inside the client, not in the provider's register().[{id, name, type, value}] with types that vary wildly (labels, dropdowns, currency, relationships). Normalize into a JSON column plus a few extracted keys you actually filter on — do not try to map every type eagerly.subtasks, include_closed, and archived are flags people forget, then file "missing task" bugs. Make them explicit arguments on your client methods rather than implicit defaults.ClickUpClient) — don't wait for upstream. Anything the package doesn't wrap (Goals, Views, Time Tracking v2) is a five-line method on your own client, and keeping it there also insulates you when you eventually drop the dependency.How can I help you explore Laravel packages today?