Three new opt-in integration connectors that plug into the existing
`app/integrations/base.py:BaseConnector` pattern and the integrations
settings UI. Each connector subclasses `BaseConnector`, persists its
state inside the existing `Integration.config` JSONB (no new tables),
encrypts every secret at rest via `app/utils/secret_crypto`, and
degrades gracefully when the integration row is missing or
`is_active=False` -- every method returns
`{"ok": false, "error": "Integration not configured"}` without
raising, so the timer, exports, and dashboards keep working when a
connector is disabled or broken.
All third-party HTTP calls go through `requests` with a 10-second
timeout and a `try/except requests.RequestException`. Tokens are
never written to logs in their raw form -- only short
`xoxb-...` / `ghp_...` truncations.
GitHub connector (`app/integrations/github_connector.py`, provider
key `github_connector`):
- Webhook receiver at `POST /api/integrations/github/webhook`
verifies `X-Hub-Signature-256` with HMAC-SHA256 against the
per-integration webhook secret before reading the payload.
- Handles `issues.opened` (creates a task with
`external_ref="github_issue_{n}"`, mapped priority and `todo`
status), `issues.assigned` (optionally starts a timer for the
linked TimeTracker user when `users.github_username` matches),
`issues.closed` (marks the existing task `done`), and `ping`.
- Manual sync (`POST /api/integrations/github/sync`, admin only)
pulls open issues from
`GET /repos/{owner}/{repo}/issues?state=open&per_page=50` and
upserts tasks by `external_ref`. Optional `label_filter`.
Google Calendar connector (`app/integrations/google_calendar_connector.py`,
provider key `google_calendar_connector`):
- OAuth2 flow at `/integrations/google/{connect,callback,disconnect}`
using raw `requests` against
`https://oauth2.googleapis.com/token`. Tokens (`access_token`,
`refresh_token`, `token_expiry`) are stored encrypted in
`Integration.config`. `client_id`/`client_secret` come from
Flask config (`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`) and are
never hardcoded.
- `_refresh_token_if_needed()` refreshes within 5 minutes of expiry
on every API call.
- `sync()` supports `import` / `export` / `both`:
* import: pulls dated events from the configured `calendar_id`
over the last `sync_days_back` days (clamped 1-30), skips
all-day events and anything tagged `[TT]` or already linked via
`gcal:{event_id}` in the notes of an existing `TimeEntry`.
* export: posts completed entries created since `last_sync_at`
back to Google as `[TT] {project} -- {task or notes}` events
with `timeZone: "UTC"`.
- `revoke()` calls `https://oauth2.googleapis.com/revoke` and
wipes the stored tokens.
- APScheduler job `google_calendar_sync` runs every 30 minutes;
each user is wrapped in `try/except` so one broken token cannot
block the rest.
Slack connector (`app/integrations/slack_connector.py`, provider key
`slack_connector`):
- Webhook receiver at `POST /api/integrations/slack/events`
verifies `X-Slack-Signature` (HMAC-SHA256 of
`v0:{timestamp}:{body}`) and rejects requests older than 5
minutes. Replies to Slack's URL verification handshake
immediately.
- Slash command `/tt` supports `start [project]` (id or
case-insensitive partial name match against the user's allowed
projects), `stop`, `status`, `today` (via
`notification_service.get_today_summary_for_user`), and an
in-place help text fallback. Every reply is ephemeral JSON so it
fits inside Slack's 3-second budget without touching
`response_url`.
- `notify_timer_started` / `notify_timer_stopped` post a
stopwatch/checkmark message to the configured channel. Wired
into both the page route (`app/routes/timer.py`) and the JSON
API (`app/routes/api.py`) as a fire-and-forget hook: the import
+ call are wrapped in `try/except` and only log at `DEBUG` on
failure, so Slack outages can't slow down the timer flow.
- `post_daily_summary` posts a daily roll-up; APScheduler job
`slack_daily_summary` runs every 30 minutes and matches each
user's configured `HH:MM` against the window.
Plumbing and storage:
- New blueprint `app/routes/integrations_webhooks.py` registers
the webhook receivers (`csrf.exempt`, signature-verified) plus a
uniform `config`/`status`/`test` API surface
(`/api/integrations/{github,google,slack}/{config,status,test}`)
used by the settings UI. Optional-registered in
`app/blueprint_registry.py`.
- Alembic revision `155_add_integration_columns`:
* `users.github_username` (String(100), nullable) - GitHub login
join key for the assignment auto-start-timer flow.
* `tasks.external_ref` (String(200), nullable, indexed) -
canonical external id for connector-created tasks; the new
index lets webhook receivers de-duplicate cheaply.
Both columns are added defensively (inspector-checked) so the
migration is safe to re-run.
- New cards in `app/templates/integrations/_connector_cards.html`
(included by `templates/integrations/list.html`) drive the
Personal connectors UI -- Tailwind CSS only, vanilla JS, per-card
status fetch, save, test, and sync actions.
Documentation:
- `docs/integrations/README.md` indexes all built-in connectors.
- `docs/integrations/GITHUB_CONNECTOR.md`,
`docs/integrations/GOOGLE_CALENDAR.md`, and
`docs/integrations/SLACK.md` cover setup, OAuth/webhook wiring,
config fields, endpoints, and operational notes for each
connector.
- `docs/api/REST_API.md` lists the new endpoints under a new
"Personal integration connectors" subsection.
- `CHANGELOG.md` notes the feature under the [Unreleased] section.
`LLMService`, `TimeTrackingService`, `ForecastService`, and the
`Integration` model schema are intentionally untouched -- only
`users` and `tasks` gain columns via migration.
33 KiB
TimeTracker REST API Documentation
Overview
The TimeTracker REST API provides programmatic access to all time tracking, project management, and reporting features. This API is designed for developers who want to integrate TimeTracker with other tools or build custom applications.
Integrations should use /api/v1 only (this document). The web application also exposes same-origin session JSON under /api/* (for example search and timer helpers used by the browser). Those routes are not the stable integration surface; use tokens and /api/v1 for scripts, mobile, and desktop clients.
For maintainers
Ship new HTTP capabilities under /api/v1 first, with OpenAPI updates in app/routes/api_docs.py. Add or change /api/* only for logged-in UI needs or short-lived shims; reuse services from app/services/ rather than duplicating logic.
Base URL
https://your-domain.com/api/v1
Authentication
All API endpoints require authentication using API tokens. API tokens are managed by administrators through the admin dashboard.
Creating API Tokens
-
Log in as an administrator
-
Navigate to Admin > Security & Access > Api-tokens (
/admin/api-tokens) -
Click Create Token
-
Fill in the required information:
- Name: A descriptive name for the token
- Description: Optional description
- User: The user this token will authenticate as
- Scopes: Select the permissions this token should have
- Expires In: Optional expiration period in days
-
Click Create Token
-
Important: Copy the generated token immediately - you won't be able to see it again!
Using API Tokens
Include your API token in every request using one of these methods:
Method 1: Bearer Token (Recommended)
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://your-domain.com/api/v1/projects
Method 2: API Key Header
curl -H "X-API-Key: YOUR_API_TOKEN" \
https://your-domain.com/api/v1/projects
Token Format
API tokens follow the format: tt_<32_random_characters>
Example: tt_abc123def456ghi789jkl012mno345pq
Rate limiting
Authenticated API requests are counted per API token using sliding minute and hour windows. Defaults are 100 requests/minute and 1000/hour unless overridden in configuration.
API_TOKEN_RATE_LIMIT_PER_MINUTE— max requests per token per minute (default100).API_TOKEN_RATE_LIMIT_PER_HOUR— max requests per token per hour (default1000).
When Redis is available (REDIS_URL and Redis enabled in app config), limits are shared across all app workers. Otherwise a process-local fallback is used (fine for single-worker development; use Redis in production with multiple workers).
Idempotent time entry creation
For safe retries (mobile offline sync, webhooks, automation), send a unique Idempotency-Key header (max 128 characters) on POST /api/v1/time-entries. The server stores the response for that key for 24 hours (per token). Repeating the same key returns the same JSON body and HTTP status without creating a duplicate entry.
Scopes
API tokens use scopes to control access to resources. When creating a token, select the appropriate scopes:
| Scope | Description |
|---|---|
read:projects |
View projects |
write:projects |
Create and update projects |
read:time_entries |
View time entries |
write:time_entries |
Create and update time entries |
read:tasks |
View tasks |
write:tasks |
Create and update tasks |
read:clients |
View clients |
write:clients |
Create and update clients |
read:quotes |
View quotes |
write:quotes |
Create and update quotes |
read:reports |
View reports and analytics |
read:users |
View user information |
admin:all |
Full administrative access (use with caution) |
Note: For most integrations, you'll want both read and write scopes for the resources you're working with.
Pagination
List endpoints support pagination to handle large datasets efficiently. For performance and benchmark targets, see PERFORMANCE.md.
Query Parameters
page- Page number (default: 1)per_page- Items per page (default: 50, max: 100)
Response Format
List responses use a resource-named key (e.g. time_entries, projects, clients) plus a top-level pagination object:
{
"time_entries": [...],
"pagination": {
"page": 1,
"per_page": 50,
"total": 150,
"pages": 3,
"has_next": true,
"has_prev": false,
"next_page": 2,
"prev_page": null
}
}
Date/Time Format
All timestamps use ISO 8601 format:
- Date:
YYYY-MM-DD(e.g.,2024-01-15) - DateTime:
YYYY-MM-DDTHH:MM:SSorYYYY-MM-DDTHH:MM:SSZ(e.g.,2024-01-15T14:30:00Z)
Error Handling
HTTP Status Codes
200 OK- Request successful201 Created- Resource created successfully400 Bad Request- Invalid input401 Unauthorized- Authentication required or invalid token403 Forbidden- Insufficient permissions (scope issue)404 Not Found- Resource not found500 Internal Server Error- Server error
Error Response Format
All error responses (4xx/5xx) include at least error (user-facing message) and message. Optional error_code (e.g. unauthorized, forbidden, not_found, validation_error) allows machine-readable handling. Validation errors include an errors object with field-level messages.
Example (401):
{
"error": "Invalid token",
"message": "The provided API token is invalid or expired",
"error_code": "unauthorized"
}
For scope errors (403):
{
"error": "Insufficient permissions",
"message": "This endpoint requires the 'write:projects' scope",
"error_code": "forbidden",
"required_scope": "write:projects",
"available_scopes": ["read:projects", "read:time_entries"]
}
For validation errors (400):
{
"error": "Validation failed",
"message": "Validation failed",
"error_code": "validation_error",
"errors": { "name": ["Name is required"], "project_id": ["project_id is required"] }
}
API Endpoints
System
Get API Information
GET /api/v1/info
Returns API version and available endpoints. No authentication required.
setup_required is a boolean: when true, the installation’s initial web setup is not complete; finish setup in the browser. Desktop and mobile apps use this (and JSON shape) to avoid treating arbitrary HTTP 200 pages as TimeTracker. During that phase, GET /api/v1/info, GET /api/v1/health, and POST /api/v1/auth/login are not redirected to the HTML setup wizard so clients still receive JSON.
Response:
{
"api_version": "v1",
"app_version": "1.0.0",
"setup_required": false,
"documentation_url": "/api/docs",
"endpoints": {
"projects": "/api/v1/projects",
"time_entries": "/api/v1/time-entries",
"tasks": "/api/v1/tasks",
"clients": "/api/v1/clients"
}
}
Health Check
GET /api/v1/health
Check if the API is operational. No authentication required.
Admin version check (web JSON under /api)
These routes live on the legacy session JSON blueprint (same prefix style as /api/health in the app). They are admin-only (User.is_admin, including RBAC admin roles).
Authentication: browser session cookie (same-origin fetch after login) or an API token (Authorization: Bearer tt_… or X-API-Key). No dedicated scope is required; the server checks that the authenticated user is an administrator.
Check installed version against latest GitHub release
GET /api/version/check
Compares the running instance version to the latest published release on GitHub (see Version management — admin update notification for configuration and caching). Returns update_available: false when the current install is not a comparable semantic version (for example some dev-* tags), when GitHub cannot be reached and no stale cache exists, or when the user has dismissed this release version.
Responses: 401 if unauthenticated, 403 if not admin.
Example (Bearer):
curl -s -H "Authorization: Bearer YOUR_API_TOKEN" \
https://your-domain.com/api/version/check
Response (200):
{
"update_available": true,
"current_version": "4.0.0",
"latest_version": "4.1.0",
"release_notes": "…",
"published_at": "2026-04-01T10:00:00Z",
"release_url": "https://github.com/DRYTRIX/TimeTracker/releases/tag/v4.1.0"
}
Dismiss update prompt for a release version
POST /api/version/dismiss
Body (JSON): { "latest_version": "4.1.0" } (with or without a leading v; stored normalized).
Persists dismissed_release_version for the current user so GET /api/version/check returns update_available: false until a newer release appears. Returns { "ok": true } on success, 400 if latest_version is missing or not a valid semantic version string.
The web UI also mirrors dismissal in localStorage (tt_dismissed_release_version) as a client-side fallback; the database remains authoritative for update_available.
Dashboard productivity (web JSON under /api)
These routes are used by the web dashboard after login. They live on the same legacy JSON blueprint as /api/health (not under /api/v1). Authentication: browser session cookie (@login_required); unauthenticated requests receive 401.
Value dashboard aggregates
GET /api/stats/value-dashboard
Returns productivity aggregates for the current user only (completed time entries: end_time is set). Used by the main dashboard “Value insights” widget.
Caching: responses may be cached for up to 10 minutes per user when Redis is available (REDIS_URL and working connection). If Redis is unavailable, each request recomputes from the database.
Response (200):
{
"total_hours": 132.5,
"entries_count": 248,
"active_days": 18,
"avg_session_length": 1.4,
"most_productive_day": "Tuesday",
"this_week_hours": 24.5,
"this_month_hours": 110.2,
"last_7_days": [
{ "date": "2026-04-09", "hours": 2.5 },
{ "date": "2026-04-10", "hours": 0.0 }
],
"estimated_value_tracked": 1234.56,
"estimated_value_currency": "EUR"
}
most_productive_day: English weekday name (Sunday–Saturday) with the highest total tracked time across all history, ornullwhen there is no qualifying data.last_7_days: seven objects in chronological order for the last seven local calendar days (app timezone), including days with 0 hours.estimated_value_tracked:nullwhen the estimated billable total is zero or no rate applies; otherwisehours ×resolved rate usingCOALESCE(project.hourly_rate, entry client default, project client default)(see server implementation inStatsService).estimated_value_currencycomes from Settings → currency with application default fallback.
This week vs last week (hours by day)
GET /api/reports/week-comparison
Returns a partial calendar week (Monday 00:00 local time through now) compared with the same weekdays in the previous week, for the current user only. Completed entries only (end_time is set). The week definition matches the main dashboard “Week’s hours” aggregate (AnalyticsService.get_dashboard_stats).
Response (200):
{
"current_week": {
"total_hours": 18.5,
"by_day": [
{ "day": "2026-04-20", "hours": 6.0 },
{ "day": "2026-04-21", "hours": 4.25 }
]
},
"last_week": {
"total_hours": 22.0,
"by_day": [
{ "day": "2026-04-13", "hours": 5.0 },
{ "day": "2026-04-14", "hours": 7.5 }
]
},
"change_percent": -15.9
}
by_day: dense list from week Monday through the comparison end date (this week through today; last week through the parallel weekday). Eachdayis an ISO dateYYYY-MM-DD;hoursis a float (including0.0for days with no entries).change_percent: percent change ofcurrent_week.total_hoursvslast_week.total_hours, rounded to one decimal.nullwhen last week’s total is zero (avoid division by zero).
The main dashboard renders this as a grouped bar chart (Chart.js) with a short summary line; data is loaded by app/static/dashboard-enhancements.js on dashboard refresh.
Personal productivity stats
GET /api/productivity/stats
Returns streaks, focus metrics, daily breakdown, project mix, activity heatmap, and insight strings for the current user only. Powers the My productivity page (/dashboard/productivity). See docs/features/PRODUCTIVITY_DASHBOARD.md.
Query parameters:
period(optional, int) — days for focus and project breakdown (default 30, max 90). Daily breakdown and heatmap use fixed windows (14 and 84 days).
Caching: up to 5 minutes per user and period via app.utils.cache.get_cache() when available. Skipped when the user has an active timer so today/week stats stay current.
Response (200): { "ok": true, "period": 30, "summary": {...}, "daily_breakdown": [...], "streak": {...}, "focus": {...}, "projects": [...], "heatmap": [...], "insights": [...] }
Errors: { "ok": false, "error": "..." } with HTTP 500 on server failure.
Project forecast (web JSON)
GET /api/projects/<project_id>/forecast
Deterministic velocity, budget, timeline, and task metrics for a project the user can access. Optional AI narrative when the AI helper is enabled.
Query parameters:
ai— when truthy (1,true,yes,on), include LLM-generated narrative, risks, and recommendations (requires AI helper enabled).refresh— when truthy, bypass the in-process cache for this project.
Caching: in-memory per process, 10 minutes per project and mode (deterministic vs ai), unless refresh=true.
Response (200): { "ok": true, "forecast": {...}, "ai": {...} } when ai=true (ai may report ok: false with error_code while still returning forecast).
See docs/BUDGET_ALERTS_AND_FORECASTING.md.
AI time entry suggestions (web JSON)
GET /api/ai/suggest
Returns up to five suggested time entries (recent project/task pairs with last notes/tags). Deterministic suggestions always; when rich=1 and the AI helper is enabled, may merge LLM suggestions.
Query parameters:
q(optional) — filter by project name or notes substring.rich(optional) — request LLM-enhanced suggestions when AI is enabled.
Response (200): { "ok": true, "suggestions": [{ "project_id", "project_name", "task_id", "task_name", "notes", "tags", "billable", "confidence", "source" }] }
Used by the Start Timer modal and manual entry Autofill when ai_enabled is true in the template context.
Search
Global Search
GET /api/v1/search
Perform a global search across projects, tasks, clients, and time entries.
Required Scope: read:projects
Query Parameters:
q(required) - Search query (minimum 2 characters)limit(optional) - Maximum number of results per category (default: 10, max: 50)types(optional) - Comma-separated list of types to search:project,task,client,entry
Example:
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://your-domain.com/api/v1/search?q=website&limit=10"
Search by specific types:
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://your-domain.com/api/v1/search?q=website&types=project,task"
Response (200):
{
"results": [
{
"type": "project",
"category": "project",
"id": 1,
"title": "Website Redesign",
"description": "Complete website overhaul",
"url": "/projects/1",
"badge": "Project"
},
{
"type": "task",
"category": "task",
"id": 5,
"title": "Update homepage",
"description": "Website Redesign",
"url": "/tasks/5",
"badge": "In Progress"
}
],
"query": "website",
"count": 2,
"partial": false,
"errors": {}
}
Partial results and per-domain errors
Search runs independently for projects, tasks, clients, and time entries (see app/services/global_search_service.py). If one domain hits a database error (SQLAlchemyError), that domain is skipped, the others still return hits, and the response includes:
partial:truewhen any domain failed; otherwisefalse.errors: Object whose keys areprojects,tasks,clients, orentries(only keys for failed domains are present), each mapping to a short error string. Intended for observability and UI messaging, not as a stable API error code.
Search Behavior:
- Projects: Searches in name and description (active projects only)
- Tasks: Searches in name and description (tasks from active projects only)
- Clients: Searches in name, email, and company
- Time Entries: Searches in notes and tags (non-admin users see only their own entries)
Error Responses:
400 Bad Request- Query is too short (less than 2 characters). Body includeserror,results(empty array),partial: false, anderrors: {}.401 Unauthorized- Missing or invalid API token403 Forbidden- Token lacksread:projectsscope
Note: The legacy endpoint GET /api/search (session cookie, Flask-Login) uses the same search logic and the same results / query / count / partial / errors shape. For queries shorter than two characters it returns 200 with empty results and partial: false. Overlapping session routes may return X-API-Deprecated: true and a Link header pointing at this v1 path; integrations should call GET /api/v1/search only.
Projects
List Projects
GET /api/v1/projects
Required Scope: read:projects
Query Parameters:
status- Filter by status (active,archived,on_hold)client_id- Filter by client IDpage- Page numberper_page- Items per page
Example:
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://your-domain.com/api/v1/projects?status=active&per_page=20"
Response:
{
"projects": [
{
"id": 1,
"name": "Website Redesign",
"description": "Complete website overhaul",
"client_id": 5,
"hourly_rate": 75.00,
"estimated_hours": 120,
"status": "active",
"created_at": "2024-01-01T10:00:00Z"
}
],
"pagination": {...}
}
Get Project
GET /api/v1/projects/{project_id}
Required Scope: read:projects
Create Project
POST /api/v1/projects
Required Scope: write:projects
Request Body:
{
"name": "New Project",
"description": "Project description",
"client_id": 5,
"hourly_rate": 75.00,
"estimated_hours": 100,
"status": "active"
}
Update Project
PUT /api/v1/projects/{project_id}
Required Scope: write:projects
Archive Project
DELETE /api/v1/projects/{project_id}
Required Scope: write:projects
Note: This archives the project rather than permanently deleting it.
Time Entries
List Time Entries
GET /api/v1/time-entries
Required Scope: read:time_entries
Query Parameters:
project_id- Filter by projectuser_id- Filter by user (admin only)start_date- Filter by start date (ISO format)end_date- Filter by end date (ISO format)billable- Filter by billable status (trueorfalse)include_active- Include active timers (trueorfalse)page- Page numberper_page- Items per page
Example:
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://your-domain.com/api/v1/time-entries?project_id=1&start_date=2024-01-01"
Create Time Entry
POST /api/v1/time-entries
Required Scope: write:time_entries
Request Body:
{
"project_id": 1,
"task_id": 5,
"start_time": "2024-01-15T09:00:00Z",
"end_time": "2024-01-15T17:00:00Z",
"notes": "Worked on feature X",
"tags": "development,frontend",
"billable": true
}
Note: end_time is optional. Omit it to create an active timer.
Optional header: Idempotency-Key — see Idempotent time entry creation above.
Import time entries (CSV)
POST /api/v1/time-entries/import-csv
Required Scope: write:time_entries
Accepts a CSV file (same column expectations as the web Import/Export flow) either as:
- Multipart form: field name
file, or - JSON body:
{ "csv": "..." }or{ "data": "..." }, or - Raw body: CSV text with
Content-Type: text/csv(or similar).
Returns a JSON summary (counts, errors) and an appropriate HTTP status.
Bulk actions on time entries
POST /api/v1/time-entries/bulk
Required Scope: write:time_entries
Request body (JSON):
{
"entry_ids": [1, 2, 3],
"action": "delete",
"value": null
}
action (required): one of delete, set_billable, set_paid, add_tag, remove_tag.
value: required for tag actions (string tag); for set_billable / set_paid, pass a boolean.
Active (running) entries are skipped for non-delete actions; delete skips active entries.
Same access rules as the web UI: non-admins may only affect their own entries.
Update Time Entry
PUT /api/v1/time-entries/{entry_id}
Required Scope: write:time_entries
Delete Time Entry
DELETE /api/v1/time-entries/{entry_id}
Required Scope: write:time_entries
Timer Control
Get Timer Status
GET /api/v1/timer/status
Required Scope: read:time_entries
Returns the current active timer for the authenticated user.
Start Timer
POST /api/v1/timer/start
Required Scope: write:time_entries
Request Body:
{
"project_id": 1,
"task_id": 5
}
Responses:
201 Created— Timer started; JSON includesmessageandtimer(time entry fields).409 Conflict— Allow only one active timer per user is enabled in System Settings (single_active_timer) and the user already has a running timer. Response uses the standard error shape witherror_codeset totimer_already_running.400 Bad Request— Validation or other errors (e.g. invalid project, inactive project).
Enforcement uses the persisted Settings row, not the SINGLE_ACTIVE_TIMER env var alone (the env var seeds the setting on first install).
Stop Timer
POST /api/v1/timer/stop
Required Scope: write:time_entries
Stops the active timer for the authenticated user.
Tasks
List Tasks
GET /api/v1/tasks
Required Scope: read:tasks
Query Parameters:
project_id- Filter by projectstatus- Filter by statuspage- Page numberper_page- Items per page
Create Task
POST /api/v1/tasks
Required Scope: write:tasks
Request Body:
{
"name": "Implement login feature",
"description": "Add user authentication",
"project_id": 1,
"status": "todo",
"priority": 1
}
Clients
List Clients
GET /api/v1/clients
Required Scope: read:clients
Create Client
POST /api/v1/clients
Required Scope: write:clients
Request Body:
{
"name": "Acme Corp",
"email": "contact@acme.com",
"company": "Acme Corporation",
"phone": "+1-555-0123"
}
Quotes
List Quotes
GET /api/v1/quotes
Required Scope: read:quotes
Get Quote
GET /api/v1/quotes/{quote_id}
Required Scope: read:quotes
Create Quote
POST /api/v1/quotes
Required Scope: write:quotes
Request Body (example):
{
"client_id": 1,
"title": "Website maintenance retainer",
"description": "Monthly maintenance and support",
"tax_rate": 21.0,
"currency_code": "EUR"
}
Update Quote
PUT /api/v1/quotes/{quote_id}
Required Scope: write:quotes
Delete Quote
DELETE /api/v1/quotes/{quote_id}
Required Scope: write:quotes
Inventory
Inventory endpoints require the inventory module to be enabled (Admin settings). They use read:projects and write:projects scopes.
List Transfers
GET /api/v1/inventory/transfers
Required Scope: read:projects
Query Parameters:
date_from- Filter transfers on or after this date (YYYY-MM-DD)date_to- Filter transfers on or before this date (YYYY-MM-DD)page- Page numberper_page- Items per page (max 100)
Response: transfers (array of transfer objects with reference_id, moved_at, stock_item_id, from_warehouse_id, to_warehouse_id, quantity, notes, movement_ids) and pagination.
Create Transfer
POST /api/v1/inventory/transfers
Required Scope: write:projects
Request Body:
{
"stock_item_id": 1,
"from_warehouse_id": 2,
"to_warehouse_id": 3,
"quantity": 10,
"notes": "Optional notes"
}
Response: 201 Created with reference_id, transfers (pair of movements), and success message.
Get Transfer by Reference ID
GET /api/v1/inventory/transfers/<reference_id>
Required Scope: read:projects
Returns a single transfer (the pair of out/in movements) or 404 if not found.
Inventory Reports
Required Scope: read:projects for all report endpoints.
-
Valuation:
GET /api/v1/inventory/reports/valuation
Query:warehouse_id,category,currency_code. Returnstotal_value,by_warehouse,by_category,item_details. -
Movement History:
GET /api/v1/inventory/reports/movement-history
Query:date_from,date_to,stock_item_id,warehouse_id,movement_type,page,per_page. Returnsmovementsand optionalpagination. -
Turnover:
GET /api/v1/inventory/reports/turnover
Query:start_date,end_date,item_id. Returnsstart_date,end_date,items(turnover metrics per item). -
Low Stock:
GET /api/v1/inventory/reports/low-stock
Query:warehouse_id(optional). Returnsitems(entries below reorder point withquantity_on_hand,reorder_point,shortfall, etc.).
Reports
Get Summary Report
GET /api/v1/reports/summary
Required Scope: read:reports
Query Parameters:
start_date- Start date (ISO format)end_date- End date (ISO format)project_id- Filter by projectuser_id- Filter by user (admin only)
Response:
{
"summary": {
"start_date": "2024-01-01T00:00:00Z",
"end_date": "2024-01-31T23:59:59Z",
"total_hours": 160.5,
"billable_hours": 145.0,
"total_entries": 85,
"by_project": [
{
"project_id": 1,
"project_name": "Website Redesign",
"hours": 85.5,
"entries": 45
}
]
}
}
Users
Get Current User
GET /api/v1/users/me
Required Scope: read:users
Returns information about the authenticated user.
Personal integration connectors
Endpoints for the per-user GitHub, Google Calendar, and Slack
connectors (see docs/integrations/README.md).
All config/status/test/sync routes are session-authenticated
(@login_required); the two webhook receivers are unauthenticated but
verify a provider signature on every request.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/api/integrations/github/webhook |
HMAC-SHA256 (X-Hub-Signature-256) |
Receives GitHub issues / ping events. |
POST |
/api/integrations/github/sync |
session, admin | One-shot pull of open issues. |
POST |
/api/integrations/github/config |
session | Save the GitHub card. |
POST |
/api/integrations/github/test |
session | Verify the personal access token. |
GET |
/api/integrations/github/status |
session | Current config snapshot (no secrets). |
GET |
/integrations/google/connect |
session | Start the Google OAuth dance. |
GET |
/integrations/google/callback |
session | OAuth callback — stores tokens. |
POST |
/integrations/google/disconnect |
session | Revokes the token and clears config. |
POST |
/api/integrations/google/sync |
session | Runs import/export/both. |
GET |
/api/integrations/google/status |
session | Linked email, calendar id, last sync. |
POST |
/api/integrations/slack/events |
HMAC-SHA256 (X-Slack-Signature) |
Slack URL verification + /tt slash commands. |
POST |
/api/integrations/slack/config |
session | Save the Slack card. |
POST |
/api/integrations/slack/test |
session | Sends a test message to the channel. |
GET |
/api/integrations/slack/status |
session | Current config snapshot (no secrets). |
These endpoints sit under the regular Flask cookie session (not the
Bearer token flow used by /api/v1/...) because they back the
integrations settings UI, which is browser-based.
Interactive API Documentation
For interactive API documentation and testing, visit:
https://your-domain.com/api/docs
This Swagger UI interface allows you to:
- Browse all available endpoints
- Test API calls directly from your browser
- View detailed request/response schemas
- Try out different parameters
Code Examples
Python
import requests
API_TOKEN = "tt_your_token_here"
BASE_URL = "https://your-domain.com/api/v1"
headers = {
"Authorization": f"Bearer {API_TOKEN}",
"Content-Type": "application/json"
}
# List projects
response = requests.get(f"{BASE_URL}/projects", headers=headers)
projects = response.json()
# Create time entry
time_entry = {
"project_id": 1,
"start_time": "2024-01-15T09:00:00Z",
"end_time": "2024-01-15T17:00:00Z",
"notes": "Development work",
"billable": True
}
response = requests.post(f"{BASE_URL}/time-entries", json=time_entry, headers=headers)
JavaScript/Node.js
const axios = require('axios');
const API_TOKEN = 'tt_your_token_here';
const BASE_URL = 'https://your-domain.com/api/v1';
const headers = {
'Authorization': `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json'
};
// List projects
axios.get(`${BASE_URL}/projects`, { headers })
.then(response => console.log(response.data))
.catch(error => console.error(error));
// Start timer
axios.post(`${BASE_URL}/timer/start`,
{ project_id: 1, task_id: 5 },
{ headers }
)
.then(response => console.log('Timer started:', response.data))
.catch(error => console.error(error));
cURL
# List projects
curl -H "Authorization: Bearer tt_your_token_here" \
https://your-domain.com/api/v1/projects
# Create time entry
curl -X POST \
-H "Authorization: Bearer tt_your_token_here" \
-H "Content-Type: application/json" \
-d '{"project_id":1,"start_time":"2024-01-15T09:00:00Z","end_time":"2024-01-15T17:00:00Z"}' \
https://your-domain.com/api/v1/time-entries
Best Practices
Security
- Store tokens securely: Never commit tokens to version control
- Use environment variables: Store tokens in environment variables
- Rotate tokens regularly: Create new tokens periodically and delete old ones
- Use minimal scopes: Only grant the permissions needed
- Set expiration dates: Configure tokens to expire when appropriate
Performance
- Use pagination: Don't fetch all records at once
- Filter results: Use query parameters to reduce data transfer
- Cache responses: Cache data that doesn't change frequently
- Batch operations: Combine multiple operations when possible
Error Handling
- Check status codes: Always check HTTP status codes
- Handle rate limits: Implement exponential backoff for rate limit errors
- Log errors: Log API errors for debugging
- Validate input: Validate data before sending to API
Rate Limiting
The API implements rate limiting to ensure fair usage:
- Per-token limits: 100 requests per minute, 1000 requests per hour
- Response headers: Rate limit information is included in response headers
X-RateLimit-Limit: Maximum requests allowedX-RateLimit-Remaining: Requests remaining in current windowX-RateLimit-Reset: Unix timestamp when the limit resets
When rate limited, you'll receive a 429 Too Many Requests response.
Webhook Support
Webhooks are supported for real-time notifications. You can receive notifications when time entries are created/updated, projects change status, tasks are completed, and timer events occur. See Webhooks for setup and event types.
Support
For API support:
- Documentation: This guide and
/api/docs - GitHub Issues: Report bugs and request features
- Community: Join our community forum
Changelog
Version 1.0.0 (Current)
- Initial REST API release
- Full CRUD operations for projects, time entries, tasks, and clients
- Token-based authentication with scopes
- Comprehensive filtering and pagination
- Timer control endpoints
- Reporting endpoints
- Interactive Swagger documentation