The webhook system enables integrations with external systems by sending HTTP requests when specific events occur in TimeTracker.
Webhooks allow you to:
- Receive real-time notifications when events occur (project created, task completed, etc.)
- Integrate with external services (Slack, Discord, custom APIs, etc.)
- Automate workflows based on TimeTracker events
- Build custom integrations without polling the API
- Event Subscriptions: Subscribe to specific events or all events using wildcards
- Secure Signatures: HMAC-SHA256 signatures for webhook verification
- Automatic Retries: Failed deliveries are automatically retried with exponential backoff
- Delivery Tracking: View delivery history, success rates, and error details
- Customizable: Configure HTTP method, headers, timeouts, and retry behavior
- REST API: Full CRUD API for managing webhooks programmatically
project.created- A project is createdproject.updated- A project is updatedproject.deleted- A project is deletedproject.archived- A project is archivedproject.unarchived- A project is unarchived
task.created- A task is createdtask.updated- A task is updatedtask.deleted- A task is deletedtask.completed- A task is completedtask.assigned- A task is assigned to a usertask.status_changed- A task's status changes
time_entry.created- A time entry is createdtime_entry.updated- A time entry is updatedtime_entry.deleted- A time entry is deletedtime_entry.started- A timer is startedtime_entry.stopped- A timer is stopped
invoice.created- An invoice is createdinvoice.updated- An invoice is updatedinvoice.deleted- An invoice is deletedinvoice.sent- An invoice is sent to a clientinvoice.paid- An invoice is paidinvoice.overdue- An invoice becomes overdue
client.created- A client is createdclient.updated- A client is updatedclient.deleted- A client is deleted
user.created- A user is createduser.updated- A user is updateduser.deleted- A user is deleted
comment.created- A comment is createdcomment.updated- A comment is updatedcomment.deleted- A comment is deleted
*- Subscribe to all events
All webhook payloads follow this structure:
{
"event_type": "project.created",
"timestamp": "2025-01-23T10:30:00Z",
"user": {
"id": 1,
"username": "john",
"display_name": "John Doe"
},
"entity": {
"type": "project",
"id": 123,
"name": "My Project"
},
"action": "created",
"description": "Created project \"My Project\"",
"data": {
// Additional event-specific data
}
}Each webhook includes an HMAC-SHA256 signature in the X-Webhook-Signature header:
X-Webhook-Signature: sha256=<signature>
To verify the signature:
import hmac
import hashlib
def verify_webhook_signature(payload, signature, secret):
expected_signature = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Remove 'sha256=' prefix if present
if signature.startswith('sha256='):
signature = signature[7:]
return hmac.compare_digest(expected_signature, signature)Each webhook request includes these headers:
Content-Type: The configured content type (default:application/json)User-Agent:TimeTracker-Webhook/1.0X-Webhook-Event: The event type (e.g.,project.created)X-Webhook-ID: The webhook IDX-Webhook-Signature: HMAC signature (if secret is configured)
- Navigate to Admin β Webhooks
- Click Create Webhook
- Configure:
- Name: Descriptive name for the webhook
- URL: Endpoint URL to receive webhooks
- Events: Select which events to subscribe to
- HTTP Method: POST, PUT, or PATCH
- Retry Settings: Max retries, delay, timeout
- Save the webhook
The webhook secret will be generated automatically. Save this secret - it's only shown once!
- Max Retries: Number of retry attempts (default: 3)
- Retry Delay: Seconds between retries (default: 60, uses exponential backoff)
- Timeout: Request timeout in seconds (default: 30)
- Active: Enable/disable the webhook
- pending: Initial delivery attempt
- success: Successfully delivered (HTTP 2xx)
- failed: Delivery failed (exceeded max retries)
- retrying: Scheduled for retry
Failed deliveries are automatically retried with exponential backoff:
- 1st retry: After
retry_delay_seconds - 2nd retry: After
retry_delay_seconds * 2 - 3rd retry: After
retry_delay_seconds * 4 - etc.
The retry task runs every 5 minutes.
GET /api/v1/webhooks
Authorization: Bearer <token>POST /api/v1/webhooks
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "My Webhook",
"url": "https://example.com/webhook",
"events": ["project.created", "task.completed"],
"max_retries": 3,
"retry_delay_seconds": 60,
"timeout_seconds": 30
}GET /api/v1/webhooks/<webhook_id>
Authorization: Bearer <token>PATCH /api/v1/webhooks/<webhook_id>
Authorization: Bearer <token>
Content-Type: application/json
{
"is_active": false,
"events": ["project.created"]
}DELETE /api/v1/webhooks/<webhook_id>
Authorization: Bearer <token>GET /api/v1/webhooks/<webhook_id>/deliveries?status=failed
Authorization: Bearer <token>GET /api/v1/webhooks/events
Authorization: Bearer <token>Webhook API endpoints require these scopes:
read:webhooks- View webhooks and deliverieswrite:webhooks- Create, update, delete webhooks
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = 'your-webhook-secret';
app.use(express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const eventType = req.headers['x-webhook-event'];
// Verify signature
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const providedSignature = signature.replace('sha256=', '');
if (expectedSignature !== providedSignature) {
return res.status(401).send('Invalid signature');
}
// Parse payload
const payload = JSON.parse(req.body);
// Handle event
console.log(`Received ${eventType}:`, payload);
// Process based on event type
switch (eventType) {
case 'project.created':
console.log('New project:', payload.entity.name);
break;
case 'task.completed':
console.log('Task completed:', payload.entity.name);
break;
}
res.status(200).send('OK');
});
app.listen(3000);import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = 'your-webhook-secret'
def verify_signature(payload, signature):
expected = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
if signature.startswith('sha256='):
signature = signature[7:]
return hmac.compare_digest(expected, signature)
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Webhook-Signature')
event_type = request.headers.get('X-Webhook-Event')
if not verify_signature(request.data.decode('utf-8'), signature):
return jsonify({'error': 'Invalid signature'}), 401
payload = request.get_json()
# Handle event
print(f"Received {event_type}: {payload}")
return jsonify({'status': 'ok'}), 200- Always verify signatures - Never trust webhook payloads without verification
- Handle idempotency - Use
event_idto prevent duplicate processing - Respond quickly - Return HTTP 200 quickly, process asynchronously if needed
- Monitor deliveries - Check delivery status regularly
- Use HTTPS - Always use HTTPS endpoints for webhooks
- Test webhooks - Use the test feature before going live
- Set appropriate timeouts - Match your endpoint's processing time
- Handle errors gracefully - Return appropriate HTTP status codes
- Check if webhook is active
- Verify event subscription
- Check delivery logs for errors
- Verify endpoint URL is accessible
- Check endpoint returns HTTP 2xx
- Review error messages in delivery logs
- Ensure endpoint responds within timeout
- Verify secret matches webhook secret
- Check payload encoding (UTF-8)
- Ensure signature header format is correct
- Compare signatures byte-by-byte (use
hmac.compare_digest)
- Maximum payload size: 10MB
- Maximum retries: 10
- Retry task runs every 5 minutes
- Webhooks are delivered synchronously (may affect response time for triggering actions)