Timetracking
This guide explains how the Friday API tracks employee time with time records, how clock-in and clock-out work, what a switch is, how historical create and update writes enforce a non-overlapping timeline, and how to read time records and timetrack reports.
Prerequisites
- Plan: Timetracking or higher.
- Auth: An API key for the company, sent as
X-API-Keyon every request. - Employees: The employee must belong to the company associated with your API key and must be active.
What Is a Time Record?
A time record represents one continuous span of employee time. It starts when an employee clocks in and ends when they clock out.
An active time record has end: null.
Closed records for an employee sit on a single sequential timeline. Intervals are half-open ([start, end)): records may share an endpoint (they meet), but they must not interior-overlap. See Non-overlapping timeline for how create, bulk create, and update enforce this.
{
"time_record_id": 1042,
"type": "work",
"start": "2026-03-05T09:00:00.000Z",
"end": null,
"employee_id": 25,
"customer_id": 3,
"project_id": 7,
"note": "Starting morning shift"
}| Field | Meaning |
|---|---|
type | Either work or break. |
start | Clock-in timestamp. |
end | Clock-out timestamp, or null while the record is active. |
employee_id | Employee whose time is being tracked. |
customer_id | Optional customer/job associated with the time. |
project_id | Optional project associated with the time. |
note / end_note | Optional notes captured at clock-in or clock-out. |
Typical Flow
1. Clock in for work → POST /time-records/clock-in
2. Optionally start break → POST /time-records/clock-in with type: "break"
3. Resume or switch work → POST /time-records/clock-in with type: "work" or "switch"
4. Clock out → POST /time-records/clock-out
5. Read history/reports → GET /time-records or /timetrack/*
For imports and corrections (not live sessions), use POST /time-records, POST /time-records/bulk, or PATCH /time-records/:time_record_id. Those writes enforce the non-overlapping timeline.
The API keeps only one active record per employee at a time. When a new valid clock-in starts a different type of record, the existing active record is automatically ended first.
Clock In
POST /time-records/clock-in
Clock-in starts a new active time record for an employee. The only required field is employee_id.
If type is omitted, the API defaults to work.
{
"employee_id": 25,
"type": "work",
"customer_id": 3,
"project_id": 7,
"note": "Starting morning shift",
"start_coordinates": "40.7128,-74.0060",
"start_location": "Job site - Main St"
}| Field | Required? | Notes |
|---|---|---|
employee_id | Yes | Employee to clock in. |
type | Optional | work, break, or switch. Defaults to work. |
customer_id | Optional | Customer/job to associate with the record. May be required by company settings. |
project_id | Optional | Project to associate with the record. May be required by company settings. |
note | Optional | Clock-in note. |
start_coordinates | Conditional | Required when job geofencing applies. Format: latitude,longitude. |
start_location | Optional | Human-readable clock-in location label. |
Conflicts and validation
- If the employee already has an active record of the same type, the API returns
409(often withcode: "TIMELINE_ALREADY_CLOCKED_IN"). - If the employee starts a different type of record, the existing record is automatically clocked out first.
- If company settings require a customer or project and neither is provided, the API returns
400. - If a specified customer or project is disabled, the API returns
400. - If job geofencing applies and coordinates are missing, invalid, or outside the allowed perimeter, the API returns
400. - If another clock-in for the same employee is already in progress, the API returns
429withcode: "TIMELINE_LOCK_NOT_ACQUIRED"— retry the request.
Breaks
Use type: "break" when an employee pauses work but remains in the middle of their day.
{
"employee_id": 25,
"type": "break",
"note": "Lunch break"
}An employee must already have an active work record before starting a break. If they are not clocked in, the API returns:
{
"error": "Employee must be clocked in to take a break"
}When the break starts, the active work record is ended and a break record begins.
Switches
Use type: "switch" when an employee stays clocked in but changes what they are working on, such as moving from one customer or project to another.
{
"employee_id": 25,
"type": "switch",
"customer_id": 8,
"project_id": 12,
"note": "Moving to afternoon job"
}A switch:
- Automatically clocks out the current active work record.
- Creates a new
workrecord with the new customer/project/note details. - Returns the newly created time record.
Important:
switchis a request action, not a stored time record type. The new record is stored and returned astype: "work".
Clock Out
POST /time-records/clock-out
Clock-out ends the employee's current active time record.
{
"employee_id": 25,
"end_note": "Finished for the day",
"end_coordinates": "40.7128,-74.0060",
"end_location": "Job site - Main St"
}| Field | Required? | Notes |
|---|---|---|
employee_id | Yes | Employee to clock out. |
end_note | Optional | Clock-out note. |
end_coordinates | Conditional | Required when job geofencing applies to the active record. |
end_location | Optional | Human-readable clock-out location label. |
If the employee has no active time record, the API returns 409.
If another clock-out for the same employee is already in progress, the API returns 429 with code: "TIMELINE_LOCK_NOT_ACQUIRED" — retry the request.
Non-overlapping timeline
Historical writes — POST /time-records, POST /time-records/bulk, and PATCH /time-records/:time_record_id — require a completed record (start and end) and enforce a non-overlapping timeline per employee. Work and break records follow the same rule.
These endpoints are for imports, corrections, and sync jobs. For live sessions, use clock-in and clock-out instead.
Bare writes (default)
If the new or edited span interior-overlaps an existing record, the API rejects the request with 409:
{
"errors": ["This time overlaps an existing time record"],
"code": "TIMELINE_OVERLAP",
"conflicts": [
{
"time_record_id": 123,
"type": "work",
"start": "2026-07-01T13:00:00.000Z",
"end": "2026-07-01T21:00:00.000Z"
}
]
}conflicts[].end is null when the conflicting record is still active. Records that only meet at an endpoint (for example work ending exactly when a break starts) are allowed.
On bulk create, overlapping items fail individually with the same code and conflicts shape in the failed entry (HTTP 207 partial success). Accepted items are persisted in order, so a later item can conflict with an earlier item from the same request.
Resolving overlaps with intent: "overwrite"
intent: "overwrite"To make your record win over conflicting closed neighbors, send "intent": "overwrite" on create, each bulk item, or update:
{
"employee_id": 25,
"type": "break",
"start": "2026-07-10T15:30:00.000Z",
"end": "2026-07-10T16:00:00.000Z",
"intent": "overwrite"
}| Conflict geometry | What happens to the existing record |
|---|---|
| Fully covered by yours (or identical span) | Deleted — including notes, photos, and job attribution |
| Yours covers its start | Trimmed: its start moves to your record's end |
| Yours covers its end | Trimmed: its end moves to your record's start |
| It fully contains yours (e.g. break inside work) | Split into two records around yours |
Overwrite never modifies an active (open) record. If your span collides with one, the API returns 409 with code: "TIMELINE_OVERWRITE_OPEN_RECORD" — clock the employee out first.
Bare overlapping writes never auto-overwrite. Because overwrite can delete and trim data, send it only after inspecting the conflicts list, or when your integration is the sole writer for that employee's timeline. Do not put intent: "overwrite" in a blind retry loop — a second overwrite can re-trim neighbors the first attempt already reshaped.
Update response extras
On a successful PATCH that reshapes neighbors, the response includes:
policy: "normalize"affected_record_ids— records touched by the reshape (trimmed, split, or deleted neighbors, plus the updated record)deleted_record_ids— neighbor records that were deleted
These fields are omitted when the update does not reshape other records. Create responses return only the inserted record in data; side effects are visible via webhooks and subsequent reads.
Webhooks
When overwrite reshapes the timeline, per-record webhooks fire for every affected record — neighbors first, your record last:
- Trim →
time_record.updated - Split →
time_record.updated(left) thentime_record.created(right remainder) - Delete →
time_record.deleted - Then your record →
time_record.createdortime_record.updated
Refresh any local timeline cache after an overwrite.
Other timeline errors
| Code | HTTP | Meaning |
|---|---|---|
TIMELINE_END_BEFORE_START | 400 | end is before start |
TIMELINE_ZERO_DURATION | 400 | start and end are the same (at minute precision) |
TIMELINE_SPAN_TOO_LONG | 400 | The record spans more than one midnight in the company timezone — split by day |
TIMELINE_OPEN_NOT_ALLOWED | 400 | Historical create/update requires an end time |
TIMELINE_LOCK_NOT_ACQUIRED | 429 | Another write for this employee is in progress — retry |
Customers, Projects, and Geofencing
Time records can be associated with a customer, a project, or both. Companies may require a customer or project on clock-in. Use the Customers and Projects guide for more detail on how those objects relate.
If job geofencing applies, clock-in and clock-out require coordinates in latitude,longitude format. The API enforces the same customer/project geofencing rules configured in Friday.
Example coordinate value:
40.7128,-74.0060
If coordinates are missing or outside the allowed perimeter, the request is rejected with a clear error message.
Reading Time Records
GET /time-records
Returns a paginated list of time records sorted by start time, newest first.
| Query parameter | Purpose |
|---|---|
page, per_page | Pagination. |
employee_id | Filter by employee. |
type | Filter by work or break. |
start_after | Only records that started on or after this timestamp. |
start_before | Only records that started on or before this timestamp. |
active=true | Only currently active records. |
GET /time-records/:time_record_id
Returns one time record with additional detail fields such as coordinates, expiration, note image URLs, and employee summary.
Timetrack Reports
Use timetrack report endpoints when you need totals rather than raw records.
| Action | Endpoint |
|---|---|
| Company report | GET /timetrack/company |
| Employee report | GET /timetrack/employee/:employee_id |
| Download report | GET /timetrack/company/download |
Company and employee reports require start and end query parameters. Optional include_* parameters let you include day breakdowns, raw time records, customer reports, and project reports.
Downloads support PDF and CSV outputs.
Try It Out
Use the API Reference to test a full time tracking flow in sandbox:
- Clock in an employee
- Clock out an employee
- Create a time record
- Update a time record
- List time records
- Timetrack report — company
Common Pitfalls
- Starting a break before work — A break requires an active work record.
- Using
switchas a record type filter —switchis only accepted in the clock-in request. Stored records areworkorbreak. - Overlapping create or update — Bare historical writes that interior-overlap return
409withTIMELINE_OVERLAPand aconflictslist. Useintent: "overwrite"only after reviewing conflicts, or when your integration owns the timeline. - Overwrite vs an active record — Overwrite never force-clocks out an open record. Clock out first, or you get
TIMELINE_OVERWRITE_OPEN_RECORD. - Blind overwrite retries — Retrying with
intent: "overwrite"after a timeout can re-trim neighbors the first successful attempt already reshaped. - Multi-midnight spans — A single historical record cannot span more than one midnight in the company timezone (
TIMELINE_SPAN_TOO_LONG). Split by day. - Concurrent timeline writes — Simultaneous writes for the same employee may return
429withTIMELINE_LOCK_NOT_ACQUIRED. Retry. - Missing customer/project requirements — Some companies require customer or project selection for clock-in.
- Disabled customer or project — Disabled jobs cannot be used for new time records.
- Missing geofence coordinates — If geofencing applies, send
start_coordinatesorend_coordinatesas required. - Wrong environment — Sandbox and production keys are separate, and each key must be used with the matching base URL.
Quick Reference
| Action | Method | Path |
|---|---|---|
| List time records | GET | /time-records |
| Get time record | GET | /time-records/:time_record_id |
| Create time record | POST | /time-records |
| Bulk create | POST | /time-records/bulk |
| Update time record | PATCH | /time-records/:time_record_id |
| Clock in | POST | /time-records/clock-in |
| Clock out | POST | /time-records/clock-out |
| Company time report | GET | /timetrack/company |
| Employee time report | GET | /timetrack/employee/:employee_id |
| Download time report | GET | /timetrack/company/download |
Updated 28 days ago
