TimetableBlue Drop Labs

Connect your tools

The API uses JSON and the same permissions as the web app. Every application record uses a UUID. Dates use YYYY-MM-DD; timestamps use RFC 3339 with seconds and an explicit offset. Exact durations are stored as integer seconds. There is no automatic rounding or minimum duration.

Authentication

Create a personal token in Settings. Send it in the Authorization header. Tokens are shown once, stored as hashes, and can be revoked immediately. A token belongs to its user and organization. A member token only accesses that person’s time. Manager and admin tokens can manage team time; only admins can manage invitations and roles.

export UTT_TOKEN='your-personal-token'
curl http://unnamed-time-tracker.ddev.site/api/me \
  -H "Authorization: Bearer $UTT_TOKEN"

Each personal token is tied to the team where it was created. It cannot switch teams, and X-Team-ID never overrides a token’s team. Create another token after switching teams in the web app.

Browser login uses a separate HttpOnly session cookie. Browser pages send X-Team-ID to pin requests to their loaded team; the server verifies membership on every request, so switching in another tab cannot redirect an old form’s writes. GET /api/csrf returns a CSRF token; browser mutations send X-CSRF-Token. API tokens do not need CSRF headers and never create a browser session. Use HTTPS when deploying beyond local development.

Add a completed entry

List /api/projects to find a project UUID and its client. Submit either a duration or a pair of timestamps.

{
  "project_id": "019a0000-0000-7000-8000-000000000001",
  "date": "2026-09-09",
  "duration": "1h 15m",
  "description": "Implement project navigation",
  "source": "burrowtime",
  "external_id": "stable-local-frame-id"
}

duration also accepts decimal hours, such as 1.25, or 75m. For exact machine input, replace duration with "duration_seconds": 4500. Never send both forms. Fractional seconds are rejected rather than silently rounded. Duration entries support one second through one year.

curl http://unnamed-time-tracker.ddev.site/api/entries \
  -H "Authorization: Bearer $UTT_TOKEN" \
  -H 'Content-Type: application/json' \
  --data-binary @entry.json

For timestamps, omit date and duration:

{
  "project_id": "019a0000-0000-7000-8000-000000000001",
  "started_at": "2026-09-09T09:00:00-04:00",
  "ended_at": "2026-09-09T10:15:00-04:00",
  "description": "Implement project navigation"
}

Managers and admins may add user_id to create an entry for someone in their organization. Entries cannot be reassigned to another person after creation.

Retry safely

The combination of organization, user, source, and external_id identifies an imported entry. Submit the same body again and the API returns the existing entry with duplicate: true and HTTP 200. New entries return HTTP 201. If the external ID is reused with different data, the API returns HTTP 409. PATCH the existing entry’s UUID to intentionally change it. Keep your source name stable and external IDs case-sensitive. Send a source name of at most 64 characters and an external ID of at most 128 characters.

{"entry": {"id": "019a0000-0000-7000-8000-000000000002"}, "duplicate": false}

The actual response includes all entry fields. Timestamp fields in responses are Unix seconds; running and elapsed_seconds are also included. Lists of entries include total, limit, and offset.

Timers

POST /api/timers/start accepts project_id, optional description, optional started_at, and optional keep_running. Starting a timer stops your other timers unless keep_running is true. Multiple timers and overlapping completed entries count in full.

POST /api/entries/{uuid}/stop stops a timer. Repeating it is safe. PATCH /api/entries/{uuid} can edit a running timer’s start, description, or project. A running timer cannot start in the future. DELETE /api/entries/{uuid} removes either a running timer or a completed entry.

Endpoints

