Shifts and costs
Shifts are the schedule: planned blocks of work, each owned by one department on one date. Time entries are what actually happened once a team member clocked in: work segments and breaks. Costs are the money attached to that work: labor, materials and expenses. This guide covers the read operations for all three and the filters that change what comes back. Every field is listed on the operation’s page in the API Reference.
| Operation | Request | Permission |
|---|---|---|
listShifts | GET /shifts | view_shifts |
getShift | GET /shifts/{id} | view_shifts |
listClockedShifts | GET /clocked-shifts | view_shifts |
listTimeEntries | GET /time-entries | view_shifts |
listCostsByPeriod | GET /costs | approve_time_entries |
getCostsForDay | GET /costs/day | approve_time_entries |
Paths are relative to your region’s base URL, for example https://api-us.suiteop.com/api/v1. See Authentication for permissions.
All six operations are read-only. Nothing in the API creates, assigns, clocks or approves a shift, a time entry or a cost.
Listing scheduled shifts
Section titled “Listing scheduled shifts”GET /shifts is paginated (default limit 50, maximum 1000) and ordered by date, then start time.
curl "https://api-us.suiteop.com/api/v1/shifts?dateFrom=2026-07-01&dateTo=2026-07-31&status=published" \ -H "Authorization: Bearer sk_live_your_key_here"dateFromanddateToare inclusive calendar dates such as2026-07-01, matched against the shift’s own date, not against any clock-in. A datetime is rejected.statusis one ofdraft,published,in_progress,completedorcancelled. Omit it and draft and cancelled shifts come back too. Deleted shifts never do.isOpenis a stored flag for unclaimed work someone can still pick up. It is not worked out from whether a member is assigned.departmentIdtakes an ID fromlistDepartments.
List rows carry the schedule (scheduledStart, scheduledEnd, timezone, breakMinutes, assignedMemberId) but not the hours worked. GET /shifts/{id} adds assignedMemberName and linkedTasks, the IDs of the tasks worked during the shift. A deleted shift, or one in another organization, is reported as not found.
Reading what was worked
Section titled “Reading what was worked”Hours worked come from two operations. For totals across the team, GET /costs also reports tracked minutes; see Cost totals for a period.
GET /clocked-shifts lists the shifts one member actually clocked into, oldest clock-in first. It is paginated (default limit 50, maximum 200). Pass that member’s organizationMemberId, which is the memberId from listTeamMembers, not a user ID. There is no organization-wide “who is clocked in” list, so call it once per member.
clockStatusisactive(clocked in),on_breakorcompleted(clocked out).dateFromanddateTofilter the clock-in instant in UTC, not the scheduled date.dateTocovers the whole of that UTC day.taskIdis accepted but ignored. To see which tasks a member worked, open their day withgetCostsForDay.
Clocked-shift rows carry no clock times. Read them from GET /time-entries?shiftId=…, which returns every segment recorded against one shift, oldest first, under data.items with a total. Each segment has a type of work, paid_break or unpaid_break; endTime is null while a segment is still running. shiftId is required: there is no listing by member, task, property or date range, and no paging.
Cost totals for a period
Section titled “Cost totals for a period”GET /costs adds up money already spent into one row per member per day or week. start and end are required ISO 8601 instants such as 2026-07-01T00:00:00.000Z (an offset such as +02:00 is read as the same instant in UTC, and a bare date is rejected); end is exclusive.
curl -G "https://api-us.suiteop.com/api/v1/costs" \ -H "Authorization: Bearer sk_live_your_key_here" \ --data-urlencode "start=2026-07-01T00:00:00.000Z" \ --data-urlencode "end=2026-08-01T00:00:00.000Z" \ --data-urlencode "groupBy=week" \ --data-urlencode "tz=America/New_York" \ --data-urlencode "pendingOnly=false"groupByisday(the default) orweek. Weeks start on Monday.tzis an IANA zone name. It decides which local day or week a row is counted under, andperiodStartis a bare date in that zone. It never moves the window:startandendstay absolute instants. Without it, buckets are cut at UTC midnight.memberIds,departmentIdsandpropertyIdseach take up to 200 IDs. Send several by repeating the key (memberIds=…&memberIds=…); a comma-separated list is not split and is rejected as an invalid ID. Department matching goes through the linked task, so costs with no task, such as loose materials and manual expenses, drop out when you filter by department.
Amounts are integers in minor currency units: 12500 means 125.00. Each row splits labor from non-labor (laborCostCents, nonLaborCostCents and their billable counterparts) and adds pendingCount, approvedCount and oldestPendingAt. It also carries totalDurationMinutes, the work minutes the member tracked in that period, with paidBreakMinutes and unpaidBreakMinutes alongside. The same billable money is split by who pays it into ownerBillableCents (the owner statement), guestBillableCents, recoveredBillableCents (billed another way, outside SuiteOp, not a loss), absorbedBillableCents (paid by your organization) and unroutedBillableCents. The five add up to laborBillableCents plus nonLaborBillableCents; unroutedBillableCents is normally 0, because billable rows are routed to the owner by default. A member and period only appear when they have matching cost rows, so this is not a complete timesheet. Rows are not paginated and come newest period first, so a wide window with groupBy=day returns one row for every member and day that has matching costs.
One member’s day
Section titled “One member’s day”GET /costs/day opens a single member’s single calendar day. Use GET /costs to find the days worth opening.
const params = new URLSearchParams({ memberId: '3f2b4c81-9d2e-4a7b-8c1f-5e6a7b8c9d01', date: '2026-07-15', tz: 'America/New_York',})const res = await fetch(`https://api-us.suiteop.com/api/v1/costs/day?${params}`, { headers: { Authorization: 'Bearer sk_live_your_key_here' },})const { data } = await res.json()The response has two lists:
entries: every time entry the member started that day, with the task, property and department it belongs to and the cost attributed to it. A task’s labor cost is split across its entries by duration, or equally when none has a duration, so the parts add back up to the task’s cost.orphanCosts: the day’s other cost rows, meaning materials, expenses and labor that no time entry on that day accounts for.
Both carry the cost’s routing. billTo says who pays the billable amount: owner, guest, recovered or absorbed, and null when there is nothing to bill. billToReason explains a recovered line (kept_from_booking_revenue, covered_by_management_fee, invoiced_to_owner_separately, part_of_recurring_charge, charged_to_guest) or an absorbed one (our_error, goodwill, quality_issue, insufficient_documentation), and is null otherwise. A time entry takes the routing of the cost row its money comes from: its own, or its task’s labor cost. Treat both as open strings, since new values can be added. The cost rows under costs on getTask carry the same two fields.
date is a bare date. Without tz, the day runs from UTC midnight to UTC midnight, which moves a late-evening shift onto the next day. memberId is required. The API also defines an unassigned bucket for costs attached to nobody, but a query string can’t send null, so memberId=null is rejected as an invalid UUID.