spatie/laravel-harvest-sdk
Laravel-friendly SDK for the Harvest.com API. Configure account ID, access token, and user agent, then resolve the Harvest client from the container or facade to call API endpoints. Not a complete implementation; PRs welcome.
Installation:
composer require spatie/laravel-harvest-sdk
Publish the config file:
php artisan vendor:publish --provider="Spatie\HarvestSdk\HarvestServiceProvider"
Configuration:
Update .env with your Harvest API credentials:
HARVEST_ACCOUNT_ID=your_account_id
HARVEST_TOKEN=your_api_token
First Use Case: Fetch a user’s time entries for today:
use Spatie\HarvestSdk\Facades\Harvest;
$timeEntries = Harvest::time()->entries()->today()->get();
Harvest::time(), Harvest::projects(), Harvest::invoices(), etc..env or config/harvest.php for API credentials and defaults.Harvest::time()->entries()->create([
'project_id' => 12345,
'task_id' => 67890,
'hours' => 2.5,
'notes' => 'Fixed bug in auth module',
'started_at' => now()->format('Y-m-d H:i:s'),
'ended_at' => now()->addHours(2.5)->format('Y-m-d H:i:s'),
]);
$entries = Harvest::time()->entries()->all();
foreach ($entries as $entry) {
if ($entry->hours > 8) {
Harvest::time()->entries()->update($entry->id, ['hours' => 8]);
}
}
$projects = Harvest::projects()->all();
$activeProjects = Harvest::projects()->active()->get();
Harvest::projects()->create([
'name' => 'New Client Project',
'code' => 'NCP',
'is_active' => true,
]);
$invoice = Harvest::invoices()->create([
'client_id' => 1,
'number' => 'INV-2023-001',
'due_date' => now()->addDays(14),
'line_items' => [
['kind' => 'Time', 'quantity' => 10, 'rate' => 75.00],
],
]);
Harvest::webhooks()->register(
url: route('harvest.webhook'),
events: ['time_entry.created', 'invoice.created']
);
routes/web.php):
Route::post('/harvest/webhook', [HarvestWebhookController::class, 'handle']);
class Project extends Model {
protected $casts = ['harvest_id' => 'integer'];
}
Harvest::time()->entries()->create([...])->later();
$projects = Cache::remember('harvest.projects', now()->addHours(1), function () {
return Harvest::projects()->all();
});
Rate Limiting:
429 Too Many Requests gracefully:
try {
$response = Harvest::time()->entries()->get();
} catch (\Spatie\HarvestSdk\Exceptions\RateLimitExceeded $e) {
sleep($e->retryAfter);
retry();
}
ID Mismatches:
account_id (not user ID) for scoping. Ensure your .env has the correct HARVEST_ACCOUNT_ID.Webhook Verification:
X-Harvest-Signature header. Validate it in your webhook handler:
use Spatie\HarvestSdk\Webhook;
public function handle(Request $request) {
if (!Webhook::verify($request->header('X-Harvest-Signature'), $request->getContent())) {
abort(403);
}
}
Time Zone Handling:
$entry->started_at->setTimezone('America/New_York');
Enable Debugging:
Add to config/harvest.php:
'debug' => env('HARVEST_DEBUG', false),
This logs API requests/responses to storage/logs/harvest.log.
Common Errors:
401 Unauthorized: Check HARVEST_TOKEN in .env.404 Not Found: Verify HARVEST_ACCOUNT_ID and resource IDs.422 Unprocessable Entity: Validate payloads against Harvest’s API specs.Custom API Clients:
Override the default Guzzle client in config/harvest.php:
'client' => function () {
return new \GuzzleHttp\Client([
'timeout' => 30,
'headers' => ['User-Agent' => 'MyApp/1.0'],
]);
},
Missing Endpoints: Extend the SDK by creating a custom facade or service:
class CustomHarvest extends \Spatie\HarvestSdk\Facades\Harvest {
public function customEndpoint() {
return $this->client->get('/custom/endpoint');
}
}
Testing:
Use the HarvestFake class for unit tests:
use Spatie\HarvestSdk\Testing\HarvestFake;
public function test_time_entries() {
HarvestFake::fake();
HarvestFake::timeEntries()->shouldReceive('get')->andReturn([]);
$entries = Harvest::time()->entries()->get();
$this->assertEmpty($entries);
}
Pagination: Handle paginated responses manually:
$entries = [];
$page = 1;
do {
$response = Harvest::time()->entries()->page($page)->get();
$entries = array_merge($entries, $response);
$page++;
} while (count($response) > 0);
How can I help you explore Laravel packages today?