Method Path Purpose
GET /api/health Service health, public
POST /api/login Email/password login; email, password
POST /api/logout End browser session
GET /api/csrf Browser CSRF token
GET, PATCH /api/me Profile; update name, timezone, or password with current_password
GET, POST /api/clients List or create clients with name
PATCH /api/clients/{uuid} Change name or archived
GET, POST /api/projects List or create with name, client_id, optional hex color
PATCH /api/projects/{uuid} Change name, client_id, color, or archived
GET, POST /api/entries List or create entries
GET, PATCH, PUT, DELETE /api/entries/{uuid} Read, update, or delete an entry; PUT currently has partial-update semantics
POST /api/timers/start Start a timer
POST /api/entries/{uuid}/stop Stop a timer
GET /api/reports Daily totals and client/project/user breakdowns
GET /api/reports/forecast Monthly individual/team forecasts
GET /api/reports/insights Previous-period comparisons, client/person changes, and recorded work patterns
GET, POST /api/reports/saved List personal/team saved definitions or create one
PATCH, DELETE /api/reports/saved/{uuid} Update or delete a permitted saved report
GET /api/reports/export Download CSV
GET /api/organizations List active team memberships; API tokens see only their own team
POST /api/organizations Session only: create a team with name and become its admin
PATCH /api/organization Admin: rename the current team with name
POST /api/organizations/{uuid}/switch Session only: select an active membership
GET /api/users Own profile, or team directory for managers/admins
PATCH /api/users/{uuid} Admin changes role or boolean active
GET, POST /api/invitations Admin lists or creates invitations with email, role
DELETE /api/invitations/{uuid} Revoke an invitation
GET /api/invitations/lookup?token=... Validate invitation, public
POST /api/invitations/accept Set token, name, timezone, password, public
GET, POST /api/tokens List own tokens or create one with name
DELETE /api/tokens/{uuid} Revoke own token
POST /api/imports/clockify/connect Verify api_key, optional region; return remote identity/workspaces
POST /api/imports/clockify/prepare Read personal history with api_key, region, workspace_id; return preview UUID and destinations
GET /api/imports/clockify List your completed syncs
POST /api/imports/clockify/{uuid}/review Validate mappings and return a dry-run summary
POST /api/imports/clockify/{uuid}/apply Apply the last reviewed mappings; retry-safe
DELETE /api/imports/clockify/{uuid} Cancel your pending preview

Inviting an email already registered in a different team is supported. Invitation lookup returns existing_account; those users accept with the invitation token and their current password. Their profile is retained. New accounts still provide name, timezone, and a new password. Deactivation removes only that team’s membership access.

Clockify mapping API

The Clockify endpoints use your normal local session or personal API token. api_key is a separate Clockify credential, used only to read Clockify. Supported regions are global (default), eu, us, uk, and au. The prepare endpoint reads only the Clockify key owner’s history; there is no remote user override. Previews and results are scoped to the signed-in local user, even for managers/admins.

Prepare may return a warnings array with unavailable project/company IDs, placeholder names, and affected entry counts. No time has been imported. Show these warnings and ask whether to continue. If the user agrees, map or skip the placeholder destinations and include "acknowledge_warnings": true alongside clients and projects in the review body. Review rejects missing, false, or non-boolean acknowledgement. Cancel by deleting the preview.

Use the remote IDs returned by prepare as keys in the review body. Existing destination IDs are local UUIDs:

{
  "clients": {
    "CLOCKIFY_CLIENT_ID": {"mode": "existing", "id": "LOCAL_CLIENT_UUID"},
    "ANOTHER_CLOCKIFY_CLIENT": {"mode": "skip"}
  },
  "projects": {
    "CLOCKIFY_PROJECT_ID": {"mode": "new", "name": "Imported project"}
  }
}

Every client and every project under a non-skipped client requires a choice. A project may use existing, new, or skip too. Existing projects must belong to the selected client. Creating destinations requires manager/admin permissions; members must choose existing records or skip. New names default to the Clockify names when omitted. Unassigned time appears as explicit client/project choices in the preview.

Review saves validated mapping choices and rolls back destination/time writes. Apply takes {} and uses those saved choices; it checks permissions and destinations again and commits atomically. Results include entries_created, clients_created, projects_created, duplicates, changed_entries, running_skipped, skipped_entries, and exact seconds. Applying the same completed preview again returns the saved result. To fetch additional time, prepare a new preview. Previously imported external entry IDs are preserved across the command and web importer.

Previews expire after one hour. Completed previews retain resolved mappings and summary counts; their raw history snapshot is removed. API keys are never persisted. Remote failures return HTTP 502 with a sanitized error; invalid mappings use 422 and expired/unreviewed previews use 409.

