-
-
Notifications
You must be signed in to change notification settings - Fork 0
Legacy Projects
Manage legacy projects in Teamleader Focus β the original project system.
Legacy Projects is the original Teamleader project system. Unlike the current system, work is organised into milestones (phases) rather than groups, and each project must have at least one milestone and one participant at creation.
Access via Teamleader::legacyProjects().
Only available on accounts that have not yet migrated. Check with
Teamleader::accounts()->isUsingLegacyProjects().
forCustomer()signature is(id, type)β id first, type second. This is the opposite ofProjects::forCustomer().Unknown filter keys and sort fields throw since v2.2.1. Both were previously passed through or dropped without comment.
No sideloading.
| SDK method | Teamleader::legacyProjects() |
| API path |
projects.* β the bare path belongs to the OLD system |
| Webhook events |
project.created, project.updated, project.deleted
|
The current system is Projects, on projects-v2/projects.* with nextgenProject.* events. So the class names and the endpoint paths run in opposite directions: this is the older class on the shorter path, while Projects is the newer class on the longer one. Teamleader chose the projects-v2 prefix for the new module specifically to avoid colliding with these existing endpoints.
Do not infer which system an account is on from whether a list call returns rows. Both endpoints answer. Ask directly:
Teamleader::accounts()->getProjectsVersion(); // 'projects-v2' or 'legacy'
Teamleader::accounts()->isUsingLegacyProjects(); // boolAccounts are migrated over time; Accounts::getAutoSwitchDate() reports when, if a switch is scheduled. See Accounts.
Note there is no
project.closedwebhook event β the legacy system fires onlycreated,updatedanddeleted. The current system addsnextgenProject.closed.
projects
| Capability | Supported |
|---|---|
| Pagination | β Supported |
| Filtering | β Supported |
| Sorting | β
Supported (due_on, title, created_at) |
| Sideloading | β Not supported |
| Creation | β Supported |
| Update | β Supported |
| Deletion | β Supported |
use McoreServices\TeamleaderSDK\Facades\Teamleader;
$projects = Teamleader::legacyProjects()->list();
$projects = Teamleader::legacyProjects()->list(
['status' => 'active'],
['sort' => [['field' => 'due_on', 'order' => 'asc']], 'page_size' => 50]
);The customer filter can be passed either as a nested object or as flattened keys:
// Both work
$projects = Teamleader::legacyProjects()->list([
'customer' => ['type' => 'company', 'id' => 'company-uuid'],
]);
$projects = Teamleader::legacyProjects()->list([
'customer.type' => 'company',
'customer.id' => 'company-uuid',
]);Anything else throws before the request is sent:
Teamleader::legacyProjects()->list(['project_id' => 'project-uuid']);
// InvalidArgumentException: Unsupported filter key for projects.list:
// project_id. Supported: customer, customer.type, customer.id, status,
// participant_id, term, updated_since.$project = Teamleader::legacyProjects()->info('project-uuid');Takes no includes β this system has no sideloading.
Four fields are required and validated:
| Required | Notes |
|---|---|
title |
Project title |
starts_on |
Start date (YYYY-MM-DD) |
milestones |
Array β at least one milestone required |
participants |
Array β at least one participant required |
$project = Teamleader::legacyProjects()->create([
'title' => 'Office Renovation',
'starts_on' => '2025-05-01',
'milestones' => [
[
'name' => 'Phase 1: Planning',
'due_on' => '2025-05-31',
'responsible_user_id' => 'user-uuid',
'billing_method' => 'time_and_materials',
],
],
'participants' => [
['type' => 'user', 'id' => 'user-uuid'],
],
'customer' => ['type' => 'company', 'id' => 'company-uuid'],
'description' => 'Full office renovation project',
]);Teamleader requires at least one participant with the
decision_makerrole. The SDK checks thatparticipantsis a non-empty array but does not check roles, so a payload without a decision maker will be rejected server-side rather than client-side.
Injects id into the request body.
Teamleader::legacyProjects()->update('project-uuid', ['title' => 'Office Renovation β Updated']);Teamleader::legacyProjects()->delete('project-uuid');Closes the project and all its phases and tasks.
Teamleader::legacyProjects()->close('project-uuid');Unlike Projects::close(), there is no closing strategy to choose β the cascade is unconditional.
Teamleader::legacyProjects()->reopen('project-uuid');// Add β role defaults to 'member'
Teamleader::legacyProjects()->addParticipant(
'project-uuid',
['type' => 'user', 'id' => 'user-uuid'],
'decision_maker'
);
// Change an existing participant's role
Teamleader::legacyProjects()->updateParticipant(
'project-uuid',
['type' => 'user', 'id' => 'user-uuid'],
'member'
);Roles: decision_maker, member.
β οΈ Id first, type second β opposite ofProjects::forCustomer(), which is(type, id). Same method name, reversed arguments, so this is easy to get wrong when supporting both systems.
$projects = Teamleader::legacyProjects()->forCustomer('company-uuid'); // type defaults to 'company'
$projects = Teamleader::legacyProjects()->forCustomer('contact-uuid', 'contact');Teamleader::legacyProjects()->active(); // status: active
Teamleader::legacyProjects()->byStatus('on_hold'); // active | on_hold | done | cancelled
Teamleader::legacyProjects()->forParticipant('user-uuid');
Teamleader::legacyProjects()->search('renovation');
Teamleader::legacyProjects()->updatedSince('2025-01-01T00:00:00+02:00');All accept an optional $options array for pagination and sorting.
Verified against @teamleader/focus-api-specification. This is the complete set.
| Filter | Type | Description |
|---|---|---|
customer |
object |
{type: contact|company, id: uuid} β singular object |
status |
string |
active, on_hold, done, cancelled
|
participant_id |
string | Filter by participant UUID |
term |
string | Search title or description |
updated_since |
string | ISO 8601 datetime |
customer may be passed as a nested object or as the flattened customer.type / customer.id pair, so all three keys are accepted.
Unknown filter keys throw since v2.2.1. They were previously dropped silently β the API ignores keys it does not recognise and answers 200 with every project, so a mistyped key returned a plausible but unfiltered result.
| Field | Description |
|---|---|
due_on |
Project due date |
title |
Project title |
created_at |
Creation date |
Pass either the sort option as an array of objects, or sort_field + sort_order:
$projects = Teamleader::legacyProjects()->list([], [
'sort' => [['field' => 'due_on', 'order' => 'asc']],
]);
$projects = Teamleader::legacyProjects()->list([], [
'sort_field' => 'title',
'sort_order' => 'asc',
]);A bare field name and a list of names are also accepted:
$projects = Teamleader::legacyProjects()->list([], ['sort' => 'due_on']);
$projects = Teamleader::legacyProjects()->list([], ['sort' => ['due_on', 'title']]);Since v2.2.1 the field is validated against the three above. Anything else throws β previously it was passed straight to the API, which ignores unrecognised sort fields and returns records in its own order.
If your integration supports both, these are the things that differ beyond the endpoint:
| Legacy Projects | Projects (nextgen) | |
|---|---|---|
| Status values |
active, on_hold, done, cancelled
|
open, planned, running, overdue, over_budget, closed
|
| Customer filter |
customer β singular object |
customers β plural array |
forCustomer() |
(id, type) |
(type, id) |
| Webhooks |
project.* (no closed) |
nextgenProject.* |
| Organised into | milestones, participants | groups, tasks, materials |
| Sideloading | none |
legacy_project, custom_fields
|
close() |
unconditional cascade | takes a closing strategy |
| Required on create | title, starts_on, milestones, participants | title only |
Branch once at the top rather than guessing:
if (Teamleader::accounts()->isUsingLegacyProjects()) {
$projects = Teamleader::legacyProjects()->active();
} else {
$projects = Teamleader::projects()->list(['status' => 'open']);
}There is no meta block β the API returns pagination metadata only when a resource sends includes=pagination, and this one does not. There is no total count, so the end of a list is a page shorter than the requested page size.
use InvalidArgumentException;
// Missing required field
try {
Teamleader::legacyProjects()->create(['title' => 'Test']); // missing starts_on, milestones, participants
} catch (InvalidArgumentException $e) {
// "Field 'starts_on' is required for creating a project"
}
// Empty milestones array
try {
Teamleader::legacyProjects()->create([
'title' => 'Test', 'starts_on' => '2025-01-01',
'milestones' => [], 'participants' => [['type' => 'user', 'id' => 'uuid']],
]);
} catch (InvalidArgumentException $e) {
// 'At least one milestone is required'
}
// Empty participants array
try {
Teamleader::legacyProjects()->create([
'title' => 'Test', 'starts_on' => '2025-01-01',
'milestones' => [['name' => 'Phase 1']], 'participants' => [],
]);
} catch (InvalidArgumentException $e) {
// 'At least one participant is required'
}
// Unsupported filter key
try {
Teamleader::legacyProjects()->list(['project_id' => 'project-uuid']);
} catch (InvalidArgumentException $e) {
// 'Unsupported filter key for projects.list: project_id. Supported:
// customer, customer.type, customer.id, status, participant_id, term,
// updated_since.'
}
// Unsupported sort field
try {
Teamleader::legacyProjects()->list([], ['sort' => 'starts_on']);
} catch (InvalidArgumentException $e) {
// 'Invalid sort field: starts_on. projects.list accepts: due_on, title,
// created_at.'
}
// Wrong project system β the call succeeds and returns rows either way.
// There is no exception for this. Check the account first.-
Accounts β
getProjectsVersion()tells you which system an account uses -
Projects β The current system, on
projects-v2/projects.* - Legacy Milestones β Phases within a legacy project
-
Time Tracking β Filter legacy project time via
relates_towithproject -
Webhooks β This resource fires
project.*events -
Files β Use
files()->forLegacyProject(), which maps to subject typeproject
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