# Clockify Time Entry API Guide
## Overview
The Time Entry API manages creating, updating, deleting, and querying time entries (time tracking records) within Clockify workspaces.
## Authentication
- `X-Api-Key`: Personal API key
- `X-Addon-Token`: For addon authentication
## Base URL
```
https://api.clockify.me/api/v1
```
---
## Endpoints
### 1. Add Time Entry
**Method:** `POST`
**Path:** `/v1/workspaces/{workspaceId}/time-entries`
**Purpose:** Create a new time entry for the current user
#### Path Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| workspaceId | string | Yes | Workspace identifier |
#### Request Body
```json
{
"start": "2019-08-24T14:15:22Z",
"end": "2019-08-24T15:45:22Z",
"billable": true,
"description": "Working on new feature",
"projectId": "5b641568b07987035750505e",
"taskId": "5b715448b0798751107918ab",
"tagIds": [
"64c777ddd3fcab07cfbb210c",
"74d888eee4gdbc18dgcc321d"
],
"customFields": [
{
"customFieldId": "5e4117fe8c625f38930d57b7",
"value": "CF-001"
}
]
}
```
#### Field Definitions
- **start**: Required, ISO-8601 datetime
- **end**: Optional for running timer (omit for timer start)
- **billable**: Optional, defaults to project setting
- **description**: Optional, may be required by workspace settings
- **projectId**: Optional, may be required by workspace settings
- **taskId**: Optional, requires projectId
- **tagIds**: Optional array of tag IDs
- **customFields**: Optional array of custom field values
#### Response (201 Created)
```json
{
"approvalRequestId": "5e4117fe8c625f38930d57b7",
"billable": true,
"costRate": {
"amount": 10500,
"currency": "USD"
},
"customFieldValues": [
{
"customFieldId": "44a687e29ae1f428e7ebe305",
"sourceType": "WORKSPACE",
"timeEntryId": "64c777ddd3fcab07cfbb210c",
"value": "CF-001"
}
],
"description": "Working on new feature",
"hourlyRate": {
"amount": 10500,
"currency": "USD"
},
"id": "5b715448b0798751107918ab",
"isLocked": false,
"project": {
"clientId": "64c777ddd3fcab07cfbb210c",
"clientName": "Client X",
"color": "#000000",
"id": "5b641568b07987035750505e",
"name": "Software Development"
},
"tags": [
{
"archived": false,
"id": "64c777ddd3fcab07cfbb210c",
"name": "Sprint1",
"workspaceId": "64a687e29ae1f428e7ebe303"
}
],
"task": {
"id": "5b715448b0798751107918ab",
"name": "Bugfixing"
},
"timeInterval": {
"duration": "PT1H30M",
"end": "2019-08-24T15:45:22Z",
"start": "2019-08-24T14:15:22Z"
},
"userId": "5a0ab5acb07987125438b60f",
"workspaceId": "64a687e29ae1f428e7ebe303"
}
```
---
### 2. Get Time Entries for User
**Method:** `GET`
**Path:** `/v1/workspaces/{workspaceId}/user/{userId}/time-entries`
**Purpose:** Retrieve time entries for a specific user with filtering
#### Path Parameters
- **workspaceId**: Workspace identifier
- **userId**: User identifier
#### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| description | string | No | Filter by description substring |
| start | string | No | Start date (ISO-8601) |
| end | string | No | End date (ISO-8601) |
| project | string | No | Filter by project ID |
| task | string | No | Filter by task ID |
| tags | string | No | Filter by tag IDs (repeatable) |
| project-required | boolean | No | Filter entries with/without project |
| task-required | boolean | No | Filter entries with/without task |
| consider-duration-format | boolean | No | Use workspace duration format |
| hydrated | boolean | No | Include full object details |
| in-progress | boolean | No | Filter only running timers |
| page | integer | No | Page number (default: 1) |
| page-size | integer | No | Page size (default: 50) |
#### Request Example
```http
GET /v1/workspaces/64a687e29ae1f428e7ebe303/user/5a0ab5acb07987125438b60f/time-entries?start=2024-01-01T00:00:00Z&end=2024-01-31T23:59:59Z&page=1&page-size=100
X-Api-Key: YOUR_API_KEY
```
#### Response (200 OK)
Returns array of time entry objects (same structure as create response)
---
### 3. Get Time Entry by ID
**Method:** `GET`
**Path:** `/v1/workspaces/{workspaceId}/time-entries/{timeEntryId}`
**Purpose:** Retrieve a specific time entry
#### Path Parameters
- **workspaceId**: Workspace identifier
- **timeEntryId**: Time entry identifier
#### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| consider-duration-format | boolean | No | Use workspace duration format |
| hydrated | boolean | No | Include full object details |
#### Response (200 OK)
Returns single time entry object
---
### 4. Update Time Entry
**Method:** `PUT`
**Path:** `/v1/workspaces/{workspaceId}/time-entries/{timeEntryId}`
**Purpose:** Update an existing time entry
#### Request Body
```json
{
"start": "2019-08-24T14:15:22Z",
"end": "2019-08-24T16:15:22Z",
"billable": true,
"description": "Updated description",
"projectId": "5b641568b07987035750505e",
"taskId": "5b715448b0798751107918ab",
"tagIds": [
"64c777ddd3fcab07cfbb210c"
],
"customFields": [
{
"customFieldId": "5e4117fe8c625f38930d57b7",
"value": "CF-002"
}
]
}
```
#### Response (200 OK)
Returns updated time entry object
#### Notes
- Cannot update locked time entries
- Cannot update approved time entries (must withdraw approval first)
- Validation checks workspace settings (forceDescription, forceProjects, etc.)
---
### 5. Delete Time Entry
**Method:** `DELETE`
**Path:** `/v1/workspaces/{workspaceId}/time-entries/{timeEntryId}`
**Purpose:** Delete a specific time entry
#### Response
204 No Content (success)
#### Constraints
- Cannot delete locked entries
- Cannot delete approved entries
- Deletes associated custom field values
---
### 6. Stop Timer
**Method:** `PATCH`
**Path:** `/v1/workspaces/{workspaceId}/user/{userId}/time-entries`
**Purpose:** Stop the currently running timer for a user
#### Request Body
```json
{
"end": "2019-08-24T16:30:00Z"
}
```
#### Field Definitions
- **end**: Required, ISO-8601 datetime for when timer should stop
#### Response (200 OK)
Returns the stopped time entry object
#### Use Cases
- Stop timer from external integrations
- Batch stop operations
- Automated timer management
---
### 7. Get In-Progress Time Entries
**Method:** `GET`
**Path:** `/v1/workspaces/{workspaceId}/time-entries/in-progress`
**Purpose:** Get all currently running timers in the workspace
#### Response (200 OK)
```json
[
{
"id": "5b715448b0798751107918ab",
"description": "Current work",
"userId": "5a0ab5acb07987125438b60f",
"timeInterval": {
"start": "2019-08-24T14:15:22Z",
"duration": "PT2H30M"
},
"projectId": "5b641568b07987035750505e"
}
]
```
#### Use Cases
- Monitor active timers
- Prevent multiple simultaneous timers
- Team activity dashboard
---
### 8. Add Time Entry for Another User
**Method:** `POST`
**Path:** `/v1/workspaces/{workspaceId}/user/{userId}/time-entries`
**Purpose:** Create a time entry for another user (requires permission)
#### Prerequisites
- Workspace must have "ADD_TIME_FOR_OTHERS" feature
- User must have appropriate permissions
#### Request Body
Same structure as regular time entry creation
#### Response (201 Created)
Returns created time entry object
---
### 9. Bulk Edit Time Entries
**Method:** `PUT`
**Path:** `/v1/workspaces/{workspaceId}/time-entries/bulk-edit`
**Purpose:** Update multiple time entries at once
#### Request Body
```json
{
"timeEntryIds": [
"5b715448b0798751107918ab",
"6c826559c18a9862218a29bc"
],
"operations": [
{
"op": "CHANGE-BILLABLE-STATUS",
"value": true
},
{
"op": "CHANGE-PROJECT",
"value": "5b641568b07987035750505e"
},
{
"op": "CHANGE-TASK",
"value": "5b715448b0798751107918ab"
},
{
"op": "ADD-TAGS",
"value": ["64c777ddd3fcab07cfbb210c"]
},
{
"op": "REMOVE-TAGS",
"value": ["74d888eee4gdbc18dgcc321d"]
}
]
}
```
#### Supported Operations
- **CHANGE-BILLABLE-STATUS**: Set billable flag
- **CHANGE-PROJECT**: Change project
- **CHANGE-TASK**: Change task
- **ADD-TAGS**: Add tags to entries
- **REMOVE-TAGS**: Remove tags from entries
#### Response (200 OK)
Returns array of updated time entry objects
#### Constraints
- All entries must be unlocked
- All entries must not be approved
- Maximum number of entries per request varies by workspace
---
### 10. Duplicate Time Entry
**Method:** `POST`
**Path:** `/v1/workspaces/{workspaceId}/time-entries/{timeEntryId}/duplicate`
**Purpose:** Create a copy of an existing time entry
#### Request Body
```json
{
"start": "2019-08-25T14:15:22Z",
"end": "2019-08-25T15:45:22Z"
}
```
#### Field Definitions
- **start**: Required, new start time
- **end**: Optional, new end time (omit for running timer)
#### Response (201 Created)
Returns duplicated time entry with all original properties except times
#### Use Cases
- Quick entry for repetitive tasks
- Template-based time tracking
- Batch similar entries
---
### 11. Mark Time Entries as Invoiced
**Method:** `PATCH`
**Path:** `/v1/workspaces/{workspaceId}/time-entries/invoiced`
**Purpose:** Mark multiple time entries as invoiced
#### Request Body
```json
{
"timeEntryIds": [
"5b715448b0798751107918ab",
"6c826559c18a9862218a29bc"
],
"invoiced": true
}
```
#### Response (200 OK)
Returns array of updated time entry objects
#### Use Cases
- Invoice generation workflows
- Billing cycle management
- Prevent duplicate invoicing
---
### 12. Delete All Time Entries for User
**Method:** `DELETE`
**Path:** `/v1/workspaces/{workspaceId}/user/{userId}/time-entries`
**Purpose:** Delete all time entries for a specific user (dangerous operation)
#### Response
204 No Content (success)
#### Warnings
- ⚠️ **DANGEROUS**: Cannot be undone
- Only deletes unlocked, unapproved entries
- Use with extreme caution
- Recommend filtering by date range instead
---
## Data Models
### TimeEntry Object
```typescript
{
approvalRequestId?: string;
billable: boolean;
costRate?: Rate;
customFieldValues: CustomFieldValue[];
description: string;
hourlyRate?: Rate;
id: string;
isLocked: boolean;
kioskId?: string;
project?: ProjectReference;
tags: Tag[];
task?: TaskReference;
timeInterval: TimeInterval;
type: "REGULAR" | "BREAK";
userId: string;
workspaceId: string;
}
```
### TimeInterval Object
```typescript
{
duration: string; // ISO-8601 duration (e.g., "PT1H30M")
end?: string; // ISO-8601 datetime (null for running timer)
start: string; // ISO-8601 datetime
offsetStart?: number; // Timezone offset in minutes
offsetEnd?: number; // Timezone offset in minutes
timeZone?: {
id: string;
rules: {
fixedOffset: boolean;
transitions: any[];
transitionRules: any[];
}
}
}
```
### ProjectReference Object
```typescript
{
clientId?: string;
clientName?: string;
color: string; // Hex color code
id: string;
name: string;
}
```
### TaskReference Object
```typescript
{
id: string;
name: string;
}
```
### Tag Object
```typescript
{
archived: boolean;
id: string;
name: string;
workspaceId: string;
}
```
---
## Implementation Examples
### Python: Create Time Entry
```python
import requests
from datetime import datetime, timedelta
API_KEY = "your_api_key_here"
BASE_URL = "https://api.clockify.me/api/v1"
headers = {
"X-Api-Key": API_KEY,
"Content-Type": "application/json"
}
def create_time_entry(workspace_id, start_time, end_time, project_id, description=""):
url = f"{BASE_URL}/workspaces/{workspace_id}/time-entries"
payload = {
"start": start_time.isoformat() + "Z",
"billable": True,
"description": description,
"projectId": project_id
}
# Add end time if provided (otherwise starts timer)
if end_time:
payload["end"] = end_time.isoformat() + "Z"
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 201:
return response.json()
else:
raise Exception(f"Failed to create time entry: {response.text}")
# Create completed time entry
start = datetime(2024, 1, 29, 9, 0)
end = datetime(2024, 1, 29, 17, 0)
entry = create_time_entry(
"workspace_id",
start,
end,
"project_id",
"Full day of development work"
)
print(f"Created entry: {entry['id']}")
```
### Python: Start and Stop Timer
```python
def start_timer(workspace_id, description, project_id=None):
"""Start a new timer (no end time)"""
url = f"{BASE_URL}/workspaces/{workspace_id}/time-entries"
payload = {
"start": datetime.utcnow().isoformat() + "Z",
"description": description
}
if project_id:
payload["projectId"] = project_id
response = requests.post(url, headers=headers, json=payload)
return response.json()
def stop_timer(workspace_id, user_id):
"""Stop currently running timer"""
url = f"{BASE_URL}/workspaces/{workspace_id}/user/{user_id}/time-entries"
payload = {
"end": datetime.utcnow().isoformat() + "Z"
}
response = requests.patch(url, headers=headers, json=payload)
return response.json()
# Start timer
timer = start_timer("workspace_id", "Working on feature X", "project_id")
# Do work...
# Stop timer
stopped = stop_timer("workspace_id", "user_id")
print(f"Timer duration: {stopped['timeInterval']['duration']}")
```
### Python: Get Time Entries with Filtering
```python
from datetime import date
def get_time_entries_for_period(workspace_id, user_id, start_date, end_date, project_id=None):
"""Get all time entries for a date range"""
url = f"{BASE_URL}/workspaces/{workspace_id}/user/{user_id}/time-entries"
params = {
"start": start_date.isoformat() + "T00:00:00Z",
"end": end_date.isoformat() + "T23:59:59Z",
"page-size": 1000,
"hydrated": "true"
}
if project_id:
params["project"] = project_id
response = requests.get(url, headers=headers, params=params)
return response.json()
# Get all entries for January 2024
entries = get_time_entries_for_period(
"workspace_id",
"user_id",
date(2024, 1, 1),
date(2024, 1, 31)
)
print(f"Found {len(entries)} time entries")
for entry in entries:
duration = entry['timeInterval']['duration']
desc = entry['description']
print(f" {duration}: {desc}")
```
### Python: Bulk Edit Time Entries
```python
def bulk_update_project(workspace_id, time_entry_ids, new_project_id):
"""Change project for multiple time entries"""
url = f"{BASE_URL}/workspaces/{workspace_id}/time-entries/bulk-edit"
payload = {
"timeEntryIds": time_entry_ids,
"operations": [
{
"op": "CHANGE-PROJECT",
"value": new_project_id
}
]
}
response = requests.put(url, headers=headers, json=payload)
return response.json()
def bulk_add_tags(workspace_id, time_entry_ids, tag_ids):
"""Add tags to multiple time entries"""
url = f"{BASE_URL}/workspaces/{workspace_id}/time-entries/bulk-edit"
payload = {
"timeEntryIds": time_entry_ids,
"operations": [
{
"op": "ADD-TAGS",
"value": tag_ids
}
]
}
response = requests.put(url, headers=headers, json=payload)
return response.json()
# Move multiple entries to different project
entry_ids = ["entry1", "entry2", "entry3"]
bulk_update_project("workspace_id", entry_ids, "new_project_id")
# Add tags to entries
tag_ids = ["tag1", "tag2"]
bulk_add_tags("workspace_id", entry_ids, tag_ids)
```
### Python: Duplicate Entry for Multiple Days
```python
def duplicate_entry_for_days(workspace_id, template_entry_id, dates):
"""Duplicate an entry for multiple dates"""
url_template = f"{BASE_URL}/workspaces/{workspace_id}/time-entries/{template_entry_id}/duplicate"
# Get original entry to calculate duration
entry_url = f"{BASE_URL}/workspaces/{workspace_id}/time-entries/{template_entry_id}"
original = requests.get(entry_url, headers=headers).json()
# Parse original duration
start = datetime.fromisoformat(original['timeInterval']['start'].replace('Z', '+00:00'))
end = datetime.fromisoformat(original['timeInterval']['end'].replace('Z', '+00:00'))
duration = end - start
duplicated = []
for target_date in dates:
# Calculate new start/end times
new_start = datetime.combine(target_date, start.time())
new_end = new_start + duration
payload = {
"start": new_start.isoformat() + "Z",
"end": new_end.isoformat() + "Z"
}
response = requests.post(url_template, headers=headers, json=payload)
if response.status_code == 201:
duplicated.append(response.json())
return duplicated
# Duplicate Monday's entry for Tuesday-Friday
from datetime import date, timedelta
monday_entry_id = "entry_id"
dates = [date(2024, 1, 23) + timedelta(days=i) for i in range(1, 5)]
duplicates = duplicate_entry_for_days("workspace_id", monday_entry_id, dates)
print(f"Created {len(duplicates)} duplicate entries")
```
### Python: Check for Running Timers
```python
def has_running_timer(workspace_id, user_id):
"""Check if user has a running timer"""
url = f"{BASE_URL}/workspaces/{workspace_id}/time-entries/in-progress"
response = requests.get(url, headers=headers)
if response.status_code == 200:
timers = response.json()
user_timers = [t for t in timers if t['userId'] == user_id]
return len(user_timers) > 0, user_timers
return False, []
def ensure_no_running_timer(workspace_id, user_id):
"""Stop any running timer before starting new one"""
has_timer, timers = has_running_timer(workspace_id, user_id)
if has_timer:
# Stop the running timer
stop_timer(workspace_id, user_id)
print(f"Stopped running timer: {timers[0]['description']}")
return not has_timer
# Safe timer start
if ensure_no_running_timer("workspace_id", "user_id"):
start_timer("workspace_id", "New task")
```
---
## Best Practices
1. **Time Zones**: Always use UTC times (append 'Z' to ISO-8601 strings)
2. **Duration Format**: Use ISO-8601 duration (PT{hours}H{minutes}M)
3. **Running Timers**: Check for existing running timers before starting new ones
4. **Validation**: Respect workspace settings (forceDescription, forceProjects, etc.)
5. **Pagination**: Use pagination for large date ranges
6. **Bulk Operations**: Use bulk edit for updating multiple entries
7. **Locked Entries**: Check isLocked before attempting updates
8. **Error Handling**: Handle validation errors from workspace settings
---
## Common Use Cases
### Daily Time Report
```python
def daily_time_report(workspace_id, user_id, target_date):
"""Generate daily time report"""
entries = get_time_entries_for_period(
workspace_id,
user_id,
target_date,
target_date
)
total_minutes = 0
billable_minutes = 0
by_project = {}
for entry in entries:
# Parse duration (PT1H30M format)
duration = entry['timeInterval']['duration']
hours = int(duration.split('H')[0].replace('PT', '')) if 'H' in duration else 0
minutes = int(duration.split('H')[1].split('M')[0]) if 'H' in duration and 'M' in duration else 0
if 'H' not in duration and 'M' in duration:
minutes = int(duration.replace('PT', '').replace('M', ''))
entry_minutes = hours * 60 + minutes
total_minutes += entry_minutes
if entry['billable']:
billable_minutes += entry_minutes
# Group by project
project_name = entry.get('project', {}).get('name', 'No Project')
by_project[project_name] = by_project.get(project_name, 0) + entry_minutes
return {
"total_hours": total_minutes / 60,
"billable_hours": billable_minutes / 60,
"by_project": {k: v/60 for k, v in by_project.items()}
}
```
### Weekly Time Synchronization
```python
def sync_week_from_template(workspace_id, template_day_entries, week_start_date):
"""Replicate time entries from a template day across a week"""
for i in range(5): # Mon-Fri
target_date = week_start_date + timedelta(days=i)
for entry in template_day_entries:
# Extract time from template
start_time = datetime.fromisoformat(
entry['timeInterval']['start'].replace('Z', '+00:00')
).time()
end_time = datetime.fromisoformat(
entry['timeInterval']['end'].replace('Z', '+00:00')
).time()
# Create new entry for target date
new_start = datetime.combine(target_date, start_time)
new_end = datetime.combine(target_date, end_time)
create_time_entry(
workspace_id,
new_start,
new_end,
entry['project']['id'],
entry['description']
)
```
---
## Rate Limiting
- Addon token: 50 requests/second per workspace
- Consider batching operations for large datasets
---
## Notes
- Time entries cannot be edited if locked (workspace automatic lock settings)
- Time entries in approval workflow cannot be edited without withdrawal
- Running timers have no end time (null)
- Duration is calculated server-side based on start/end times
- Custom fields must exist in workspace and match type constraints
- Billable status inherits from project if not explicitly set
- Maximum description length varies by workspace settings