Filters and reporting

Entries and reports accept from, to, user_id, client_id, and project_id. Use user_id=me for your own time. Members cannot query someone else’s time. When passing dates, supply both from and to. Reports support ranges of up to one year. Entry lists accept limit from 1 through 1,000, offset, and running=1.

Saved reports store a view and validated filters with personal or team visibility. Team sharing does not grant access to other people’s time. Period insights compare the chosen range with the immediately preceding range of equal length and include per-person daily activity. See report definitions, permissions, and insights for the full contract.

Timed entries are split across local midnights in the user’s current time zone, including daylight-saving changes. Duration-only entries retain their chosen local date. Team totals sum those local-day totals. The CSV emits one row per entry per local day and escapes spreadsheet formula prefixes. The entry list shows each full entry once.

Forecasts accept month=YYYY-MM and the user/client/project filters. They use recent, weighted weekday averages and explicitly include zero-hour days after the first entry. Today counts as recorded. Estimates begin on the following day. No-history users have a null estimate; users_without_history flags incomplete team estimates. Forecasts do not model planned leave or holidays.

Errors

Errors return {"error":"A message explaining what to fix."}. Common statuses are 401 for sign-in/token failures, 403 for insufficient permissions or CSRF failure, 404 for missing or inaccessible records, 409 for duplicate/conflicting imports, and 422 for invalid input. Login attempts are rate-limited.

Burrow Time

This repository includes scripts/burrowtime-push.py, which reads the documented output of burrowtime log --json. It previews by default and only uploads with --apply.

{"my-local-project": "019a0000-0000-7000-8000-000000000001"}

Save that mapping as project-map.json, substituting a real UUID, then:

burrowtime log --today --json > /tmp/time-entries.json
python3 scripts/burrowtime-push.py --map project-map.json --entries /tmp/time-entries.json
python3 scripts/burrowtime-push.py --map project-map.json --entries /tmp/time-entries.json --apply

Descriptions preserve sorted Burrow Time tags. Existing Burrow Time configuration is unchanged. The helper validates all mappings before sending entries and makes retries safe through each frame’s stable ID. See docs/IMPORTS.md for the separate Clockify importer.

Agent integration and Google sign-in

The agent setup guide provides a local MCP connector and companion skill. The connector calls this API with a personal team token and uses source=mcp plus a stable UUID external_id for completed entries and timer starts. There is no separate hosted /mcp endpoint in this release.

Google uses browser sessions. GET /api/auth/providers is public and returns { "google": true|false }. GET /api/auth/google/start begins sign-in, and GET /api/auth/google/callback handles Google’s response. Authenticated GET /api/auth/google returns availability and the connected email. POST /api/auth/google/link and /api/auth/google/unlink require a browser session, CSRF token, and current_password; personal API tokens cannot manage these connections. Linking returns a Google authorization url. The Google identity connects to the shared account rather than a team membership. See Google setup for deployment details.

Stripe invoicing

Optional admin billing endpoints, customer mappings, project rates, draft exports, retry behavior, and status reconciliation are documented in the Stripe guide.

Profile photos and team icons

POST /api/me/photo uploads the current account’s profile photo; DELETE /api/me/photo removes it. Profile photos follow the account across teams. Team admins use POST /api/organization/icon and DELETE /api/organization/icon to manage the current team’s icon.

Uploads use multipart/form-data with one file field named image. Send the usual CSRF header for cookie sessions. Accepts PNG, JPEG, or WebP up to 5 MB and 16 megapixels. The server normalizes images to 512 × 512 PNG, cropping photos centrally and preserving full team icons. Responses are { "url": "/api/media/..." } or { "url": null } after removal. Invalid files return 422; storage outages return 503 without replacing the previous image.

GET /api/media/status returns { "enabled": true|false } for signed-in users. Uploads require backend R2 configuration. /api/me includes avatar_url and organization_icon_url; /api/users includes avatar_url for each visible person. These URLs require an authenticated session or API token and return private PNG responses, not JSON. Membership and current image version are checked on every request.