Custom Roles
Custom roles are the custom profiles referenced on the Users page. They are unrelated to the descriptive user roles managed at /v2/roles.
The custom role object
| Attribute | Type | Description |
|---|---|---|
id |
integer | Unique ID for the custom role. |
name |
string | The name of the custom role. |
permission_ids |
array of integers | The IDs of the permissions the custom role grants, deduplicated and sorted ascending. Permission IDs the account cannot see are stored on the custom role but omitted from the response. |
created_at |
datetime | Date and time the custom role was created. Use the ISO 8601 Format. |
updated_at |
datetime | Date and time the custom role was last updated. Use the ISO 8601 Format. |
Required permissions
The account must have custom profiles enabled. You must have permission to manage all users in order to interact with the /v2/custom_roles endpoint. Insufficient permissions will result in a 403 Forbidden status code.
List all custom roles
Beta endpoint. Fully available, but the response shape may still change. Until it is frozen, every response includes
Harvest-Api-Stability: beta.
Returns a list of the account’s custom roles, sorted by descending ID.
The response contains an object with a custom_roles property that contains an array of up to per_page custom roles. Each entry in the array is a separate custom role object. If no more custom roles are available, the resulting array will be empty. Several additional pagination properties are included in the response to simplify paginating your custom roles.
GET /v2/custom_roles
| Parameter | Type | Description |
|---|---|---|
updated_since |
datetime | Only return custom roles whose updated_at is later than this timestamp. |
cursor |
string | A cursor from a previous response’s links or next_page value. Takes precedence over page. |
page |
integer | The page number to use in pagination. Omitting this parameter, or passing 1, uses cursor-based pagination. |
per_page |
integer | The number of records to return per page. Can range between 1 and 2000. (Default: 2000) |
Example Request:
curl "https://api.harvestapp.com/v2/custom_roles" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Harvest-Account-Id: $ACCOUNT_ID" \
-H "User-Agent: MyApp ([email protected])"
Example Response:
{
"custom_roles": [
{
"id": 901,
"name": "Team Lead",
"permission_ids": [3, 7, 12],
"created_at": "2026-10-16T10:00:00Z",
"updated_at": "2026-10-16T10:00:00Z"
},
{
"id": 900,
"name": "Contractor",
"permission_ids": [3],
"created_at": "2026-10-15T09:30:00Z",
"updated_at": "2026-10-15T09:30:00Z"
}
],
"per_page": 2000,
"total_pages": 1,
"total_entries": 2,
"next_page": null,
"previous_page": null,
"page": 1,
"links": {
"first": "https://api.harvestapp.com/v2/custom_roles?page=1&per_page=2000&ref=first",
"next": null,
"previous": null,
"last": "https://api.harvestapp.com/v2/custom_roles?page=1&per_page=2000&ref=last"
}
}Retrieve a custom role
Beta endpoint. Fully available, but the response shape may still change. Until it is frozen, every response includes
Harvest-Api-Stability: beta.
Retrieves the custom role with the given ID. Returns a custom role object and a 200 OK response code if a valid identifier was provided.
GET /v2/custom_roles/{CUSTOM_ROLE_ID}
Example Request:
curl "https://api.harvestapp.com/v2/custom_roles/901" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Harvest-Account-Id: $ACCOUNT_ID" \
-H "User-Agent: MyApp ([email protected])"
Example Response:
{
"id": 901,
"name": "Team Lead",
"permission_ids": [3, 7, 12],
"created_at": "2026-10-16T10:00:00Z",
"updated_at": "2026-10-16T10:00:00Z"
}Create a custom role
Beta endpoint. Fully available, but the response shape may still change. Until it is frozen, every response includes
Harvest-Api-Stability: beta.
Creates a custom role. An account can have a maximum of 50 custom roles. Returns a custom role object and a 201 Created response code if the call succeeded. The response includes a Location header pointing to the created custom role.
POST /v2/custom_roles
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | required | The name of the custom role. Maximum 100 characters, unique within the account, compared case-insensitively. |
permission_ids |
array of integers | required | The complete set of permission IDs the custom role grants. Must contain at least one ID; null and an empty array are rejected. |
The permission_ids values are the IDs published by GET /v2/permissions. Numeric strings are accepted.
Example Request:
curl -X POST "https://api.harvestapp.com/v2/custom_roles" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Harvest-Account-Id: $ACCOUNT_ID" \
-H "Content-Type: application/json" \
-H "User-Agent: MyApp ([email protected])" \
-d '{"name": "Team Lead", "permission_ids": [3, 7, 12]}'
Example Response:
{
"id": 901,
"name": "Team Lead",
"permission_ids": [3, 7, 12],
"created_at": "2026-10-16T10:00:00Z",
"updated_at": "2026-10-16T10:00:00Z"
}Update a custom role
Beta endpoint. Fully available, but the response shape may still change. Until it is frozen, every response includes
Harvest-Api-Stability: beta.
Updates the custom role with the given ID by setting the values of the parameters passed. Any parameters not provided are left unchanged. Returns a custom role object and a 200 OK response code if the call succeeded.
PATCH /v2/custom_roles/{CUSTOM_ROLE_ID}
| Parameter | Type | Description |
|---|---|---|
name |
string | The name of the custom role. Maximum 100 characters, unique within the account, compared case-insensitively. |
permission_ids |
array of integers | The complete set of permission IDs the custom role grants. Must contain at least one ID; null and an empty array are rejected. Omitting the parameter leaves the permission set unchanged. |
Changing permission_ids updates the permissions of every user on the custom role, which can remove a user as project manager from the projects they manage. Renaming the custom role does not change anyone’s permissions.
Example Request:
curl -X PATCH "https://api.harvestapp.com/v2/custom_roles/901" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Harvest-Account-Id: $ACCOUNT_ID" \
-H "Content-Type: application/json" \
-H "User-Agent: MyApp ([email protected])" \
-d '{"permission_ids": [3, 7, 12, 18]}'
Example Response:
{
"id": 901,
"name": "Team Lead",
"permission_ids": [3, 7, 12, 18],
"created_at": "2026-10-16T10:00:00Z",
"updated_at": "2026-10-16T14:30:00Z"
}Delete a custom role
Beta endpoint. Fully available, but the response shape may still change. Until it is frozen, every response includes
Harvest-Api-Stability: beta.
Deletes the custom role with the given ID. Returns the deleted custom role object and a 200 OK response code if the call succeeded.
DELETE /v2/custom_roles/{CUSTOM_ROLE_ID}
Users on the custom role are moved to the member access role and their direct permissions are cleared.
Example Request:
curl -X DELETE "https://api.harvestapp.com/v2/custom_roles/900" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Harvest-Account-Id: $ACCOUNT_ID" \
-H "User-Agent: MyApp ([email protected])"
Example Response:
{
"id": 900,
"name": "Contractor",
"permission_ids": [3],
"created_at": "2026-10-15T09:30:00Z",
"updated_at": "2026-10-15T09:30:00Z"
}Errors
The following status codes are returned for the custom role endpoints:
| Status | Description |
|---|---|
400 |
A required User-Agent header is missing (User-Agent header required), permission_ids is not an array of permission IDs (Invalid permission_ids provided: must be an array of permission ids), or updated_since is not a valid datetime (Invalid updated_since datetime provided: "…"). |
401 |
Authentication failed: Authentication failed for API request. |
403 |
The account is inactive (Account inactive), the account does not have custom profiles enabled (Custom profiles feature is not enabled for this account), or the caller is not authorized (Not authorized!). |
404 |
The custom role does not exist, was deleted, or belongs to another account: Record not found. |
415 |
A Content-Type: application/json header is required when sending a body: Content-Type header required to be application/json, got …. |
422 |
A validation failed. See the messages below. |
500 |
An unexpected error occurred. |
The 422 responses use these messages:
Name has already been takenPermission ids can't be blankPermission ids contain invalid permissionsPermission ids contain unavailable permissionsPermission ids can’t include Forecast permissionsA custom profile with these exact permissions already exists ("…")., naming the existing custom roleThese permissions match the "…" base profile. Add at least one extra permission to create a custom profile.(ends withto save a custom profile.when updating)Maximum of 50 custom role templates per accountYou cannot give someone permissions that you do not have yourself. Ask an Administrator to do it. You do not have: …You cannot change your own permissions. Ask another Administrator to do it.Invalid cursor parameter …