spatie/laravel-harvest-sdk
A Laravel-friendly SDK for the Harvest.com API, with config publishing, credential setup, and container or facade access. It covers users and more, though not the full API—PRs welcome for missing features.
Architecture fit. The package is an idiomatic Laravel wrapper, not a domain layer: a container-bound Harvest entry point, a facade, a publishable config, and a set of readonly DTOs (UserData, ProjectData, TimeEntryData) hydrated from Harvest v2 JSON. It fits cleanly into a standard Laravel service architecture as a driven adapter at the edge of the application. It does not attempt to own persistence, queueing, or sync state, which is the correct scope for a library at this maturity. The one architectural constraint worth flagging early is that the package exposes transport concerns (pagination objects, raw DTO arrays) directly to callers — so the wrapper we build around it is what determines whether this stays a swappable dependency or becomes load-bearing.
Integration feasibility. High for the covered surface, unknown outside it. The README is explicit and unusually honest: this is not a full Harvest implementation. Documented and demonstrably working are users (me, all), projects (all), and time entries (paginated, date-bounded). Everything else — invoices, estimates, expenses, tasks, roles, clients/contacts, and all write operations including starting/stopping timers — is absent and must be hand-rolled against api.harvestapp.com/v2. Credentials are a static account ID plus a personal access token plus a required User-Agent (Harvest uses it for integration identification; a placeholder here is a production incident waiting to happen). Multi-tenant OAuth, token refresh, and webhook subscription are entirely out of scope for the package.
Technical risk. Ranked:
Http::withToken() fallback is an afternoon — but it is a recurring scope tax that should be budgeted per epic rather than discovered per sprint.spent_date semantics interact with account and user timezones. For anything touching payroll or invoicing reconciliation, off-by-one-day errors are silent and expensive to unwind.Key questions for the team before commitment.
src/ to confirm coverage rather than trusting the README.Stack fit. Requires Laravel with Composer; the package resolves through the service container, uses the framework's HTTP client, and honours standard config-caching behaviour. It will sit comfortably alongside existing Laravel 10/11/12/13 applications. The 2.1.0 Laravel 13 support commit tells us framework-version churn is being tracked, which is the single most important compatibility signal for a Laravel-only dependency.
Migration path. There is nothing to migrate from at install time; the migration concern is the exit ramp. Install behind a first-party interface from day one:
HarvestGateway (our interface)
├── SdkHarvestGateway → facade/container, covered endpoints
└── HttpHarvestGateway → raw HTTP fallback for uncovered endpoints
Controllers, jobs, and console commands depend on HarvestGateway, never on Spatie\Harvest\Facades\Harvest. This costs roughly half a day and buys three things: a documented seam for the uncovered 70% of the Harvest API, trivial mocking in tests, and the ability to replace or fork the package without touching call sites. It is the highest-leverage decision in this integration and should be non-negotiable in code review.
Compatibility. Pin the major version in composer.json and let Dependabot handle minors; the changelog is well-formed and releases are tagged, so upgrade review is cheap. Verify that the published config does not conflict with cached-config deployment steps, and confirm whether a base URL override exists — if it does not, integration testing must rely on Http::fake() rather than a local stub server, which is acceptable but constrains test design.
Sequencing.
User-Agent, resolve Harvest::users()->me()->dto(). A failure here is almost always a token/account mismatch, a junk user agent, or stale config cache.HarvestGateway abstraction in the same PR.Http::preventStrayRequests(). This is the only defence against Harvest drift.Maintenance. Low steady-state, spiky on Harvest API changes. The package's surface is small enough to read in an hour, so diagnosing a hydration failure is tractable even without upstream fixes. Budget a modest recurring allocation — one engineer-day per quarter — for dependency bumps, fixture refresh, and a coverage audit against the roadmap. The realistic maintenance tail is not "the library breaks"; it is "we slowly fork behaviour into HttpHarvestGateway until the package is a minority of our Harvest code," and that drift should be visible in a tracker rather than discovered during an incident.
Support. No commercial support, no SLA, no vendor escalation path. Internal support is entirely our own, mitigated by MIT and codebase size. The mitigating factor is Spatie's track record; the aggravating factor is that a 0-dependent package has no user community to surface regressions or contribute fixes upstream. Assume we are the maintainer of last resort.
Scaling. Two independent constraints. First, Harvest's per-account rate limits: the correct architecture is sync-into-local-tables with jobs and an overlapping window, never live fan-out from a dashboard — a page that queries Harvest per request will 429 in front of a client. Second, our ingestion volume: time entries are page-yielding, so jobs must iterate pages incrementally and reserve ->collect() for short ranges and interactive use; ->collect() on a multi-month backfill is an unbounded-memory event. Chunk backfills by month, dispatch one job per chunk, and bound every query by date — Harvest requires a from, which is a healthy forcing function. Note also that all() returns plain arrays of DTOs, not Collections; wrap deliberately and check return types rather than assuming Laravel collection semantics.
Failure modes. Enumerated with the operational response:
HARVEST_ACCOUNT_ID, or a malformed User-Agent. Alert on the smoke-test endpoint; this is the cheapest canary.Ramp-up. Genuinely fast — a mid-level Laravel engineer is productive in an afternoon using the README's users/projects/time-entries examples. The ramp-up that matters is not the API, it is the judgement: knowing that this is a partial client, that the DTOs are the real contract, that time entries must be bounded and synced rather than queried live, and that date semantics deserve paranoia. Put those four points in the onboarding doc and the ADR; they are not derivable from the README alone. Overall recommendation: adopt for read-only time-entry and project integration, conditional on the gateway abstraction shipping in the first PR and write operations being scoped as a separate, separately-estimated epic.
How can I help you explore Laravel packages today?