Retrieve a project's expenses and budgets
Operation ID: getExpensesSummary
Group: PSA › Expenses
Open in the interactive reference
GET /psa/expenses/summary
Read-only: Reads and returns; changes nothing.
Retrieves a project's expenses and budgets. The response also lists every budget defined for the project in expenseBudgets, and the default catch-all budget in defaultBudget, when one is set. Send the project's numeric psaProjectId in the query string.
Only the expenses array is paginated. budgetMappings covers only the expenses on the current page, and expenseBudgets is the same on every page. The cursor counts expenses from the start, so a page can skip or repeat an expense if expenses are added or removed between requests.
With only View permission on the project, you see only your own expenses. With Edit permission, you see all of the project's expenses.
This endpoint does not follow the standard collection rules in Pagination and querying.
- Page with
pageSize(default 50, maximum 200, minimum 1) andpageCursor, notpage[size]andpage[cursor]. - The response contains the records in
data.expensesand the next page's cursor indata.pagination.nextCursor. - Apart from
psaProjectId, it takes no filters. - There is no
sortorfields.
This endpoint ignores any query parameter it does not declare, and returns no error. A request that follows the standard rules therefore gets the first page, unfiltered, every time.
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. |
psaProjectId |
query | number | yes | The id of the PSA project. It is the numeric key, not the UUID that GET psa/projects/:id takes. Example: 32341. |
pageCursor |
query | string | no | The cursor from the previous response's data.pagination.nextCursor. Omit on the first request. |
pageSize |
query | number | no | The number of expenses per page, between 1 and 200. Defaults to 50. The maximum is 200. Example: 50. |
Response
200 Success
Under data (getExpensesSummaryOutput):
| Field | Type | Required | Description |
|---|---|---|---|
expenses |
array<object> | yes | The expense entries on this page, each linked to the project through its assigned budget. Example: [{"id":88213,"employeeName":"Priya Natarajan","categoryName":"Airfare","amount":"412.50","amountCurrency":"USD","status":"Approved","billDate":"2026-07-02"}]. |
expenses[].id |
number | no | The unique identifier for the expense entry. |
expenses[].expenseReportId |
number | no | The id of the expense report this expense belongs to. It is a numeric key, not the UUID that GET accounting/expense-reports/:id takes. |
expenses[].employeeName |
string | no | The name of the employee who incurred the expense. |
expenses[].categoryName |
string | no | The name of the expense category. |
expenses[].amount |
decimal | no | The gross amount of the expense, in the currency given by amountCurrency. |
expenses[].amountCurrency |
string | no | The currency of amount. Format ISO 4217, for example 'USD'. |
expenses[].reimbursableNetAmount |
decimal | no | The reimbursable net amount of the expense, after tax and any company-paid portions, in the currency given by reimbursableNetAmountCurrency. |
expenses[].reimbursableNetAmountCurrency |
string | no | The currency of reimbursableNetAmount. Format ISO 4217, for example 'USD'. |
expenses[].status |
string | no | The status of the expense entry. |
expenses[].notes |
string | no | Free-form notes on the expense. |
expenses[].billDate |
date | no | The date the expense was incurred. Format YYYY-MM-DD. |
expenses[].billableExpense |
string | no | The billable classification of the expense. |
expenses[].expenseReportNumber |
string | no | The human-readable number of the expense report. |
budgetMappings |
array<object> | yes | The mapping of each expense on this page to its assigned budget. It covers only the expenses in this page's expenses array. Example: [{"expenseItemBaseId":88213,"expenseBudgetId":501,"name":"Travel","budget":"10000.00","currency":"USD"}]. |
budgetMappings[].expenseItemBaseId |
number | yes | The id of the expense this mapping is for. It matches an id in the expenses array of this response. |
budgetMappings[].expenseBudgetId |
number | yes | The id of the budget this expense is mapped to. It matches an id in the expenseBudgets array of this response. |
budgetMappings[].name |
string | yes | The display name of the budget. |
budgetMappings[].budget |
decimal | no | The budget amount, in the currency given by currency. Absent when the budget has no amount. |
budgetMappings[].currency |
string | no | The currency of budget. Format ISO 4217, for example 'USD'. Absent when the budget has no amount. |
budgetMappings[].isDefault |
boolean | null | no | Whether this is the project's default catch-all budget. |
budgetMappings[].billable |
boolean | null | no | Whether expenses against this budget are billable to the customer. |
budgetMappings[].productId |
number | null | no | The id of the product tied to this budget. It is a numeric key, not the UUID that GET accounting/products/:id takes. Absent when the budget is not tied to a product. |
budgetMappings[].productLineId |
number | null | no | The id of the product line tied to this budget. It is a numeric key, not the UUID that GET accounting/product-lines/:id takes. Absent when the budget is not tied to a product line. |
budgetMappings[].salesOrderId |
number | no | The id of the sales order this budget bills against. It is a numeric key, not the UUID that GET accounting/sales-orders/:id takes. |
expenseBudgets |
array<object> | yes | Every budget defined for the project. This array is never paginated. Example: [{"id":501,"name":"Travel","budget":"10000.00","currency":"USD","isDefault":false,"lastModifiedDate":"2026-07-02T09:14:22.000Z"}]. |
expenseBudgets[].id |
number | yes | The unique identifier for the budget. |
expenseBudgets[].name |
string | yes | The display name of the budget. |
expenseBudgets[].budget |
decimal | no | The budget amount, in the currency given by currency. Absent when the budget has no amount. |
expenseBudgets[].currency |
string | no | The currency of budget. Format ISO 4217, for example 'USD'. Absent when the budget has no amount. |
expenseBudgets[].isDefault |
boolean | yes | Whether this is the project's default catch-all budget. |
expenseBudgets[].lastModifiedDate |
date-time | no | The date and time the budget definition last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. |
expenseBudgets[].billable |
boolean | null | no | Whether expenses against this budget are billable to the customer. |
expenseBudgets[].productId |
number | null | no | The id of the product tied to this budget. It is a numeric key, not the UUID that GET accounting/products/:id takes. Absent when the budget is not tied to a product. |
expenseBudgets[].productLineId |
number | null | no | The id of the product line tied to this budget. It is a numeric key, not the UUID that GET accounting/product-lines/:id takes. Absent when the budget is not tied to a product line. |
expenseBudgets[].salesOrderId |
number | no | The id of the sales order this budget bills against. It is a numeric key, not the UUID that GET accounting/sales-orders/:id takes. |
defaultBudget |
object | no | The project's default catch-all budget for expenses with no explicit mapping. Absent when the project has no default budget. |
defaultBudget.id |
number | yes | The unique identifier for the budget. |
defaultBudget.name |
string | yes | The display name of the budget. |
defaultBudget.budget |
decimal | no | The budget amount, in the currency given by currency. Absent when the budget has no amount. |
defaultBudget.currency |
string | no | The currency of budget. Format ISO 4217, for example 'USD'. Absent when the budget has no amount. |
defaultBudget.isDefault |
boolean | yes | Always true, because this is the default budget. |
defaultBudget.lastModifiedDate |
date-time | no | The date and time the budget definition last changed, in UTC. Format YYYY-MM-DDTHH:mm:ss.sssZ. |
defaultBudget.billable |
boolean | null | no | Whether expenses against this budget are billable to the customer. |
defaultBudget.productId |
number | null | no | The id of the product tied to this budget. It is a numeric key, not the UUID that GET accounting/products/:id takes. Absent when the budget is not tied to a product. |
defaultBudget.productLineId |
number | null | no | The id of the product line tied to this budget. It is a numeric key, not the UUID that GET accounting/product-lines/:id takes. Absent when the budget is not tied to a product line. |
defaultBudget.salesOrderId |
number | no | The id of the sales order this budget bills against. It is a numeric key, not the UUID that GET accounting/sales-orders/:id takes. |
pagination |
object | yes | The pagination cursor for the expenses array. Example: {"hasMore":false}. |
pagination.nextCursor |
string | no | The cursor for the next page. Pass it as the pageCursor query parameter. Absent on the last page. |
pagination.hasMore |
boolean | yes | True when more expenses remain beyond this page. |
{
"data": {
"expenses": [
{
"id": 88213,
"employeeName": "Priya Natarajan",
"categoryName": "Airfare",
"amount": "412.50",
"amountCurrency": "USD",
"status": "Approved",
"billDate": "2026-07-02"
}
],
"budgetMappings": [
{
"expenseItemBaseId": 88213,
"expenseBudgetId": 501,
"name": "Travel",
"budget": "10000.00",
"currency": "USD"
}
],
"expenseBudgets": [
{
"id": 501,
"name": "Travel",
"budget": "10000.00",
"currency": "USD",
"isDefault": false,
"lastModifiedDate": "2026-07-02T09:14:22.000Z"
}
],
"defaultBudget": {
"id": 0,
"name": "<name>",
"budget": "19.99",
"currency": "<currency>",
"isDefault": true,
"lastModifiedDate": "2026-01-15T09:30:00.000Z",
"billable": true,
"productId": 0,
"productLineId": 0,
"salesOrderId": 0
},
"pagination": {
"hasMore": false
}
},
"diagnostics": [
{
"code": "<code>",
"message": "<message>",
"severity": "error",
"path": [
"<path>"
],
"details": "<details>"
}
]
}
Errors
| Code | Status |
|---|---|
EMPLOYEE_NOT_FOUND |
400 |
Statuses
400— Invalid request or business failure. Codes: EMPLOYEE_NOT_FOUND401— The request carries no valid access token.403— Not authorized404— Not found429— Too many requests from this client address. The response has no body and noRetry-Afterheader.500— Internal server error