Skip to content
Dashboard

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.

OperationRequestPermission
listShiftsGET /shiftsview_shifts
getShiftGET /shifts/{id}view_shifts
listClockedShiftsGET /clocked-shiftsview_shifts
listTimeEntriesGET /time-entriesview_shifts
listCostsByPeriodGET /costsapprove_time_entries
getCostsForDayGET /costs/dayapprove_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.

GET /shifts is paginated (default limit 50, maximum 1000) and ordered by date, then start time.

Terminal window
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"
  • dateFrom and dateTo are inclusive calendar dates such as 2026-07-01, matched against the shift’s own date, not against any clock-in. A datetime is rejected.
  • status is one of draft, published, in_progress, completed or cancelled. Omit it and draft and cancelled shifts come back too. Deleted shifts never do.
  • isOpen is a stored flag for unclaimed work someone can still pick up. It is not worked out from whether a member is assigned.
  • departmentId takes an ID from listDepartments.

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.

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.

  • clockStatus is active (clocked in), on_break or completed (clocked out).
  • dateFrom and dateTo filter the clock-in instant in UTC, not the scheduled date. dateTo covers the whole of that UTC day.
  • taskId is accepted but ignored. To see which tasks a member worked, open their day with getCostsForDay.

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.

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.

Terminal window
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"
  • groupBy is day (the default) or week. Weeks start on Monday.
  • tz is an IANA zone name. It decides which local day or week a row is counted under, and periodStart is a bare date in that zone. It never moves the window: start and end stay absolute instants. Without it, buckets are cut at UTC midnight.
  • memberIds, departmentIds and propertyIds each 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.

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.