Overview
Time entry categories provide a way to classify the type of work being performed. Categories are company-specific and can be used to distinguish between billable work, training, travel, meetings, and other work types.
Key Features
- Company-scoped: Each category belongs to a single company
- Color Coding: Visual identification with hex color codes
- Default Category: Mark one category as default for quick entry creation
- Active/Inactive Status: Control which categories are available for selection
- Unique Names: Category names must be unique within a company
Authentication
All endpoints require:
- Valid JWT authentication token
- Platform admin role (checked via middleware)
Create Category
curl -X POST https://api.example.com/categories \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"companyId": "550e8400-e29b-41d4-a716-446655440000",
"name": "Development",
"color": "#6366F1",
"isDefault": false,
"isActive": true
}'
{
"success": true,
"data": {
"id": "880e8400-e29b-41d4-a716-446655440000",
"companyId": "550e8400-e29b-41d4-a716-446655440000",
"name": "Development",
"color": "#6366F1",
"isDefault": false,
"isActive": true,
"createdAt": "2026-03-04T15:30:00.000Z",
"updatedAt": "2026-03-04T15:30:00.000Z",
"createdBy": "440e8400-e29b-41d4-a716-446655440000"
}
}
Request Body
UUID of the company this category belongs to
Category name (1-100 characters)Must be unique within the company
Hex color code for visual identification (e.g., “#6366F1”)Must match pattern: ^#[0-9A-Fa-f]{6}$
Whether this category should be pre-selected when creating new time entriesOnly one category per company can be marked as default. Application logic should enforce this in the UI.
Whether the category is active and available for selection
Response Fields
All request fields plus:
id: Generated UUID
createdAt, updatedAt: Timestamps
createdBy: User ID who created the category
List Categories
curl -X GET "https://api.example.com/categories?companyId=550e8400-e29b-41d4-a716-446655440000&isActive=true" \
-H "Authorization: Bearer YOUR_TOKEN"
{
"success": true,
"data": [
{
"id": "880e8400-e29b-41d4-a716-446655440000",
"companyId": "550e8400-e29b-41d4-a716-446655440000",
"name": "Development",
"color": "#6366F1",
"isDefault": true,
"isActive": true,
"createdAt": "2026-03-04T15:30:00.000Z",
"updatedAt": "2026-03-04T15:30:00.000Z"
},
{
"id": "881e8400-e29b-41d4-a716-446655440000",
"companyId": "550e8400-e29b-41d4-a716-446655440000",
"name": "Training",
"color": "#F59E0B",
"isDefault": false,
"isActive": true,
"createdAt": "2026-03-04T15:31:00.000Z",
"updatedAt": "2026-03-04T15:31:00.000Z"
},
{
"id": "882e8400-e29b-41d4-a716-446655440000",
"companyId": "550e8400-e29b-41d4-a716-446655440000",
"name": "Travel",
"color": "#8B5CF6",
"isDefault": false,
"isActive": true,
"createdAt": "2026-03-04T15:32:00.000Z",
"updatedAt": "2026-03-04T15:32:00.000Z"
}
]
}
Query Parameters
Filter by active status
true: Only active categories
false: Only inactive categories
- Omit: All categories
Unlike other list endpoints, categories do not support pagination. All categories for the company are returned in a single response.
Update Category
curl -X PATCH https://api.example.com/categories/880e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Software Development",
"color": "#3B82F6",
"isDefault": true
}'
{
"success": true,
"data": {
"id": "880e8400-e29b-41d4-a716-446655440000",
"companyId": "550e8400-e29b-41d4-a716-446655440000",
"name": "Software Development",
"color": "#3B82F6",
"isDefault": true,
"isActive": true,
"updatedAt": "2026-03-04T16:00:00.000Z"
}
}
Path Parameters
UUID of the category to update
Request Body
All fields are optional. Only provided fields will be updated.
Update category name (1-100 characters)Must remain unique within the company
Update hex color code (must match ^#[0-9A-Fa-f]{6}$)
Set as default categoryWhen setting isDefault: true, you should ensure only one category per company has this flag. Implement application logic to clear the flag from other categories.
Delete Category
curl -X DELETE https://api.example.com/categories/880e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer YOUR_TOKEN"
{
"success": true,
"message": "Category deleted successfully"
}
Path Parameters
UUID of the category to delete
When a category is deleted:
- All time entries linked to this category will have their
categoryId set to null (cascade: SetNull)
- This is a permanent operation and cannot be undone
- Consider marking the category as inactive instead if you want to preserve the relationship
Category Colors
Default Color
The default category color is #6366F1 (indigo).
Recommended Colors by Type
| Category Type | Suggested Color | Hex Code |
|---|
| Development | Indigo | #6366F1 |
| Training | Amber | #F59E0B |
| Travel | Purple | #8B5CF6 |
| Meetings | Blue | #3B82F6 |
| Maintenance | Green | #10B981 |
| Support | Red | #EF4444 |
| Planning | Pink | #EC4899 |
| Documentation | Cyan | #06B6D4 |
Common Category Setups
Standard Work Categories
[
{ "name": "Development", "color": "#6366F1", "isDefault": true },
{ "name": "Code Review", "color": "#8B5CF6" },
{ "name": "Testing", "color": "#10B981" },
{ "name": "Documentation", "color": "#06B6D4" },
{ "name": "Meetings", "color": "#3B82F6" }
]
Professional Services Categories
[
{ "name": "Billable Work", "color": "#10B981", "isDefault": true },
{ "name": "Internal", "color": "#6366F1" },
{ "name": "Training", "color": "#F59E0B" },
{ "name": "Travel", "color": "#8B5CF6" },
{ "name": "Admin", "color": "#64748B" }
]
Consulting Categories
[
{ "name": "Client Consulting", "color": "#3B82F6", "isDefault": true },
{ "name": "Research", "color": "#8B5CF6" },
{ "name": "Proposal Writing", "color": "#EC4899" },
{ "name": "Client Meetings", "color": "#10B981" },
{ "name": "Travel", "color": "#F97316" }
]
Best Practices
Naming Conventions
- Be Specific: Use clear names like “Client Meetings” instead of just “Meetings”
- Keep It Short: Aim for 1-2 words when possible
- Be Consistent: Use similar naming patterns across categories (e.g., all verbs or all nouns)
- Avoid Abbreviations: Use “Development” instead of “Dev” for clarity
Default Category Strategy
// Set the most commonly used category as default
await updateCategory(developmentCategoryId, {
isDefault: true
});
// This will be pre-selected in time entry forms
Active Status Management
// Archive old categories instead of deleting
await updateCategory(oldCategoryId, {
isActive: false,
name: "[ARCHIVED] Old Category Name"
});
Unique Name Constraint
The database enforces unique category names per company:
@@unique([companyId, name])
If you attempt to create a duplicate:
{
"success": false,
"error": "A category with this name already exists in this company"
}
Integration Examples
Creating categories for a new company
const categories = [
{ name: "Development", color: "#6366F1", isDefault: true },
{ name: "Training", color: "#F59E0B" },
{ name: "Travel", color: "#8B5CF6" },
{ name: "Meetings", color: "#3B82F6" },
];
for (const category of categories) {
await fetch('https://api.example.com/categories', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
companyId: newCompany.id,
...category,
isActive: true
})
});
}
Using categories in time entry forms
// Fetch active categories for dropdown
const response = await fetch(
`https://api.example.com/categories?companyId=${companyId}&isActive=true`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
const { data: categories } = await response.json();
// Find default category
const defaultCategory = categories.find(c => c.isDefault);
// Pre-populate form
setFormData({
...formData,
categoryId: defaultCategory?.id || categories[0]?.id
});
Color-coded time reporting
// Group time entries by category for visual report
const entriesByCategory = timeEntries.reduce((acc, entry) => {
const categoryName = entry.category?.name || 'Uncategorized';
const categoryColor = entry.category?.color || '#64748B';
if (!acc[categoryName]) {
acc[categoryName] = {
color: categoryColor,
hours: 0,
entries: []
};
}
acc[categoryName].hours += parseFloat(entry.hours);
acc[categoryName].entries.push(entry);
return acc;
}, {});