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-Key on 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"
}
FieldMeaning
typeEither work or break.
startClock-in timestamp.
endClock-out timestamp, or null while the record is active.
employee_idEmployee whose time is being tracked.
customer_idOptional customer/job associated with the time.
project_idOptional project associated with the time.
note / end_noteOptional 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"
}
FieldRequired?Notes
employee_idYesEmployee to clock in.
typeOptionalwork, break, or switch. Defaults to work.
customer_idOptionalCustomer/job to associate with the record. May be required by company settings.
project_idOptionalProject to associate with the record. May be required by company settings.
noteOptionalClock-in note.
start_coordinatesConditionalRequired when job geofencing applies. Format: latitude,longitude.
start_locationOptionalHuman-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 with code: "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 429 with code: "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:

  1. Automatically clocks out the current active work record.
  2. Creates a new work record with the new customer/project/note details.
  3. Returns the newly created time record.

Important: switch is a request action, not a stored time record type. The new record is stored and returned as type: "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"
}
FieldRequired?Notes
employee_idYesEmployee to clock out.
end_noteOptionalClock-out note.
end_coordinatesConditionalRequired when job geofencing applies to the active record.
end_locationOptionalHuman-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"

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 geometryWhat happens to the existing record
Fully covered by yours (or identical span)Deleted — including notes, photos, and job attribution
Yours covers its startTrimmed: its start moves to your record's end
Yours covers its endTrimmed: 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) then time_record.created (right remainder)
  • Delete → time_record.deleted
  • Then your record → time_record.created or time_record.updated

Refresh any local timeline cache after an overwrite.

Other timeline errors

CodeHTTPMeaning
TIMELINE_END_BEFORE_START400end is before start
TIMELINE_ZERO_DURATION400start and end are the same (at minute precision)
TIMELINE_SPAN_TOO_LONG400The record spans more than one midnight in the company timezone — split by day
TIMELINE_OPEN_NOT_ALLOWED400Historical create/update requires an end time
TIMELINE_LOCK_NOT_ACQUIRED429Another 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 parameterPurpose
page, per_pagePagination.
employee_idFilter by employee.
typeFilter by work or break.
start_afterOnly records that started on or after this timestamp.
start_beforeOnly records that started on or before this timestamp.
active=trueOnly 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.

ActionEndpoint
Company reportGET /timetrack/company
Employee reportGET /timetrack/employee/:employee_id
Download reportGET /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:


Common Pitfalls

  • Starting a break before work — A break requires an active work record.
  • Using switch as a record type filterswitch is only accepted in the clock-in request. Stored records are work or break.
  • Overlapping create or update — Bare historical writes that interior-overlap return 409 with TIMELINE_OVERLAP and a conflicts list. Use intent: "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 429 with TIMELINE_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_coordinates or end_coordinates as required.
  • Wrong environment — Sandbox and production keys are separate, and each key must be used with the matching base URL.

Quick Reference

ActionMethodPath
List time recordsGET/time-records
Get time recordGET/time-records/:time_record_id
Create time recordPOST/time-records
Bulk createPOST/time-records/bulk
Update time recordPATCH/time-records/:time_record_id
Clock inPOST/time-records/clock-in
Clock outPOST/time-records/clock-out
Company time reportGET/timetrack/company
Employee time reportGET/timetrack/employee/:employee_id
Download time reportGET/timetrack/company/download


Did this page help you?