Update a project

Operation ID: updatePsaProject

Group: PSA › Projects

Open in the interactive reference

PATCH /psa/projects/{id}

Retry-safe: A write that accepts Idempotency-Key: repeating a request with the same key replays the stored answer instead of executing twice.

Updates a project with the fields you send. A field you leave out keeps its current value. To clear an optional field, send null. Null is ignored for teamStartDate, teamLeadIds, and projectSettings. A null teamEndDate removes the lock date, so time and expenses stay open indefinitely.

Use the project's id, not its code or the numeric psaProjectId that other PSA endpoints return.

A teamLeadIds list you send replaces the current team leads, and each employee in it is given the PSA manager permission role. The list can't be empty. While the project has a sales arrangement, customerId is set to null on every update, even when you don't send it.

Changing or removing salesArrangementId fails with INVOICED_SALES_ARRANGEMENT_CHANGE_BLOCKED when a linked position, milestone, or fixed fee has already been invoiced. An invoiced expense budget does not block the change. A successful change also removes the links between the project's positions, milestones, and fixed fees and their sales order product items, and between its expense budgets and their sales orders.

The response is empty. Read the project with GET psa/projects/:id.

Parameters

Name In Type Required Description
Api-Version header string · v1 yes The API version this request targets, named by its public identifier. Required on every request; omitting it, or naming a version this company does not have, is answered with 400. This document describes version v1.
Idempotency-Key header string no An opaque caller-chosen string (1-255 characters) that makes this request safe to retry. Repeating the same key with an identical request body replays the original response instead of re-executing - but only once that original request succeeded; a failed attempt re-executes on retry. Repeating the key with a different body is answered with 422. A key still in flight is answered with 409 - as is a key whose original request was interrupted before it finished, for a few minutes afterwards, until that attempt is presumed dead. A completed response stays replayable for 24 hours. The key is scoped to the caller, not to the action: reusing it for a different action is answered with 409 or 422 rather than treated independently. On read actions this header is ignored. On write actions that cannot offer the guarantee it is answered with 400 IDEMPOTENCY_NOT_SUPPORTED rather than silently ignored.
id path string yes The unique identifier of the project you want to update. Example: "3b7d5e9c-1a24-4f80-9c6d-2e8f70b13a45".

Request body

Required, application/json: updatePsaProjectInput.

