Skip to main content
Timesheets are one read across all your time-tracking records. Attendance check-ins and deliverable work sessions come back in a single shape, so payroll and reporting integrations only have to understand one row type.
Read-only. Only GET /timesheet, GET /timesheet/{id}, GET /timesheet/count, POST /timesheet/count, POST /timesheet/query, and POST /timesheet/group-and-count exist. To write time, use work sessions.
Check-in/out records carry payTypes: how that entry’s paid time is split across your pay types. Work sessions never carry one.
Search columns: type, externalSync, externalSource

Processed and original clock times

startDate and endDate are the effective times used by BuildingSwell reporting. The hours-rounding job can rewrite these boundaries after a session closes. Do not assume they are the original clock times or that a closed record will never change. rawStart and rawEnd preserve the original boundary when that side is processed. They are independent: one can be populated while the other is null. A non-null raw value means that boundary was processed, even if its value equals the effective time. There is no separate rounded flag. For original clock times, use rawStart ?? startDate and rawEnd ?? endDate. For processed hours, use startDate and endDate, accounting for unpaid breaks. Raw fields are read-only; editing time can reset the processing state. Reconcile previously fetched closed records when importing hours.

Fields

Every property the API returns for a timesheet record.

shiftWorkHours

Each entry in breaks:

Row types

Two row types exist, and which one you are looking at decides what is populated.
stageId, buildOrderId, and overheadId are always null. The build_order_work_session and overhead_work_session row types they belonged to were removed server-side, and the three fields stay in the response so existing integrations keep parsing. Attribute deliverable_work_session rows through deliverableId instead, as shown below.

Break time and how it is carved out

breakTimeInSec is the unpaid break time that falls inside a record’s own start-to-end window. Paid time for a record is therefore:
The database derives the value from the breaks array in shiftWorkHours whenever the record is written, and stores it. You cannot write it, and patching breakTimeInSec has no effect. Editing a record’s times or shift re-derives it; nothing else does.

The carve-out rule

A break only counts for the portion that overlaps the record. It is not a flat deduction, which is what makes short and split sessions come out right. Each break in shiftWorkHours.breaks has a local startTime and a durationInSec, giving it a window. That window is intersected with the record’s window, and the overlapping seconds are summed across all breaks. Worked through, with a 30-minute lunch at 12:00 and a session that runs 08:00 to 17:00: So a person who clocks out before lunch loses nothing, and one who starts mid-break loses only the remainder.

Timezones

Break times in shiftWorkHours are local wall-clock, not UTC. The shop’s IANA timezone travels on the record in shiftWorkHours.timezone, stamped when the record is written, and each break is anchored to the local calendar day of startDate. A break with no timezone on the record is anchored to UTC.
Two cases to expect:
  • An open record (endDate is null) always reports 0, because there is no window to intersect yet. It fills in when the record is closed.
  • On an overnight shift, breaks scheduled before the shift’s start time are anchored to startDate’s local day rather than rolled to the next day, so the figure can differ from what the app shows at the day boundary.

What is in metadata

metadata is a free-form key-value store. No key is guaranteed to be present, and a missing key does not mean the same thing as a null one. That distinction is the source of most confusion here, so it is worth being precise about which code path writes what. Read it defensively. Treat every key as optional, and decide what an absent key means for your integration before you rely on it: for payroll, a missing costCode usually needs to fall back to a default rather than be sent as empty.

How costCode is resolved

When a work session is created without metadata.costCode, BuildingSwell walks this chain and stops at the first cost code it finds:
  1. The deliverable itself.
  2. The station the deliverable is linked to, through data.stageId.
  3. The deliverable’s ancestors, walking up the tree.
  4. The project the deliverable belongs to.
  5. The route the deliverable was built from, through originTemplateId.
Nothing found means the key is left off the object rather than written as null. Setting a cost code on the project is the usual way to guarantee every session gets one.

perDiemCode

No BuildingSwell code path writes perDiemCode. If you see it, an external sync put it there. Check externalSource to find out which one, and treat it as that system’s field rather than an API field. The per diem code list is a separate resource and is not read here.

Example queries

Attributing hours to an order, stage, and phase

For deliverable_work_session rows, the only link into the work tree is deliverableId. To get the order, station, and phase for each row, join the deliverable and then chain further joins off it. Two rules make this work:
  • A join condition can reference another join’s alias, not just the base table, so you can hop from timesheet to deliverable to order in one request.
  • Declare joins in dependency order. The join you reference (d here) must come before the joins that depend on it (o and s).
Each row comes back with:
  • o.name and o.identifier for the order, joined through the node’s rootParentId
  • s.name for the station, joined through the node’s data.stageId
  • d.computed.phase for the phase, read straight off the joined node
The s join matches stage.id against data->>'stageId', and the engine casts both sides to uuid for you. Phase and component nodes have no linked station, so s is null on those rows. Read computed.phase for the phase either way.
Without chaining: join only the deliverable, read d.computed.phase directly, then collect the distinct d.rootParentId and d.data.stageId values and resolve their names with bulk POST /deliverable/query and POST /stage/query calls. Same answer, more round-trips.
attendance rows have no work-tree attribution at all: deliverableId, stageId, buildOrderId, and overheadId are all null.

Pay types

payTypes splits one entry’s paid time across your pay-type catalog. The keys are codes, the values are seconds, and the split is expected to add up to the entry’s own paid time.
Nothing is derived. Building Swell does not put an entry on a default pay type and does not infer overtime from hours worked, because when overtime begins varies by union and jurisdiction and payroll owns those rules. null means nobody has classified the entry yet. The split lives on the entry rather than on the day, so an entry that runs past midnight carries its own hours and its own classification. A day’s total per pay type is the sum across that day’s entries. payTypes is read-only over the API, like every other field on this resource. Pay types are set in Building Swell, on the check-in/out drawer’s Pay Types tab, and read from here — so a payroll export can resolve each entry’s split without anyone re-keying it.