Skip to main content

Categories

The Categories API provides CRUD access to categories, plus ancillary actions for assigning team members to a category and migrating a category’s resources into another category.
New to the API? Start with Getting started (base URL, response envelope, errors, pagination) and Authentication (API keys). Those conventions apply to every endpoint below and are not repeated here.
Identifier note: categories are addressed by their numeric category_id. Wherever {id} appears below it is the category_id.

The category object

All endpoints that return a category use this shape:
{
  "id": 3,
  "name": "Development",
  "description": "Software development work.",
  "type": "project",
  "visibility": "everyone",
  "icon": "ti ti-folder",
  "system_default": "no",
  "slug": "3-development",
  "count": 12,
  "users_count": 4,
  "dates": {
    "created": "2026-06-15T09:30:00.000000Z",
    "updated": "2026-06-15T09:30:00.000000Z"
  }
}
FieldTypeNotes
idintegerCategory id. Used in all URLs.
namestringCategory name.
descriptionstring|nullCategory description.
typestringResource type the category belongs to (e.g. project, client, ticket, invoice, …). Set on create, immutable thereafter.
visibilitystring|nullCategory visibility.
iconstring|nullIcon class.
system_defaultstringyes if this is a system-default category (cannot be deleted).
slugstringURL slug (auto-generated).
countintegerNumber of resources of this category’s type currently in the category.
users_countintegerNumber of team members assigned to the category.
datesobjectcreated, updated.

List / search categories

GET /api/categories

Query parameters

ParameterTypeDescription
typestringFilter by category type.
visibilitystringFilter by visibility.
searchstringFree-text search on the category name.
sortstringcategory_name, category_type, category_created.
orderstringasc or desc (default desc).
limitintegerResults per page (max 100).
pageintegerPage number.

Example request

curl -G https://your-domain/api/categories \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d type=project \
  -d limit=25
<?php
$query = http_build_query(['type' => 'project', 'limit' => 25]);
$ch = curl_init("https://your-domain/api/categories?$query");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer YOUR_API_KEY'],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Example response — 200 OK

{
  "data": [ { "id": 3, "name": "Development", "type": "project" } ],
  "meta": { "current_page": 1, "per_page": 25, "total": 1, "last_page": 1 },
  "message": "Categories retrieved successfully."
}

Get a category

GET /api/categories/{id}

Example request

curl https://your-domain/api/categories/3 \
  -H "Authorization: Bearer YOUR_API_KEY"
<?php
$ch = curl_init('https://your-domain/api/categories/3');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer YOUR_API_KEY'],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
Returns the category (200) with message “Category retrieved successfully.”

Create a category

POST /api/categories

Body parameters

ParameterTypeRequiredNotes
category_namestringyesMust be unique within the same category_type.
category_typestringyesResource type (e.g. project, client, ticket, …). Not changeable later.
category_visibilitystringno
category_descriptionstringno
category_iconstringnoIcon class.

Example request

curl -X POST https://your-domain/api/categories \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d category_name="Development" \
  -d category_type=project
<?php
$ch = curl_init('https://your-domain/api/categories');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer YOUR_API_KEY'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'category_name' => 'Development',
        'category_type' => 'project',
    ]),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Example response — 201 Created

{
  "data": { "id": 3, "name": "Development", "type": "project" },
  "message": "Category created successfully."
}

Update a category

PATCH /api/categories/{id}
category_name is required and must be unique within the category’s type. category_type cannot be changed.

Body parameters

ParameterTypeRequiredNotes
category_namestringyes
category_visibilitystringno
category_descriptionstringno
category_iconstringno

Example request

curl -X PATCH https://your-domain/api/categories/3 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d category_name="Dev & Engineering"
<?php
$ch = curl_init('https://your-domain/api/categories/3');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer YOUR_API_KEY'],
    CURLOPT_POSTFIELDS     => http_build_query(['category_name' => 'Dev & Engineering']),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Example response — 200 OK

{
  "data": { "id": 3, "name": "Dev & Engineering", "type": "project" },
  "message": "Category updated successfully."
}

Delete a category

DELETE /api/categories/{id}
A category can only be deleted when it is empty (count is 0) and is not a system-default category. Otherwise a 409 is returned. To empty a non-empty category first, use the migrate action below. Deleting also removes the category’s team assignments.

Example request

curl -X DELETE https://your-domain/api/categories/3 \
  -H "Authorization: Bearer YOUR_API_KEY"
<?php
$ch = curl_init('https://your-domain/api/categories/3');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer YOUR_API_KEY'],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Example response — 200 OK

{
  "data": { "id": 3 },
  "message": "Category deleted successfully."
}

Set team members

PUT /api/categories/{id}/team
Sets the team members assigned to the category. The users array is the full set — it replaces any existing assignment; an empty array clears all. Category-based project permissions are re-synced automatically.
ParameterTypeRequiredNotes
usersarray of integersyesTeam user ids. Empty array clears all.
curl -X PUT https://your-domain/api/categories/3/team \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d "users[]=5" -d "users[]=8"
<?php
$ch = curl_init('https://your-domain/api/categories/3/team');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer YOUR_API_KEY'],
    CURLOPT_POSTFIELDS     => http_build_query(['users' => [5, 8]]),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
Returns the category (200) with message “Category team updated successfully.”

Migrate resources

PUT /api/categories/{id}/migrate
Moves all of this category’s resources (projects, clients, invoices, estimates, leads, tickets, items, expenses, contracts) into another category, and re-syncs category-based project permissions. Use this to empty a category before deleting it. Returns the (now-emptied) source category.
ParameterTypeRequiredNotes
target_idintegeryesDestination category id. Must exist and differ from {id}.
curl -X PUT https://your-domain/api/categories/3/migrate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d target_id=4
<?php
$ch = curl_init('https://your-domain/api/categories/3/migrate');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer YOUR_API_KEY'],
    CURLOPT_POSTFIELDS     => http_build_query(['target_id' => 4]),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
Returns the source category (200) with message “Category resources migrated successfully.”

Errors

See Getting started for the shared error format. Category-specific:
StatusMeaning
404 Not FoundThe category id does not exist.
409 ConflictCannot delete a system-default or non-empty category.
422 Unprocessable EntityValidation failed (e.g. missing name/type, or a duplicate name within the type).
{
  "message": "The given data was invalid.",
  "errors": {
    "category_name": ["A category with this name already exists."],
    "category_type": ["The category type field is required."]
  }
}