Field Type Required Description
name string | null no The display name of the project.
startDate date | null no The project start date. Format YYYY-MM-DD.
endDate date | null no The project end date. Format YYYY-MM-DD.
teamStartDate date | null no The team assignment start date. Format YYYY-MM-DD.
teamEndDate date | null no The team assignment end date. Format YYYY-MM-DD. Send null to remove it, so time and expenses stay open indefinitely.
teamLeadIds array<number> | null no The numeric identifiers of the employees to set as project and team leads. This replaces the current set of leads. The list can't be empty. There is no endpoint for them.
active boolean | null no Whether the underlying timetracking project is active.
notifyProjectLeads boolean | null no Whether to notify the project leads.
category any no The project category. Accepted values: 'Billable', 'Internal', 'NonBillable'. One of: "Billable", "Internal", "NonBillable".
status any no The project status. Accepted values: 'Tentative', 'Planning', 'PlanningDraft', 'NotStarted', 'InProgress', 'Paused', 'Completed', 'Cancelled'. One of: "Tentative", "Planning", "PlanningDraft", "NotStarted", "InProgress", "Paused", "Completed", "Cancelled".
currency string | null no The project's default currency. Format ISO 4217, for example 'USD'.
budgetAmount decimal | null no The project budget amount, in budgetCurrency.
budgetCurrency string | null no The currency of budgetAmount. Format ISO 4217, for example 'USD'. It should match the project's default currency.
departmentId number | null no The numeric identifier of the department to allocate the project to. GET people/departments/:id takes a UUID, so the number cannot be used to fetch the department.
entityId number | null no The numeric identifier of the legal entity the project belongs to. While the project has a sales arrangement, it must be the arrangement's entity, and null resets it to that entity; a different value fails with the message 'The project's entity must match the entity of its sales arrangement'. Without a sales arrangement, null clears it. When salesArrangementId changes and entityId is not sent, the entity follows the new sales arrangement.
customerId number | null no The numeric identifier of a customer to link directly to the project. It is forced to null whenever a sales arrangement is set. GET accounting/customers/:id takes a UUID, so the number cannot be used to fetch the customer.
enforceActivityDateRange boolean | null no Whether time entries must fall within the project's startDate and endDate.
invoiced boolean | null no Whether the project is marked as invoiced.
notes string | null no Free-form notes about the project. Example: "Kickoff moved to next sprint.".
opportunityId number | null no The numeric identifier of a CRM opportunity linked to the project. GET crm/opportunities/:id takes a UUID, so the number cannot be used to fetch the opportunity.
code string | null no A code for the project.
quoteId number | null no The numeric identifier of a CRM quote linked to the project. GET crm/quotes/:id takes a UUID, so the number cannot be used to fetch the quote.
salesArrangementId number | null no The numeric identifier of a sales arrangement that links the project to billing terms. There is no endpoint for it. Changing or removing it fails with INVOICED_SALES_ARRANGEMENT_CHANGE_BLOCKED when a linked position, milestone, or fixed fee has already been invoiced.
salesRepresentativeId number | null no The numeric identifier of the sales representative, an employee. There is no endpoint for it.
projectTypeId number | null no The numeric identifier of the project type. GET psa/project-types/:id takes a UUID, so the number cannot be used to fetch the project type.
externalLink string | null no A link to an external system for this project.
projectSettings object | null no The timetracking settings for the project. Updated when projectSettingsId is given, otherwise a new settings record is created.
projectSettings.projectSettingsId number | null no
projectSettings.disableTimetrackingOnHolidays boolean | null no
projectSettings.disableTimetrackingOnWeekends boolean | null no
projectSettings.maxHoursPerDay decimal | null no
projectSettings.maxHoursPerMonth decimal | null no
projectSettings.maxHoursPerWeek decimal | null no
projectSettings.billableHoursPerDay decimal | null no
projectSettings.notifyNewMembers boolean | null no
projectSettings.requireNotes boolean | null no
{
  "name": "<name>",
  "startDate": "2026-01-15",
  "endDate": "2026-01-15",
  "teamStartDate": "2026-01-15",
  "teamEndDate": "2026-01-15",
  "teamLeadIds": [
    0
  ],
  "active": true,
  "notifyProjectLeads": true,
  "category": "Billable",
  "status": "Tentative",
  "currency": "<currency>",
  "budgetAmount": "19.99",
  "budgetCurrency": "<budgetCurrency>",
  "departmentId": 0,
  "entityId": 0,
  "customerId": 0,
  "enforceActivityDateRange": true,
  "invoiced": true,
  "notes": "Kickoff moved to next sprint.",
  "opportunityId": 0,
  "code": "<code>",
  "quoteId": 0,
  "salesArrangementId": 0,
  "salesRepresentativeId": 0,
  "projectTypeId": 0,
  "externalLink": "<externalLink>",
  "projectSettings": {
    "projectSettingsId": 0,
    "disableTimetrackingOnHolidays": true,
    "disableTimetrackingOnWeekends": true,
    "maxHoursPerDay": "19.99",
    "maxHoursPerMonth": "19.99",
    "maxHoursPerWeek": "19.99",
    "billableHoursPerDay": "19.99",
    "notifyNewMembers": true,
    "requireNotes": true
  }
}

Response

200 Success

{
  "data": {},
  "diagnostics": [
    {
      "code": "<code>",
      "message": "<message>",
      "severity": "error",
      "path": [
        "<path>"
      ],
      "details": "<details>"
    }
  ]
}

Response headers

Errors

Code Status
NOT_FOUND 400
INVOICED_SALES_ARRANGEMENT_CHANGE_BLOCKED 400
IDEMPOTENCY_KEY_CONFLICT 409
IDEMPOTENCY_PAYLOAD_MISMATCH 422

Statuses