Privacy-focused time tracking for Android, with a REST API for integrations
A simple, privacy-focused time tracking app for Android.
TimeTracker provides a REST API for third-party integrations. Use it to check your tracking status, toggle tracking, query time entries, and read progress metrics from external tools, scripts, or smart home automations.
Base URL:
https://europe-west1-timetracker-22fbb.cloudfunctions.net/api
All endpoints require a Firebase ID token in the Authorization header:
Authorization: Bearer <FIREBASE_ID_TOKEN>
To obtain a token, sign in with Google via Firebase Auth and call getIdToken() on the Firebase user object.
Error responses:
| Status | Body | Cause |
|---|---|---|
401 |
{ "error": "Missing or malformed Authorization header" } |
Header absent or not in Bearer <token> format |
401 |
{ "error": "Invalid or expired token" } |
Token failed verification |
GET /tracking/statusReturns the current tracking state.
Response:
{
"isTracking": false,
"startTime": 0
}
| Field | Type | Description |
|---|---|---|
isTracking |
boolean |
Whether the timer is currently running |
startTime |
number |
Epoch milliseconds when tracking started, or 0 if idle |
Calculating elapsed time: If
isTrackingistrue, compute elapsed milliseconds asDate.now() - startTime. The API intentionally does not return elapsed time because it would be stale by the time you read it.
POST /tracking/toggleToggles the tracking state. If idle, starts tracking. If tracking, stops and creates a time entry.
Request body: None required.
Response when starting:
{
"isTracking": true,
"startTime": 1738857600000
}
Response when stopping:
{
"isTracking": false,
"startTime": 0,
"entry": {
"id": "abc123",
"startTime": 1738857600000,
"endTime": 1738861200000,
"type": "WORK",
"duration": 3600000
}
}
| Field | Type | Description |
|---|---|---|
isTracking |
boolean |
New tracking state after toggle |
startTime |
number |
Start time if now tracking, 0 if stopped |
entry |
object |
Only present when stopping. The created time entry |
entry.id |
string |
Firestore document ID of the new entry |
entry.startTime |
number |
Epoch ms when the session began |
entry.endTime |
number |
Epoch ms when the session ended |
entry.type |
string |
Always "WORK" |
entry.duration |
number |
Duration in milliseconds |
GET /progressReturns pre-computed progress metrics. This data is automatically recalculated by a Cloud Function whenever time entries, tracking state, or settings change.
Response:
{
"daily": {
"hoursTracked": 3.5,
"goal": 8.0,
"progress": 0.4375
},
"weekly": {
"hoursTracked": 22.0,
"goal": 37.5,
"progress": 0.5867
},
"monthly": {
"hoursTracked": 88.0,
"goal": 160.0,
"progress": 0.55
},
"tracking": {
"isTracking": true,
"startTime": 1738857600000
},
"overtimeCarryover": 2.5,
"weeksHistory": [],
"lastUpdated": 1738861200000
}
| Field | Type | Description |
|---|---|---|
daily |
ProgressMetric |
Today’s progress |
weekly |
ProgressMetric |
This week’s progress (with overtime adjustment) |
monthly |
ProgressMetric |
This month’s progress |
tracking |
object |
Current tracking state |
overtimeCarryover |
number |
Hours of overtime carried from previous week |
weeksHistory |
array |
See GET /history for the shape |
lastUpdated |
number\|null |
Epoch ms when progress was last recomputed |
ProgressMetric:
| Field | Type | Description |
|---|---|---|
hoursTracked |
number |
Hours tracked in this period (excludes active session) |
goal |
number |
Target hours for this period |
progress |
number |
Ratio hoursTracked / goal, clamped to 0.0-1.0 |
Live progress during tracking: The
hoursTrackedvalues do not include the currently active session. To display live progress, add(Date.now() - tracking.startTime) / 3600000to thehoursTrackedvalues whentracking.isTrackingistrue.
Error: Returns 404 with { "error": "No progress data found. Start tracking to generate data." } if no computed data exists yet.
GET /entriesReturns time entries with optional filtering and pagination.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
start |
number |
— | Filter entries with startTime >= start (epoch ms) |
end |
number |
— | Filter entries with startTime <= end (epoch ms) |
limit |
number |
500 |
Max entries to return (capped at 1000) |
Example:
GET /entries?start=1738368000000&end=1738972800000&limit=100
Response:
{
"entries": [
{
"id": "abc123",
"startTime": 1738857600000,
"endTime": 1738861200000,
"type": "WORK",
"duration": 3600000
}
],
"count": 1
}
| Field | Type | Description |
|---|---|---|
entries |
array |
Time entries, sorted by startTime descending |
entries[].id |
string |
Firestore document ID |
entries[].startTime |
number |
Epoch ms |
entries[].endTime |
number |
Epoch ms |
entries[].type |
string |
Entry type (e.g., "WORK") |
entries[].duration |
number |
Duration in milliseconds |
count |
number |
Number of entries returned |
GET /historyReturns aggregated weekly history with overtime tracking.
Response:
{
"weeksHistory": [
{
"year": 2026,
"week": 6,
"weekStartMillis": 1738540800000,
"weekEndMillis": 1739145599999,
"totalDuration": 144000000,
"standardWeeklyGoal": 40,
"effectiveWeeklyGoal": 37.5,
"overtimeFromPreviousWeek": 2.5,
"overtimeGenerated": 1.0,
"dailyTotals": {
"2": 28800000,
"3": 28800000,
"4": 28800000,
"5": 28800000,
"6": 28800000
},
"dailyGoals": {
"2": 8.0,
"3": 8.0,
"4": 8.0,
"5": 8.0,
"6": 8.0
}
}
],
"overtimeCarryover": 1.0
}
| Field | Type | Description |
|---|---|---|
weeksHistory |
array |
Weeks sorted by most recent first |
weeksHistory[].year |
number |
Year |
weeksHistory[].week |
number |
ISO week number |
weeksHistory[].weekStartMillis |
number |
Epoch ms of week start |
weeksHistory[].weekEndMillis |
number |
Epoch ms of week end |
weeksHistory[].totalDuration |
number |
Total tracked time in milliseconds |
weeksHistory[].standardWeeklyGoal |
number |
Base weekly goal in hours |
weeksHistory[].effectiveWeeklyGoal |
number |
Goal after overtime deduction |
weeksHistory[].overtimeFromPreviousWeek |
number |
Overtime hours carried in |
weeksHistory[].overtimeGenerated |
number |
Overtime hours generated this week |
weeksHistory[].dailyTotals |
object |
Tracked ms per day. Keys are day-of-week ("1"=Sunday, "2"=Monday, …, "7"=Saturday) |
weeksHistory[].dailyGoals |
object |
Goal hours per day. Same key format as dailyTotals |
overtimeCarryover |
number |
Current overtime carryover in hours |
All endpoints return JSON error objects on failure:
| Status | Body | Cause |
|---|---|---|
401 |
{ "error": "..." } |
Authentication failed |
404 |
{ "error": "..." } |
Resource not found |
500 |
{ "error": "Internal server error" } |
Unexpected server error |
# Get a Firebase ID token (you'd normally get this from your app)
TOKEN="your-firebase-id-token"
# Check tracking status
curl -H "Authorization: Bearer $TOKEN" \
https://europe-west1-timetracker-22fbb.cloudfunctions.net/api/tracking/status
# Start/stop tracking
curl -X POST -H "Authorization: Bearer $TOKEN" \
https://europe-west1-timetracker-22fbb.cloudfunctions.net/api/tracking/toggle
# Get today's progress
curl -H "Authorization: Bearer $TOKEN" \
https://europe-west1-timetracker-22fbb.cloudfunctions.net/api/progress
# Get this week's entries
curl -H "Authorization: Bearer $TOKEN" \
"https://europe-west1-timetracker-22fbb.cloudfunctions.net/api/entries?start=1738540800000&end=1739145599999"
# Get weekly history
curl -H "Authorization: Bearer $TOKEN" \
https://europe-west1-timetracker-22fbb.cloudfunctions.net/api/history
TimeTracker is privacy-focused. All data is stored locally by default. Cloud sync via Google Sign-In is completely optional.
Questions? Email [email protected]