Skip to content

Projects

MC0RE edited this page Aug 18, 2026 · 2 revisions

Projects

Manage projects in Teamleader Focus β€” the current ("nextgen") project system.

Overview

The Projects resource is the central hub of the v2 project management system. Projects contain groups, tasks, and materials, and can be linked to deals, quotations, customers, and owners.

Access via Teamleader::projects(), or Teamleader::nextgenProjects() β€” see below.

Read Which system, and what it's called first if you are new to this resource. Teamleader uses three different names for this one thing and they do not line up. Getting it backwards is easy and expensive.

Which system, and what it's called

SDK method Teamleader::projects() β€” alias Teamleader::nextgenProjects()
API path projects-v2/projects.*
Webhook events nextgenProject.created, nextgenProject.updated, nextgenProject.closed, nextgenProject.deleted

Reading the webhook names, it is natural to assume projects() must be the legacy resource and that some nextgenProjects() covers the new one. It is the other way round. This class is what Teamleader calls "nextgen"; nextgenProjects() was added in v2.2.0 as an alias precisely because reasoning from the event names leads people here looking for a method that didn't exist.

The old system is Legacy Projects, on the bare projects.* path with project.* events. So the class names and the endpoint paths run in opposite directions: Projects is the newer class on the longer path, LegacyProjects the older class on the shorter one. Teamleader chose the projects-v2 prefix for the new module to avoid colliding with the existing projects.* endpoints.

Do not infer which system an account is on from whether a list call returns rows. Both endpoints answer. Querying this resource and getting twenty real projects back does not mean the account is on nextgen. Ask directly:

Teamleader::accounts()->getProjectsVersion();   // 'projects-v2' or 'legacy'
Teamleader::accounts()->isUsingProjectsV2();    // bool

See Accounts.

Endpoint

projects-v2/projects

Capabilities

Capability Supported
Pagination βœ… Supported
Filtering βœ… Supported
Sorting βœ… Supported (many fields)
Sideloading βœ… Supported (legacy_project, custom_fields)
Creation βœ… Supported
Update βœ… Supported
Deletion βœ… Supported

delete() requires a strategy β€” defaults to unlink_tasks_and_time_trackings.

close() requires a strategy β€” defaults to none.

Customer filter key is customers (plural array), not customer (singular object). This differs from Deals and Invoices, which use the singular form.

Sideload via options['includes'] (plural) in list().


Methods

list(array $filters = [], array $options = [])

use McoreServices\TeamleaderSDK\Facades\Teamleader;

$projects = Teamleader::projects()->list();

$projects = Teamleader::projects()->list(
    ['status' => 'open'],
    ['sort' => [['field' => 'title', 'order' => 'asc']], 'page_size' => 50]
);

// With sideloading
$projects = Teamleader::projects()->list([], ['includes' => 'legacy_project,custom_fields']);

info(string $id, mixed $includes = null)

$project = Teamleader::projects()->info('project-uuid');
$project = Teamleader::projects()->info('project-uuid', ['legacy_project', 'custom_fields']);
$project = Teamleader::projects()->with('custom_fields')->info('project-uuid');

The fluent with() form only works from v2.2.0. The method did not exist in any earlier release, so ->with(...) raised Error: Call to undefined method on every resource that offered it.


create(array $data)

Only title is required. billing_method and color are validated when provided.

Required Notes
title Project title

Billing methods: time_and_materials, fixed_price, non_billable

$project = Teamleader::projects()->create([
    'title'          => 'Website Redesign',
    'billing_method' => 'time_and_materials',
    'color'          => '#00B2B2',
    'start_date'     => '2025-05-01',
    'end_date'       => '2025-08-31',
    'customers'      => [['type' => 'company', 'id' => 'company-uuid']],
]);

update(mixed $id, array $data)

Injects id into the request body.

Teamleader::projects()->update('project-uuid', ['title' => 'Website Redesign v2']);

delete(mixed $id, string $deleteStrategy = 'unlink_tasks_and_time_trackings')

Delete strategies (validated):

Strategy Behaviour
unlink_tasks_and_time_trackings Default β€” keeps tasks and time entries, removes project link
delete_tasks_and_time_trackings Deletes tasks and all time entries
delete_tasks_unlink_time_trackings Deletes tasks, keeps time entries unlinked
Teamleader::projects()->delete('project-uuid');
Teamleader::projects()->delete('project-uuid', 'delete_tasks_and_time_trackings');

The default is the conservative one. delete_tasks_and_time_trackings destroys tracked time permanently β€” worth being deliberate about in any automated cleanup.


duplicate(string $id, string $title)

Creates a copy of the project with a new title.

$copy = Teamleader::projects()->duplicate('project-uuid', 'Website Redesign – Phase 2');

close(string $id, string $closingStrategy = 'none')

Closing strategies (validated):

