-
-
Notifications
You must be signed in to change notification settings - Fork 0
Time Tracking
Manage time tracking entries in Teamleader Focus.
Time tracking entries record work done against a subject (company, contact, event, milestone, task, ticket). Entries can be created in three different time-recording variants depending on what information is available.
Access via Teamleader::timeTracking().
create()posts totimeTracking.addβ not.create.Three recording variants β
started_at + duration,started_at + ended_at, orstarted_on + duration. The SDK validates the combination before the request.Unknown filter keys throw since v2.2.0.
timeTracking.listignores filter keys it does not recognise and returns the complete unfiltered set with HTTP 200. A query scoped to two days returned all 32,985 entries in the account β silently.updated_since,invoicedandinvoiceableare the confirmed cases; none of them is a filter on this endpoint.The only sort field is
starts_on. Earlier versions of this page saidstarted_at, which is a filter prefix, not a sort field. The API ignores unrecognised sort fields, so sorting bystarted_atsilently did nothing; it now throws.Subject types differ between writing and filtering.
nextgenTaskcan carry tracked time but is not accepted as afilter.subject.type.
timeTracking
| Capability | Supported |
|---|---|
| Pagination | β Supported |
| Filtering | β Supported |
| Sorting | β
Supported (starts_on only) |
| Sideloading | β
Supported (materials, relates_to) |
| Creation | β Supported |
| Update | β Supported |
| Deletion | β Supported |
use McoreServices\TeamleaderSDK\Facades\Teamleader;
$entries = Teamleader::timeTracking()->list();
$entries = Teamleader::timeTracking()->list(
['user_id' => 'user-uuid', 'started_after' => '2025-04-01T00:00:00+02:00'],
['sort' => 'starts_on', 'sort_order' => 'desc', 'page_size' => 50]
);
// With sideloading
$entries = Teamleader::timeTracking()->withMaterials()->withRelations()->list();Filter values are validated as well as filter keys β subject.type, each entry in subject_types, and relates_to.type are all checked before the request is sent.
A null filter value is treated as "not set" rather than as an error, so ['user_id' => null] is skipped rather than throwing.
$entry = Teamleader::timeTracking()->info('entry-uuid');
$entry = Teamleader::timeTracking()->info('entry-uuid', 'materials');
$entry = Teamleader::timeTracking()->withMaterials()->info('entry-uuid');Posts to timeTracking.add. Three recording variants β pass exactly one combination:
Variant 1: started_at + duration (seconds)
Teamleader::timeTracking()->create([
'started_at' => '2025-05-12T09:00:00+02:00',
'duration' => 3600, // seconds
'subject' => ['type' => 'company', 'id' => 'company-uuid'],
'work_type_id' => 'work-type-uuid',
'description' => 'Client meeting',
'invoiceable' => true,
]);Variant 2: started_at + ended_at
Teamleader::timeTracking()->create([
'started_at' => '2025-05-12T09:00:00+02:00',
'ended_at' => '2025-05-12T11:00:00+02:00',
'subject' => ['type' => 'ticket', 'id' => 'ticket-uuid'],
'work_type_id' => 'work-type-uuid',
]);Variant 3: started_on + duration (duration tracking mode)
Teamleader::timeTracking()->create([
'started_on' => '2025-05-12', // date only
'duration' => 7200,
'subject' => ['type' => 'nextgenTask', 'id' => 'task-uuid'],
'work_type_id' => 'work-type-uuid',
]);
invoiceableis a field you can set on an entry. It is not a filter β see the note in the Overview.
Injects id into the request body.
Teamleader::timeTracking()->update('entry-uuid', ['description' => 'Updated', 'invoiceable' => false]);Teamleader::timeTracking()->delete('entry-uuid');Resumes a timer from a previously stopped entry. $startedAt defaults to now.
Teamleader::timeTracking()->resume('entry-uuid');
Teamleader::timeTracking()->resume('entry-uuid', '2025-05-12T14:00:00+02:00');| Method | Description |
|---|---|
forUser(string $userId) |
Filter by user_id
|
forSubject(string $id, string $type) |
Filter by subject β type validated against the filter list |
forSubjectTypes(array $types) |
Filter by subject_types β each entry validated |
betweenDates(string $start, string $end) |
started_after + started_before
|
endedBetween(string $start, string $end) |
ended_after + ended_before
|
relatedTo(string $id, string $type) |
relates_to β type validated separately |
withMaterials() |
Fluent: sideload materials
|
withRelations() |
Fluent: sideload relates_to
|
All filter helpers accept an optional $options array as their last parameter.
Teamleader::timeTracking()->forUser('user-uuid');
Teamleader::timeTracking()->forSubject('ticket-uuid', 'ticket');
Teamleader::timeTracking()->betweenDates('2025-05-01T00:00:00+02:00', '2025-05-31T23:59:59+02:00');
Teamleader::timeTracking()->relatedTo('project-uuid', 'nextgenProject');The fluent include methods relied on a
with()method that did not exist before v2.2.0 βwithMaterials()andwithRelations()raisedError: Call to undefined methodin every earlier release.
The set accepted when writing an entry is wider than the set accepted as a list filter. Verified against @teamleader/focus-api-specification.
| Type |
create() / update()
|
filter.subject / subject_types
|
|---|---|---|
company |
β | β |
contact |
β | β |
event |
β | β |
milestone |
β | β |
ticket |
β | β |
todo |
β | β |
nextgenTask |
β | β |
To find time tracked against project work, filter on relates_to instead:
// Not possible β nextgenTask is not a filter value
Teamleader::timeTracking()->forSubject('task-uuid', 'nextgenTask');
// InvalidArgumentException: ... Time can be tracked against a nextgenTask, but
// the API does not accept it as a filter value. Use relates_to instead.
// Do this instead
Teamleader::timeTracking()->relatedTo('project-uuid', 'nextgenProject');The spec's
updateschema omitsnextgenTaskwhileaddincludes it. That asymmetry looks more like a spec inconsistency than a real rule, so the SDK accepts it on both writes and lets the API be the backstop.
milestone, project, nextgenProject, nextgenProjectGroup
Note that project here is the legacy project system and nextgenProject the current one β the same split described on Projects and Legacy Projects.
Verified against the specification. This is the complete set β anything else throws.
| Filter | Type | Description |
|---|---|---|
ids |
array | Filter by entry UUIDs. A single string is wrapped for you |
user_id |
string | Filter by user UUID |
started_after |
string | ISO 8601 datetime β includes entries that started on the given date |
started_before |
string | ISO 8601 datetime |
ended_after |
string | ISO 8601 datetime |
ended_before |
string | ISO 8601 datetime |
subject |
object |
{type, id} β both required, type validated |
subject_types |
array | Array of subject type strings, each validated |
relates_to |
object |
{type, id} β both required, type validated |
Not filters, despite appearances:
| Key | Why people try it | What to do instead |
|---|---|---|
updated_since |
Common on other resources | Use started_after / ended_after
|
invoiced |
It is a field on the entry | Filter client-side after fetching |
invoiceable |
Settable on create/update | Filter client-side after fetching |
| Field | Description |
|---|---|
starts_on |
The date the entry started |
This is the only sort field the API declares. Anything else throws.
$entries = Teamleader::timeTracking()->list([], [
'sort' => 'starts_on',
'sort_order' => 'desc',
]);| Include | Description |
|---|---|
materials |
Materials linked to the entry |
relates_to |
Project, milestone, or group the entry relates to |
Sent as includes (plural) in the request body.
Because there is no total count in the response, page until a page comes back shorter than the requested size.
$all = [];
$page = 1;
do {
$response = Teamleader::timeTracking()->list(
[
'started_after' => '2026-08-01T00:00:00+02:00',
'started_before' => '2026-08-31T23:59:59+02:00',
],
['page_size' => 100, 'page_number' => $page]
);
$all = array_merge($all, $response['data']);
$page++;
} while (count($response['data']) === 100);Scoping by date is important. Without a filter this endpoint returns every entry in the account, which on an established account is tens of thousands of records.
invoiced is not a filter, so fetch by relation and filter in PHP:
$entries = Teamleader::timeTracking()->relatedTo('project-uuid', 'nextgenProject', [
'page_size' => 100,
]);
$unbilled = array_filter(
$entries['data'],
fn ($entry) => ($entry['invoiceable'] ?? false) && ! ($entry['invoiced'] ?? false)
);Teamleader::timeTracking()->create([
'started_at' => now()->toIso8601String(),
'duration' => 3600,
'subject' => ['type' => 'ticket', 'id' => 'ticket-uuid'],
'work_type_id' => 'work-type-uuid',
'description' => 'Investigated the reported sync failure',
'invoiceable' => true,
]);use InvalidArgumentException;
// Unsupported filter key β the case that returned 32,985 rows
try {
Teamleader::timeTracking()->list(['updated_since' => '2026-08-01T00:00:00+02:00']);
} catch (InvalidArgumentException $e) {
// "Invalid filter key 'updated_since' for timeTracking.list. Supported filters:
// ids, user_id, started_after, started_before, ended_after, ended_before,
// subject, subject_types, relates_to."
}
// invoiced is a field, not a filter
try {
Teamleader::timeTracking()->list(['invoiced' => false]);
} catch (InvalidArgumentException $e) {
// "Invalid filter key 'invoiced' for timeTracking.list. ..."
}
// Unsupported sort field
try {
Teamleader::timeTracking()->list([], ['sort' => 'started_at']);
} catch (InvalidArgumentException $e) {
// 'Invalid sort field: started_at. timeTracking.list accepts: starts_on.'
}
// Subject type valid for writing but not for filtering
try {
Teamleader::timeTracking()->forSubject('task-uuid', 'nextgenTask');
} catch (InvalidArgumentException $e) {
// '... Use relates_to instead.'
}
// Invalid subject type
try {
Teamleader::timeTracking()->forSubject('uuid', 'deal');
} catch (InvalidArgumentException $e) {
// 'Invalid subject type for the timeTracking.list filter: deal. Must be one of:
// company, contact, event, milestone, ticket, todo.'
}
// Invalid relates_to type
try {
Teamleader::timeTracking()->relatedTo('uuid', 'deal');
} catch (InvalidArgumentException $e) {
// 'Invalid relates_to type: deal. Must be one of: milestone, project,
// nextgenProject, nextgenProjectGroup.'
}- Timers β Start/stop a live running timer that creates entries on stop
-
Work Types β
work_type_idon entries - Tasks β Time tracked against standalone tasks
- Project Tasks β Time tracked against project tasks
- Tickets β Time tracked against support tickets
-
Projects β Filter project time via
relates_towithnextgenProject -
Legacy Projects β Filter legacy project time via
relates_towithproject
Last Updated: August 2026 β’ SDK Version: 2.2.2 β’ Made with β€οΈ by MCore Services
- Departments
- Users
- Teams
- Custom Fields
- Work Types
- Document Templates
- Currencies
- Notes
- Email Tracking
- Closing Days
- Day Off Types
- Days Off
- User Schedules
- Invoices
- Credit Notes
- Subscriptions
- Payment Methods
- Payment Terms
- Tax Rates
- Withholding Tax Rates
- Commercial Discounts
Next Gen Projects
Legacy Projects