Hey there, fellow code wrangler! Ready to dive into the world of time tracking with Harvest? In this guide, we'll walk through building a robust Harvest API integration using PHP. Harvest's API is a powerhouse for managing time entries, projects, and more. Let's get our hands dirty and build something awesome!
Before we jump in, make sure you've got:
Got all that? Great! Let's roll.
First things first, let's set up our project:
mkdir harvest-integration && cd harvest-integration composer init composer require guzzlehttp/guzzle
We're using Guzzle to make our HTTP requests nice and easy.
Harvest uses OAuth 2.0 for authentication. Here's a quick way to get your access token:
<?php require 'vendor/autoload.php'; $client = new GuzzleHttp\Client(); $response = $client->post('https://id.getharvest.com/oauth2/token', [ 'form_params' => [ 'grant_type' => 'client_credentials', 'client_id' => 'YOUR_CLIENT_ID', 'client_secret' => 'YOUR_CLIENT_SECRET', ] ]); $token = json_decode($response->getBody())->access_token;
Keep that token safe; we'll need it for all our requests!
Let's make a simple GET request to fetch your user info:
$response = $client->get('https://api.harvestapp.com/v2/users/me', [ 'headers' => [ 'Authorization' => 'Bearer ' . $token, 'Harvest-Account-Id' => 'YOUR_ACCOUNT_ID', ] ]); $user = json_decode($response->getBody()); echo "Hello, " . $user->first_name . "!";
Now, let's get our hands dirty with some CRUD operations:
$response = $client->post('https://api.harvestapp.com/v2/time_entries', [ 'headers' => [ 'Authorization' => 'Bearer ' . $token, 'Harvest-Account-Id' => 'YOUR_ACCOUNT_ID', ], 'json' => [ 'project_id' => 12345, 'task_id' => 6789, 'spent_date' => '2023-05-15', 'hours' => 2.5, ] ]);
$response = $client->get('https://api.harvestapp.com/v2/time_entries', [ 'headers' => [ 'Authorization' => 'Bearer ' . $token, 'Harvest-Account-Id' => 'YOUR_ACCOUNT_ID', ] ]);
$response = $client->patch('https://api.harvestapp.com/v2/time_entries/123456', [ 'headers' => [ 'Authorization' => 'Bearer ' . $token, 'Harvest-Account-Id' => 'YOUR_ACCOUNT_ID', ], 'json' => [ 'hours' => 3.0, ] ]);
$response = $client->delete('https://api.harvestapp.com/v2/time_entries/123456', [ 'headers' => [ 'Authorization' => 'Bearer ' . $token, 'Harvest-Account-Id' => 'YOUR_ACCOUNT_ID', ] ]);
Always check for errors and respect those rate limits:
try { $response = $client->get('https://api.harvestapp.com/v2/time_entries', [ 'headers' => [ 'Authorization' => 'Bearer ' . $token, 'Harvest-Account-Id' => 'YOUR_ACCOUNT_ID', ] ]); } catch (GuzzleHttp\Exception\ClientException $e) { $errorResponse = $e->getResponse(); $errorBody = json_decode($errorResponse->getBody()); echo "Error: " . $errorBody->message; } // Check rate limits $rateLimit = $response->getHeader('X-Rate-Limit-Remaining')[0]; if ($rateLimit < 10) { // Maybe slow down or pause requests }
Harvest uses cursor-based pagination. Here's how to handle it:
$nextPage = null; do { $response = $client->get('https://api.harvestapp.com/v2/time_entries' . ($nextPage ? "?page=$nextPage" : ''), [ 'headers' => [ 'Authorization' => 'Bearer ' . $token, 'Harvest-Account-Id' => 'YOUR_ACCOUNT_ID', ] ]); $data = json_decode($response->getBody()); foreach ($data->time_entries as $entry) { // Process each entry } $nextPage = $data->next_page; } while ($nextPage);
Harvest's API supports various filters and sorting options. Here's an example:
$response = $client->get('https://api.harvestapp.com/v2/time_entries', [ 'headers' => [ 'Authorization' => 'Bearer ' . $token, 'Harvest-Account-Id' => 'YOUR_ACCOUNT_ID', ], 'query' => [ 'from' => '2023-05-01', 'to' => '2023-05-31', 'project_id' => 12345, 'sort' => 'spent_date:desc', ] ]);
Don't forget to test your integration! Here's a simple PHPUnit test example:
use PHPUnit\Framework\TestCase; class HarvestIntegrationTest extends TestCase { public function testCreateTimeEntry() { // Your test code here } }
And there you have it! You've just built a solid Harvest API integration in PHP. Remember, this is just scratching the surface. Harvest's API has tons more features to explore, so don't be afraid to dive deeper.
Keep coding, stay curious, and may your time tracking always be accurate! 🚀