Strategy Behaviour
none Default β€” just closes the project
mark_tasks_and_materials_as_done Also marks all tasks and materials as done
Teamleader::projects()->close('project-uuid');
Teamleader::projects()->close('project-uuid', 'mark_tasks_and_materials_as_done');

Closing fires the nextgenProject.closed webhook β€” note that there is no equivalent project.closed event on the legacy system.


reopen(string $id)

Teamleader::projects()->reopen('project-uuid');

assign(string $id, string $assigneeType, string $assigneeId) / unassign()

Assignee type validated: user, team.

Teamleader::projects()->assign('project-uuid', 'user', 'user-uuid');
Teamleader::projects()->unassign('project-uuid', 'team', 'team-uuid');

Deal, Quotation, and Owner management

Teamleader::projects()->addDeal('project-uuid', 'deal-uuid');
Teamleader::projects()->removeDeal('project-uuid', 'deal-uuid');

Teamleader::projects()->addQuotation('project-uuid', 'quotation-uuid');
Teamleader::projects()->removeQuotation('project-uuid', 'quotation-uuid');

Teamleader::projects()->addOwner('project-uuid', 'user-uuid');
Teamleader::projects()->removeOwner('project-uuid', 'user-uuid');

Helper Methods

Status shortcuts

Teamleader::projects()->open();
Teamleader::projects()->closed();
Teamleader::projects()->running();
Teamleader::projects()->overdue();
Teamleader::projects()->overBudget();

Other helpers

Teamleader::projects()->search('website');
Teamleader::projects()->byIds(['uuid-1', 'uuid-2']);
Teamleader::projects()->forCustomer('company', 'company-uuid'); // type first, then id
Teamleader::projects()->forDeal('deal-uuid');
Teamleader::projects()->forQuotation('quotation-uuid');

forCustomer() takes type first, unlike LegacyProjects::forCustomer(), which takes id first. An easy one to get wrong when supporting both systems.


Filters

Verified against @teamleader/focus-api-specification. This is the complete set.

Filter Type Description
ids array Filter by project UUIDs
status string open, planned, running, overdue, over_budget, closed
customers array [{type: contact|company, id: uuid}] β€” plural, array of objects
deal_ids array Filter by deal UUIDs
quotation_ids array Filter by quotation UUIDs
term string Search project number, title, customer/assignee/owner names

Sorting

Supported sort fields: amount_billed, amount_paid, amount_unbilled, cost, customer, end_date, external_budget_spent, external_budget, internal_budget, margin, price, project_key, start_date, status, time_budget, time_estimated, time_tracked, title

$projects = Teamleader::projects()->list([], [
    'sort' => [['field' => 'end_date', 'order' => 'asc']],
]);

Sideloading

Include Description
legacy_project Linked legacy project reference (if migrated from legacy)
custom_fields Custom field values on the project

legacy_project is the bridge between the two systems: on a migrated account it points at the Legacy Projects record the project came from.

Custom field definitions for projects use context: project β€” see Custom Fields.


Response Notes

This resource returns pagination metadata. It is one of the few that does, so meta.matches gives you a real total count rather than requiring you to page until a short page.


Working across both systems

If your integration has to support accounts on either system, branch once at the top rather than guessing:

if (Teamleader::accounts()->isUsingProjectsV2()) {
    $projects = Teamleader::projects()->list(['status' => 'open']);
} else {
    $projects = Teamleader::legacyProjects()->active();
}

Things that differ between the two beyond the endpoint:

Projects (nextgen) Legacy Projects
Status values open, planned, running, overdue, over_budget, closed active, on_hold, done, cancelled
Customer filter customers β€” plural array customer β€” singular object
forCustomer() (type, id) (id, type)
Webhooks nextgenProject.* project.*
Contains groups, tasks, materials milestones, participants

Error Handling

use InvalidArgumentException;

// Missing title
try {
    Teamleader::projects()->create(['billing_method' => 'time_and_materials']);
} catch (InvalidArgumentException $e) {
    // 'Title is required for creating a project'
}

// Invalid delete strategy
try {
    Teamleader::projects()->delete('uuid', 'archive');
} catch (InvalidArgumentException $e) {
    // 'Invalid delete strategy. Must be one of: ...'
}

// Wrong project system β€” the call succeeds but returns nothing useful
// There is no exception for this. Check the account first.

Related Resources

  • Accounts β€” getProjectsVersion() tells you which system an account uses
  • Legacy Projects β€” The older system, on the bare projects.* path
  • Groups β€” Organise tasks and materials within a project
  • Project Tasks β€” Tasks inside a project
  • Materials β€” Materials inside a project
  • Project Lines β€” Unified view of all project line items
  • External-Parties β€” External stakeholders on a project
  • Custom Fields β€” Definitions use context: project
  • Time Tracking β€” Filter project time via relates_to with nextgenProject
  • Webhooks β€” This resource fires nextgenProject.* events
  • Files β€” Use files()->forProject(), which maps to nextgenProject

Clone this wiki locally