Tracker Entries and Timers API
Record work, import entries, and control active timers with team-scoped public API v1 operations.
Summary
Tracker entries record completed work in seconds. Timer operations create and finish the same entry resource, so a stopped timer can appear in ordinary queries for its assignee. Read access and mutation access are not identical; review the access table below.
Capabilities
Endpoints
| Method | Endpoint | Scope | Purpose |
|---|---|---|---|
GET | /tracker-entries | tracker-entries.read | List entries in a date range |
POST | /tracker-entries | tracker-entries.write | Create entries for one or more dates |
POST | /tracker-entries/bulk | tracker-entries.write | Import up to 100 entry definitions |
GET | /tracker-entries/{id} | tracker-entries.read | Retrieve one entry |
PATCH | /tracker-entries/{id} | tracker-entries.write | Update an entry |
DELETE | /tracker-entries/{id} | tracker-entries.write | Delete an entry |
POST | /tracker-entries/timer/start | tracker-entries.write | Start a timer |
POST | /tracker-entries/timer/stop | tracker-entries.write | Stop a timer |
GET | /tracker-entries/timer/current | tracker-entries.read | Retrieve the active timer or null |
GET | /tracker-entries/timer/status | tracker-entries.read | Read live elapsed seconds |
Prerequisites
You must have a valid API token and at least one team-owned tracker project configured. Users need the correct tracker-entry scopes (tracker-entries.read or tracker-entries.write). Data such as project and assigned user IDs must be valid UUIDs within your team's account.
Concepts
A tracker entry represents a discrete block of time attributed to a user, linked to a project, and optionally described. Entries can be created in real time using timers or input manually with specified starts, stops, and durations. Idempotency can replay a successful request, but does not prevent duplicates after every error or after its retention window. Bulk operations expand entry definitions across multiple days. Responses include duration, project context, assignee, billing state, and nullable rate/currency fields. Manual create requests do not set the entry rate, currency, or billed flag; a project’s rate is separate from the entry-level rate.
Workflow
Manual Entry Request
Create and update requests use these fields:
| Field | Type | Rules |
|---|---|---|
projectId | UUID | Necessary team-owned tracker project |
assignedId | UUID or null | Team member; omitted or null defaults to the caller on create |
start | ISO 8601 timestamp | Necessary |
stop | ISO 8601 timestamp | Necessary for manual entries |
dates | date array | 1–366 YYYY-MM-DD values |
duration | integer | Whole seconds from 0 to 31,536,000 |
description | string or null | Up to 2,000 characters |
The server stores the duration that you send. It does not recompute manual duration or check that stop follows start. Use UTC timestamps ending in Z, and keep timestamps, dates, and duration consistent in your integration.
Each value in dates creates a row with that date and the same supplied start, stop, duration, and description. Timestamps are not shifted to each date, and duplicate dates are not deduplicated. Use separate definitions when timestamps differ between days. Replace the example project UUID below with a real team project.
curl --request POST \
--url https://api.eigenn.io/v1/tracker-entries \
--header "Authorization: Bearer $EIGENN_API_TOKEN" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: entry-2026-07-12-design" \
--data '{
"projectId": "b3b6e2c2-1f2a-4e3b-9c1d-2a4b6e2c21f2",
"start": "2026-07-12T09:00:00.000Z",
"stop": "2026-07-12T10:30:00.000Z",
"dates": ["2026-07-12"],
"duration": 5400,
"description": "Invoice workflow design"
}'Creation returns 201 with data. This is an array because one request can expand across many dates.
Bulk Import
POST /tracker-entries/bulk accepts entries with 1–100 manual entry definitions. Each definition creates one stored record for every value in its dates array. The request shape is { "entries": [ ... ] }; success returns 201 with a data array. The number of returned records can exceed the number of definitions. Each definition can contain up to 366 dates, so use small batches that you can reconcile. Returned row order is not a provider-record mapping.
The server checks all project and assignee references before a single bulk insert. Response loading and validation happen after insertion, so an error can follow saved rows. Use one key per exact import request and retain the returned Eigenn IDs. After a timeout, inspect saved state for the affected assignees before replaying; the ordinary list is restricted to the caller. A new key or an uncached failed request can create duplicates.
Remote-provider synchronization is not available in Eigenn's public REST API. Normalize Harvest, Toggl, Clockify, or another provider in your own server. Then send the completed entries through this bulk endpoint.
List Entries
GET /tracker-entries needs inclusive from and to dates in YYYY-MM-DD format. projectId is optional; from must be on or before to. The date range is based on each entry’s stored date, not overlap between its start and stop timestamps. There is no pagination parameter, so use bounded date ranges.
The response contains:
meta.totalDurationin secondsmeta.totalAmountcalculated from project rates- echoed
meta.fromandmeta.to result, an object keyed by entry date
Each returned entry includes its project, nullable customer summary, assigned user, billing state, nullable entry rate/currency, timestamps, and duration. The list only includes entries assigned to the authenticated caller. A project filter with no matching entries returns an empty result.
totalAmount uses each project’s current hourly rate multiplied by duration in hours. It does not use the stored entry rate, filter to billable or unbilled work, or convert currencies. Changing a project rate changes this calculation for existing work. Do not treat a mixed-currency total as a converted financial amount.
Running-entry limitation: list and single-entry response schemas require a non-null stop and duration. A running timer in a requested date range can cause a 500, and reading that running entry through GET /tracker-entries/{id} can do the same. Use timer endpoints for running state.
Update an Entry
PATCH /tracker-entries/{id} accepts any non-empty subset of the manual-entry fields. Omitted fields keep their stored values. If you send dates, it must contain exactly one date. A patch updates one stored entry. Use create or bulk create when one definition must expand across many dates.
PATCH returns 200 with a data array containing the updated entry. An active timer does not yet have a complete stop time or duration; a patch that leaves those missing returns 409. Stop it through the timer endpoint before editing completed work.
Omitted assignee keeps the stored assignment. On PATCH, assignedId: null clears it instead of defaulting to the caller. The response requires a user object, so clearing the assignee can save the change and then fail response validation. The unassigned row also leaves the caller-only list; avoid null assignment when relying on this REST read path.
DELETE returns 200 with the deleted ID. It is permanent and only removes a row assigned to the caller; missing, foreign-team, or teammate-assigned rows return 404.
Start a Timer
POST /tracker-entries/timer/start needs projectId. Optional fields are assignedId, description, and a custom ISO 8601 start timestamp.
The server validates the project and selected assignee, stops that user's latest running timer, then creates the new one. The old timer stops at server time, even if the new timer uses a custom start. Starting is not an atomic handoff: a failed create can leave the old timer stopped. Concurrent starts can leave more than one running row; serialize starts per user and inspect saved state after an error.
A successful start returns 201 with { "data": ... }. The active entry has stop: null and duration: null; its stored date is the UTC date of the chosen start.
{
"projectId": "b3b6e2c2-1f2a-4e3b-9c1d-2a4b6e2c21f2",
"description": "Reconcile July invoices"
}Read Timer State
GET /tracker-entries/timer/current returns { "data": null } when no timer is active. A timer remains discoverable if it crosses midnight.
GET /tracker-entries/timer/status returns a data object containing:
isRunningcurrentEntryelapsedTimein whole seconds, which Eigenn calculates when it serves the request
When no timer exists, status returns isRunning: false, currentEntry: null, and elapsedTime: 0. Current and status select the latest running entry by start time, without a same-day restriction. A future custom start can produce negative elapsed seconds.
Both accept assignedId; omission uses the caller. These reads filter by team and assignee, but do not independently validate current membership. An unknown assignee normally yields no active timer rather than a 404.
Stop a Timer
POST /tracker-entries/timer/stop accepts an optional entryId, assignedId, and custom ISO 8601 stop timestamp. Without entryId, Eigenn stops the selected user's current timer. Without assignedId, it uses the authenticated user. Even with an explicit entryId, the selected assignee must match that entry.
The server calculates the duration from the stored start and the chosen stop timestamp. An earlier stop can produce and save a negative duration; the handler does not reject it. Validate timestamp order in your integration. Success returns 200 with the stopped entry in data.
No running timer, an unmatched ID/assignee, an already stopped entry, or a missing start currently raises an unmapped error and can return 500. Do not assume these cases return 404 or 409. A stop is not a safe repeatable operation by itself; after an uncertain response, check current status and the completed entry before retrying.
Behavior specification
Access and Persistence
| Operation | Current record selection |
|---|---|
| Range list and single GET | Authenticated team and caller’s assigned entries |
| DELETE | Authenticated team and caller’s assigned entries |
| Create and bulk create | Team project and team-member assignee; omission/null uses caller |
| PATCH | Existing team entry; this route does not impose the single-GET caller-assignee restriction |
| Timer current/status/stop | Team and selected assignee; omission/null uses caller |
| Timer start | Team project and validated team-member assignee |
Creating work for another teammate does not make that work visible in your ordinary list or single GET. Reconcile it through that assignee’s authorized view or the application. Do not interpret a caller-only empty list as proof that an import failed.
Manual writes and timer changes can persist before response loading, activity recording, or response validation fails. Idempotency keys replay successful responses without comparing new bodies, and failed responses do not receive successful-write replay protection. Keep one key for one unchanged request and verify saved state after uncertainty. See Errors and Safe Retries.
Common Errors
| Status | Cause |
|---|---|
401 | Missing or invalid bearer token |
403 | Missing tracker entry scope |
404 | Missing/inaccessible ordinary entry, or invalid project/assignee reference on create, patch, or timer start |
409 | In-progress idempotency key, or PATCH lacks the fields needed for a completed entry |
400 / 422 | Invalid UUID, date range, timestamp format, duration, or request shape |
429 | Request-rate limit exceeded |
500 | Timer-stop state error, response mismatch, or other unmapped failure; saved changes may remain |
Diagrams
Rendering diagram…
Screenshots
Not applicable: this page describes an API or a non-visual workflow rather than an application screen.
Verification
- List entries: Issue a GET request to
tracker-entrieswith valid date parameters. Expected outcome: Response includes the total duration, total amount, date range meta, and entries per date with correct project, user, and duration data. - Create a manual entry: POST to
tracker-entrieswith projectId, valid date, start, stop, and duration. Expected outcome: Response returns 201 and contains the created entry in an array. - Bulk import: POST to
tracker-entries/bulkwith up to 100 entries. Expected outcome: Returned array contains a record for each date in all entries submitted. - Update entry: PATCH a specific entry ID with new details. Expected outcome: Entry reflects updated fields while unchanged fields remain the same.
- Start timer: POST to
tracker-entries/timer/startwith projectId. Expected outcome: Previous timer (if any) is stopped, and a new timer entry is created and returned with stop and duration set as null. - Read timer state: GET
tracker-entries/timer/currentor/timer/status. Expected outcome: Active timer or null, plus running state and elapsed seconds if running. - Stop timer: POST to
tracker-entries/timer/stopwith or without entryId/assignedId. Expected outcome: Timer entry gets updated with correct stop time and computed duration. - Compare caller and teammate reads, edits, and deletion. Confirm assignment clearing and running-entry response limitations using disposable data.
- Check repeated dates, mixed project rates/currencies, timer concurrency, future starts, earlier stops, and saved state after uncertain writes. Do not use these edge-case fixtures for billing.
These are pending runtime acceptance checks. Source review alone does not establish verified timer behavior.