Update Psa Project
PATCH /psa/projects/{id}
Group: PSA › Projects · Operation ID: updatePsaProject
Updates the PSA project with the given identifier. Only the fields in the request body change.
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. Setting salesArrangementId, or leaving it set, forces the direct customerId link to null. The response is empty. Read the record with GET psa/projects/:id.
Open in the interactive reference
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
Api-Version |
header | "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 for the project to update. |
Request body
Required, application/json: updatePsaProjectInput.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string or null | no | The display name of the project. |
startDate |
string (date) or null | no | The project start date. Format YYYY-MM-DD. |
endDate |
string (date) or null | no | The project end date. Format YYYY-MM-DD. |
teamStartDate |
string (date) or null | no | The team assignment start date. Format YYYY-MM-DD. |
teamEndDate |
string (date) or null | no | The team assignment end date. Format YYYY-MM-DD. |
teamLeadIds |
array of number or null | no | The numeric identifiers of the employees to set as project and team leads. This replaces the current set of leads. There is no endpoint for them. |
active |
boolean or null | no | Whether the underlying timetracking project is active. |
notifyProjectLeads |
boolean or null | no | Whether to notify the project leads. |
category |
string, one of "Billable", "Internal", "NonBillable" |
no | The project category. Accepted values: 'Billable', 'Internal', 'NonBillable'. |
status |
string, one of "Tentative", "Planning", "PlanningDraft", "NotStarted", "InProgress", "Paused", "Completed", "Cancelled" |
no | The project status. Accepted values: 'Tentative', 'Planning', 'PlanningDraft', 'NotStarted', 'InProgress', 'Paused', 'Completed', 'Cancelled'. |
currency |
string or null | no | The project's default currency. Format ISO 4217, for example 'USD'. |
budgetAmount |
string (decimal) or null | no | The project budget amount, in budgetCurrency. |
budgetCurrency |
string or null | no | The currency of budgetAmount. Format ISO 4217, for example 'USD'. It should match the project's default currency. |
departmentId |
number or null | no | The numeric identifier of the department to allocate the project to. GET departments/:id takes a UUID, so the number cannot be used to fetch the department. |
customerId |
number or 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 or null | no | Whether time entries must fall within the project's startDate and endDate. |
invoiced |
boolean or null | no | Whether the project is marked as invoiced. |
notes |
string or null | no | Free-form notes about the project. |
opportunityId |
number or 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 or null | no | A code for the project. |
quoteId |
number or 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 or 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 or null | no | The numeric identifier of the sales representative, an employee. There is no endpoint for it. |
projectTypeId |
number or 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 or null | no | A link to an external system for this project. |
projectSettings |
object or null | no | The timetracking settings for the project. Updated when projectSettingsId is given, otherwise a new settings record is created. |
projectSettings.projectSettingsId |
number or null | no | |
projectSettings.disableTimetrackingOnHolidays |
boolean or null | no | |
projectSettings.disableTimetrackingOnWeekends |
boolean or null | no | |
projectSettings.maxHoursPerDay |
string (decimal) or null | no | |
projectSettings.maxHoursPerMonth |
string (decimal) or null | no | |
projectSettings.maxHoursPerWeek |
string (decimal) or null | no | |
projectSettings.billableHoursPerDay |
string (decimal) or null | no | |
projectSettings.notifyNewMembers |
boolean or null | no | |
projectSettings.requireNotes |
boolean or null | no |
Example:
{
"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,
"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
Response headers
Idempotency-Replayed— Present andtrueonly when this response replays a previously stored one for the same Idempotency-Key, rather than reflecting a live execution. Absent on a live response - never sent asfalse.
Errors
| Code | Status |
|---|---|
NOT_FOUND |
400 |
INVOICED_SALES_ARRANGEMENT_CHANGE_BLOCKED |
400 |
IDEMPOTENCY_KEY_CONFLICT |
409 |
IDEMPOTENCY_PAYLOAD_MISMATCH |
422 |
Statuses
400— Invalid request or business failure. Codes: NOT_FOUND, INVOICED_SALES_ARRANGEMENT_CHANGE_BLOCKED401— Missing or invalid access token403— Not authorized404— Not found409— The Idempotency-Key is still being processed by another request.422— The Idempotency-Key was already used with a different request payload.429— Too many requests500— Internal server error