# Clockify Webhooks API Guide
## Overview
Webhooks enable real-time event notifications from Clockify to external applications. When specific events occur (timer started, time entry deleted, etc.), Clockify sends HTTP POST requests to configured webhook URLs.
## Authentication
- `X-Api-Key`: Personal API key
- `X-Addon-Token`: For addon authentication
## Base URL
```
https://api.clockify.me/api/v1
```
---
## Webhook Concepts
### Event Flow
1. Configure webhook with target URL and event type
2. Event occurs in Clockify (e.g., timer started)
3. Clockify sends POST request to your URL
4. Your endpoint processes the event
5. Clockify logs the attempt
### Limits
- **Per Admin**: 10 webhooks maximum
- **Per Workspace**: 100 webhooks total
- **Rate Limit**: 50 requests/second (addon tokens)
### Authentication Token
- Each webhook has unique auth token (JWT)
- Token sent in request header: `X-Clockify-Webhook-Signature`
- Use to verify requests are from Clockify
---
## Endpoints
### 1. Get All Webhooks on Workspace
**Method:** `GET`
**Path:** `/v1/workspaces/{workspaceId}/webhooks`
**Purpose:** List all webhooks configured in workspace
#### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| type | string | No | Filter by type: USER_CREATED, SYSTEM, ADDON |
#### Response (200 OK)
```json
{
"webhooks": [
{
"authToken": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"enabled": true,
"id": "76a687e29ae1f428e7ebe101",
"name": "Time Entry Tracker",
"triggerSource": [
"54a687e29ae1f428e7ebe909"
],
"triggerSourceType": "PROJECT_ID",
"url": "https://example.com/webhooks/clockify",
"userId": "5a0ab5acb07987125438b60f",
"webhookEvent": "NEW_TIME_ENTRY",
"workspaceId": "64a687e29ae1f428e7ebe303"
}
],
"workspaceWebhookCount": 5
}
```
---
### 2. Get All Webhooks for Addon
**Method:** `GET`
**Path:** `/v1/workspaces/{workspaceId}/addons/{addonId}/webhooks`
**Purpose:** List webhooks created by specific addon
#### Path Parameters
- **workspaceId**: Workspace identifier
- **addonId**: Addon identifier
#### Response
Same structure as Get All Webhooks
---
### 3. Create Webhook
**Method:** `POST`
**Path:** `/v1/workspaces/{workspaceId}/webhooks`
**Purpose:** Create a new webhook
#### Request Body
```json
{
"name": "Project Updates Monitor",
"triggerSource": [
"54a687e29ae1f428e7ebe909",
"87p187e29ae1f428e7ebej56"
],
"triggerSourceType": "PROJECT_ID",
"url": "https://example.com/webhooks/clockify",
"webhookEvent": "NEW_PROJECT"
}
```
#### Field Definitions
- **name**: Optional, 2-30 characters, webhook display name
- **triggerSource**: Required, array of IDs (depends on triggerSourceType)
- **triggerSourceType**: Required, what entity triggers the webhook
- **url**: Required, HTTPS endpoint to receive events
- **webhookEvent**: Required, event type to subscribe to
#### Trigger Source Types
| Type | Description | Example IDs |
|------|-------------|-------------|
| PROJECT_ID | Specific projects | Project IDs |
| USER_ID | Specific users | User IDs |
| TAG_ID | Specific tags | Tag IDs |
| TASK_ID | Specific tasks | Task IDs |
| WORKSPACE_ID | Entire workspace | Workspace ID |
| ASSIGNMENT_ID | Specific assignments | Assignment IDs |
| EXPENSE_ID | Specific expenses | Expense IDs |
#### Response (201 Created)
```json
{
"authToken": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"enabled": true,
"id": "76a687e29ae1f428e7ebe101",
"name": "Project Updates Monitor",
"triggerSource": [
"54a687e29ae1f428e7ebe909"
],
"triggerSourceType": "PROJECT_ID",
"url": "https://example.com/webhooks/clockify",
"userId": "5a0ab5acb07987125438b60f",
"webhookEvent": "NEW_PROJECT",
"workspaceId": "64a687e29ae1f428e7ebe303"
}
```
**Important**: Save the `authToken` - it's only returned once at creation!
---
### 4. Get Webhook by ID
**Method:** `GET`
**Path:** `/v1/workspaces/{workspaceId}/webhooks/{webhookId}`
**Purpose:** Get details of specific webhook
#### Response
Single webhook object (excludes authToken for security)
---
### 5. Update Webhook
**Method:** `PUT`
**Path:** `/v1/workspaces/{workspaceId}/webhooks/{webhookId}`
**Purpose:** Update webhook configuration
#### Request Body
```json
{
"name": "Updated Webhook Name",
"triggerSource": [
"54a687e29ae1f428e7ebe909"
],
"triggerSourceType": "PROJECT_ID",
"url": "https://example.com/webhooks/clockify-updated",
"webhookEvent": "TIME_ENTRY_UPDATED"
}
```
#### Response (200 OK)
Updated webhook object (auth token unchanged)
---
### 6. Delete Webhook
**Method:** `DELETE`
**Path:** `/v1/workspaces/{workspaceId}/webhooks/{webhookId}`
**Purpose:** Remove webhook
#### Response
200 OK (success)
---
### 7. Generate New Token
**Method:** `PATCH`
**Path:** `/v1/workspaces/{workspaceId}/webhooks/{webhookId}/token`
**Purpose:** Regenerate webhook auth token (invalidates old token)
#### Response (200 OK)
```json
{
"authToken": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"enabled": true,
"id": "76a687e29ae1f428e7ebe101",
"name": "Project Updates Monitor",
"triggerSource": ["54a687e29ae1f428e7ebe909"],
"triggerSourceType": "PROJECT_ID",
"url": "https://example.com/webhooks/clockify",
"userId": "5a0ab5acb07987125438b60f",
"webhookEvent": "NEW_PROJECT",
"workspaceId": "64a687e29ae1f428e7ebe303"
}
```
**Use Case**: Token rotation for security
---
### 8. Get Webhook Logs
**Method:** `POST`
**Path:** `/v1/workspaces/{workspaceId}/webhooks/{webhookId}/logs`
**Purpose:** Retrieve webhook delivery logs
#### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| page | integer | No | Page number (default: 0) |
| size | integer | No | Page size (default: 50) |
#### Request Body
```json
{
"from": "2023-02-01T13:00:46Z",
"to": "2023-02-05T13:00:46Z",
"sortByNewest": true,
"status": "FAILED"
}
```
#### Request Fields
- **from**: Optional start datetime (ISO-8601)
- **to**: Optional end datetime (ISO-8601)
- **sortByNewest**: Optional, default true
- **status**: Optional filter: ALL, SUCCEEDED, FAILED
#### Response (200 OK)
```json
[
{
"id": "65e5b854fe0dfa24f1528ef0",
"requestBody": "{\"id\":\"65df50f5d2dd8f23a685374e\",\"name\":\"Webhook\"}",
"respondedAt": "2024-03-04T12:02:28.125+00:00",
"responseBody": "{\"id\":\"h73210f5d2dd8f23685374e\",\"response\":\"Webhook response\"}",
"statusCode": 200,
"webhookId": "65df5508d2dd8f23a68537af"
}
]
```
#### Log Fields
- **requestBody**: JSON payload sent to webhook URL
- **responseBody**: Response from webhook endpoint
- **statusCode**: HTTP status code returned
- **respondedAt**: Timestamp of delivery attempt
---
## Webhook Events
### Time Entry Events
| Event | Description | Triggered When |
|-------|-------------|----------------|
| NEW_TIMER_STARTED | Timer started | User starts new timer |
| TIMER_STOPPED | Timer stopped | Running timer stopped |
| NEW_TIME_ENTRY | Entry created | New time entry added |
| TIME_ENTRY_UPDATED | Entry modified | Time entry updated |
| TIME_ENTRY_DELETED | Entry removed | Time entry deleted |
| TIME_ENTRY_RESTORED | Entry undeleted | Deleted entry restored |
| TIME_ENTRY_SPLIT | Entry split | Time entry split into multiple |
### Project/Task Events
| Event | Description | Triggered When |
|-------|-------------|----------------|
| NEW_PROJECT | Project created | New project added |
| PROJECT_UPDATED | Project modified | Project details changed |
| PROJECT_DELETED | Project removed | Project deleted |
| NEW_TASK | Task created | New task added to project |
| TASK_UPDATED | Task modified | Task details changed |
| TASK_DELETED | Task removed | Task deleted |
### Client/Tag Events
| Event | Description | Triggered When |
|-------|-------------|----------------|
| NEW_CLIENT | Client created | New client added |
| CLIENT_UPDATED | Client modified | Client details changed |
| CLIENT_DELETED | Client removed | Client deleted |
| NEW_TAG | Tag created | New tag added |
| TAG_UPDATED | Tag modified | Tag details changed |
| TAG_DELETED | Tag removed | Tag deleted |
### User Events
| Event | Description | Triggered When |
|-------|-------------|----------------|
| USER_JOINED_WORKSPACE | User added | User invited/joined |
| USER_DELETED_FROM_WORKSPACE | User removed | User removed from workspace |
| USER_ACTIVATED_ON_WORKSPACE | User activated | Inactive user reactivated |
| USER_DEACTIVATED_ON_WORKSPACE | User deactivated | Active user deactivated |
| USER_EMAIL_CHANGED | Email updated | User's email changed |
| USER_UPDATED | User modified | User details changed |
| USERS_INVITED_TO_WORKSPACE | Users invited | Multiple users invited |
| LIMITED_USERS_ADDED_TO_WORKSPACE | Limited users added | Limited license users added |
### User Group Events
| Event | Description | Triggered When |
|-------|-------------|----------------|
| USER_GROUP_CREATED | Group created | New user group added |
| USER_GROUP_UPDATED | Group modified | Group details changed |
| USER_GROUP_DELETED | Group removed | User group deleted |
### Approval Events
| Event | Description | Triggered When |
|-------|-------------|----------------|
| NEW_APPROVAL_REQUEST | Request created | Time entries submitted for approval |
| APPROVAL_REQUEST_STATUS_UPDATED | Status changed | Request approved/rejected/withdrawn |
### Time Off Events
| Event | Description | Triggered When |
|-------|-------------|----------------|
| TIME_OFF_REQUESTED | Request created | Time off requested |
| TIME_OFF_REQUEST_UPDATED | Request modified | Request details changed |
| TIME_OFF_REQUEST_APPROVED | Request approved | Time off approved |
| TIME_OFF_REQUEST_REJECTED | Request denied | Time off rejected |
| TIME_OFF_REQUEST_WITHDRAWN | Request withdrawn | User withdrew request |
| BALANCE_UPDATED | Balance changed | Time off balance updated |
### Expense Events
| Event | Description | Triggered When |
|-------|-------------|----------------|
| EXPENSE_CREATED | Expense added | New expense created |
| EXPENSE_UPDATED | Expense modified | Expense details changed |
| EXPENSE_DELETED | Expense removed | Expense deleted |
| EXPENSE_RESTORED | Expense undeleted | Deleted expense restored |
### Invoice Events
| Event | Description | Triggered When |
|-------|-------------|----------------|
| NEW_INVOICE | Invoice created | New invoice generated |
| INVOICE_UPDATED | Invoice modified | Invoice details changed |
### Scheduling Events
| Event | Description | Triggered When |
|-------|-------------|----------------|
| ASSIGNMENT_CREATED | Assignment added | New schedule assignment |
| ASSIGNMENT_UPDATED | Assignment modified | Assignment details changed |
| ASSIGNMENT_DELETED | Assignment removed | Assignment deleted |
| ASSIGNMENT_PUBLISHED | Assignment published | Schedule published to users |
### Rate Events
| Event | Description | Triggered When |
|-------|-------------|----------------|
| COST_RATE_UPDATED | Cost rate changed | User/project/workspace cost rate updated |
| BILLABLE_RATE_UPDATED | Billable rate changed | Hourly rate updated |
---
## Webhook Payload Structure
### Request Format
Clockify sends POST request to your webhook URL:
```http
POST /your/webhook/endpoint HTTP/1.1
Host: example.com
Content-Type: application/json
X-Clockify-Webhook-Signature: eyJ0eXAiOiJKV1QiLCJhbGc...
{
"id": "time_entry_id",
"userId": "user_id",
"workspaceId": "workspace_id",
"description": "Working on feature",
"projectId": "project_id",
"timeInterval": {
"start": "2024-01-29T10:00:00Z",
"end": "2024-01-29T12:00:00Z",
"duration": "PT2H"
},
"billable": true,
"tags": [...]
}
```
### Payload Contents
Payload varies by event type but typically includes:
- Entity ID(s)
- Relevant entity details
- User who triggered the event
- Workspace context
- Timestamp
---
## Implementation Examples
### Python: Create Webhook
```python
import requests
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_webhook(workspace_id, name, url, event, trigger_source_type, trigger_sources):
"""Create a new webhook"""
endpoint = f"{BASE_URL}/workspaces/{workspace_id}/webhooks"
payload = {
"name": name,
"url": url,
"webhookEvent": event,
"triggerSourceType": trigger_source_type,
"triggerSource": trigger_sources
}
response = requests.post(endpoint, headers=headers, json=payload)
if response.status_code == 201:
webhook = response.json()
# IMPORTANT: Save this token - it's only returned once!
print(f"Webhook created! Auth token: {webhook['authToken']}")
return webhook
else:
raise Exception(f"Failed to create webhook: {response.text}")
# Create webhook for new time entries on specific projects
webhook = create_webhook(
workspace_id="workspace_id",
name="Time Entry Monitor",
url="https://myapp.com/webhooks/clockify/time-entries",
event="NEW_TIME_ENTRY",
trigger_source_type="PROJECT_ID",
trigger_sources=["project1_id", "project2_id"]
)
```
### Python: Flask Webhook Endpoint
```python
from flask import Flask, request, jsonify
import jwt
import json
app = Flask(__name__)
# Store your webhook auth tokens securely
WEBHOOK_TOKENS = {
"webhook1_id": "eyJ0eXAiOiJKV1QiLCJhbGc..."
}
def verify_webhook_signature(webhook_id, signature):
"""Verify webhook request is from Clockify"""
expected_token = WEBHOOK_TOKENS.get(webhook_id)
if not expected_token:
return False
try:
# Verify JWT signature
jwt.decode(signature, options={"verify_signature": False})
return signature == expected_token
except:
return False
@app.route('/webhooks/clockify/time-entries', methods=['POST'])
def handle_time_entry_webhook():
"""Handle time entry webhooks"""
# Get signature from header
signature = request.headers.get('X-Clockify-Webhook-Signature')
# Verify signature (in production, store webhook ID with token)
webhook_id = "webhook1_id" # In practice, identify from URL or database
if not verify_webhook_signature(webhook_id, signature):
return jsonify({"error": "Invalid signature"}), 401
# Process webhook payload
data = request.json
print(f"New time entry: {data.get('description')}")
print(f"Duration: {data.get('timeInterval', {}).get('duration')}")
print(f"User: {data.get('userId')}")
# Your business logic here
# - Update external database
# - Send notifications
# - Trigger other workflows
return jsonify({"status": "success"}), 200
if __name__ == '__main__':
app.run(port=5000)
```
### Python: Webhook Log Analysis
```python
from datetime import datetime, timedelta
def get_webhook_logs(workspace_id, webhook_id, days_back=7):
"""Get recent webhook logs"""
url = f"{BASE_URL}/workspaces/{workspace_id}/webhooks/{webhook_id}/logs"
end_date = datetime.utcnow()
start_date = end_date - timedelta(days=days_back)
payload = {
"from": start_date.isoformat() + "Z",
"to": end_date.isoformat() + "Z",
"sortByNewest": True,
"status": "ALL"
}
response = requests.post(url, headers=headers, json=payload, params={"size": 1000})
return response.json()
def analyze_webhook_health(logs):
"""Analyze webhook delivery success rate"""
total = len(logs)
if total == 0:
return {"success_rate": 0, "total": 0}
succeeded = sum(1 for log in logs if log['statusCode'] == 200)
failed = total - succeeded
# Group failures by status code
failure_codes = {}
for log in logs:
if log['statusCode'] != 200:
code = log['statusCode']
failure_codes[code] = failure_codes.get(code, 0) + 1
return {
"total": total,
"succeeded": succeeded,
"failed": failed,
"success_rate": (succeeded / total * 100),
"failure_codes": failure_codes
}
# Analyze webhook health
logs = get_webhook_logs("workspace_id", "webhook_id")
health = analyze_webhook_health(logs)
print(f"Total deliveries: {health['total']}")
print(f"Success rate: {health['success_rate']:.1f}%")
print(f"Failed: {health['failed']}")
if health['failure_codes']:
print("Failure breakdown:")
for code, count in health['failure_codes'].items():
print(f" {code}: {count} times")
```
### Python: Multi-Event Webhook Setup
```python
def setup_project_monitoring(workspace_id, project_ids, webhook_base_url):
"""Set up comprehensive project monitoring"""
events_to_monitor = [
("NEW_TIME_ENTRY", "time-entries"),
("TIME_ENTRY_UPDATED", "time-entries"),
("TIME_ENTRY_DELETED", "time-entries"),
("NEW_TASK", "tasks"),
("TASK_UPDATED", "tasks"),
("PROJECT_UPDATED", "projects")
]
created_webhooks = []
for event, endpoint in events_to_monitor:
try:
webhook = create_webhook(
workspace_id=workspace_id,
name=f"Project Monitor - {event}",
url=f"{webhook_base_url}/{endpoint}",
event=event,
trigger_source_type="PROJECT_ID",
trigger_sources=project_ids
)
created_webhooks.append(webhook)
print(f"Created webhook for {event}")
except Exception as e:
print(f"Failed to create webhook for {event}: {e}")
return created_webhooks
# Set up monitoring for multiple projects
webhooks = setup_project_monitoring(
"workspace_id",
["project1_id", "project2_id"],
"https://myapp.com/webhooks/clockify"
)
```
### Python: Workspace-Wide Event Monitor
```python
def create_workspace_monitor(workspace_id, webhook_url):
"""Monitor all time entry events across entire workspace"""
webhook = create_webhook(
workspace_id=workspace_id,
name="Workspace Time Entry Monitor",
url=webhook_url,
event="NEW_TIME_ENTRY",
trigger_source_type="WORKSPACE_ID",
trigger_sources=[workspace_id] # Entire workspace
)
return webhook
# Monitor entire workspace
monitor = create_workspace_monitor(
"workspace_id",
"https://myapp.com/webhooks/all-time-entries"
)
```
---
## Best Practices
1. **Signature Verification**: Always verify `X-Clockify-Webhook-Signature` header
2. **Idempotency**: Handle duplicate webhook deliveries gracefully
3. **Fast Response**: Return 200 OK quickly, process asynchronously
4. **Error Handling**: Return appropriate HTTP status codes
5. **Logging**: Log all webhook receipts for debugging
6. **Token Security**: Store auth tokens securely (environment variables, secrets manager)
7. **Token Rotation**: Periodically regenerate tokens
8. **Retry Logic**: Implement exponential backoff for processing failures
9. **Filtering**: Use specific trigger sources to reduce unnecessary events
10. **Monitoring**: Track webhook delivery success rates
---
## Security Considerations
### Token Verification
```python
import jwt
def verify_clockify_webhook(signature, expected_token):
"""Securely verify webhook signature"""
try:
# Decode JWT without verification (Clockify doesn't sign with shared secret)
payload = jwt.decode(signature, options={"verify_signature": False})
# Compare with expected token
return signature == expected_token
except jwt.DecodeError:
return False
```
### HTTPS Requirement
- Webhook URLs must use HTTPS
- HTTP URLs are rejected
### IP Whitelisting
Consider whitelisting Clockify's IP ranges if needed
---
## Troubleshooting
### Common Issues
**Webhooks Not Firing**
- Check webhook is enabled
- Verify trigger source IDs are correct
- Check event type matches action
- Review webhook logs
**401/403 Errors**
- Verify signature validation logic
- Check token hasn't been rotated
- Ensure HTTPS endpoint
**Timeouts**
- Process webhooks asynchronously
- Return 200 OK within 10 seconds
- Use message queue for heavy processing
**Duplicate Events**
- Implement idempotency keys
- Track processed webhook IDs
- Use database transactions
---
## Rate Limiting
- Addon token: 50 requests/second per workspace
- Webhook deliveries don't count toward rate limit
---
## Notes
- Auth token shown only at creation/regeneration
- Webhooks remain enabled even if deliveries fail
- Failed deliveries are retried with exponential backoff
- Maximum 100 webhooks per workspace
- Logs retained for 30 days
- Payload size typically < 10KB