What the labour dashboard shows, where the numbers come from, and exactly what is still missing. Written so anyone picking this up cold can tell the difference between a number we trust, a number we are waiting on, and a store that looks broken but is not.
All six calls below were made one after another on September 19, 2026, on the same credential and the same store. This is the whole argument in one table: made-up paths fail differently from blocked paths.
| Path | Code | Response | Means |
|---|---|---|---|
| /labour/this-path-does-not-exist | 404 | HTML error page | this is what a wrong path looks like |
| /totally-made-up-endpoint | 404 | HTML error page | same |
| /labour/employee | 403 | Insufficient permissions | Push's own recommendation - exists, closed to us |
| /analytics/summary/labour-actuals | 403 | Insufficient permissions | exists, closed to us |
| /analytics/summary/labour-scheduled | 403 | Insufficient permissions | exists, closed to us |
| /shifts | 200 | returned 2 shifts | works, and proves the credential is live |
If our paths were wrong they would sit in the 404 rows. They sit in the 403 rows, alongside the endpoint Push told us to use.
Live at tools.centilio.com/labour, behind a one-time-code sign-in. Sales and gross profit come from Cova, labour hours from Push Operations.
Checked live against the Sessions credential on September 18, 2026, across all 47 company IDs. The result is identical on every store, which is what makes this a scope on the credential rather than a per-store setting.
| Endpoint | Status | What it carries |
|---|---|---|
| /companies | works | 49 Sessions companies with their IDs. Two are not stores: a head-office record, and Coverdale, which is not open in Cova yet. |
| /shifts | works | The schedule, with real shift start and end times. This is what makes scheduled-by-hour possible. No money. |
| /clocks | works | The punches: clock in, clock out, approved, regular, overtime, doubletime, breaks. No money. |
| /analytics/summary/labour-actuals | refused | Labour cost by day and department. One of only two places Push publishes dollars. |
| /analytics/summary/labour-scheduled | refused | Scheduled labour cost by day and department. The other one. |
| /departments | refused | Department names. Not blocking anything on its own. |
There is no pay rate anywhere else in the API. Roughly 50 candidate endpoint paths were tried - wages, payroll, positions, rates, compensation - and none of them exist. Every valid data expansion on the two working endpoints was also checked: 55 distinct fields between them, not one of which is money.
Every call below was made against the live Sessions credential on September 18, 2026, store 22936 (Brantford). Employee and record IDs are redacted because this page is readable by the whole team; nothing else is altered. This is the reference for anyone integrating against Push or making the case to them.
GET /companies HTTP 200 { "data": [ { "id": 22915, "uuid": "<uuid redacted>", "name": "Carlisle - Sessions Cannabis Retail Inc", "legalName": "Sessions Cannabis Retail Inc - Carlisle", "address": "278 Carlisle Road", "city": "Carlisle", "provinceState": "ON", "country": "Canada", "storeNumber": null }, "...48 more" ] }
49 companies returned. Two of them are not stores: a head-office record, and Coverdale, which is not open in Cova yet.
GET /shifts?company=22936&start=2026-09-17&end=2026-09-17 HTTP 200 { "data": [ { "id": "<redacted>", "presetId": "<redacted>", "employeeId": "<redacted>", "companyId": "<redacted>", "positionId": 96889, "businessDate": "2026-09-17", "shiftChangeId": 0, "shiftChangeUserId": 0, "shiftChangePickupUserId": 0, "userChangeApproved": false, "date": { "start": "2026-09-17 09:00:00", "end": "2026-09-17 16:00:00" }, "time": { "start": { "hour": 9, "minute": 0, "formatted": "9:00AM" }, "end": { "hour": 16, "minute": 0, "formatted": "4:00PM" } }, "comments": null, "breakMinutes": 0, "tagId": "<redacted>", "published": true } ], "meta": null }
The important field is date: real shift start and end times. This is what makes scheduled-by-hour possible. There is no money field anywhere in this response.
GET /shifts?company=22936&start=2026-09-17&end=2026-09-17&include=position HTTP 200 { "position": { "id": 96889, "name": "Manager", "companyId": 22936, "showEndTimes": true } }
Five positions exist across the network. Note there is no rate on the position object - only a name.
GET /clocks?company=22936&start=2026-09-17&end=2026-09-17 HTTP 200 { "data": [ { "id": "<redacted>", "employeeId": "<redacted>", "employeePositionId": "<redacted>", "businessDate": "2026-09-17", "clockIn": "2026-09-17 08:58:34", "clockOut": "2026-09-17 15:57:21", "hoursApproved": 6.9833333333333, "breakMinutes": 15, "isApproved": true, "isCurrent": 0, "isAutoClockout": 0, "clockInComments": "", "clockOutComments": "", "managerComments": "", "approvedTime": "Approved: 6H 44M ", "regularHours": 6.73, "overtimeHours": 0, "doubletimeHours": 0, "overtimeWeekly": 0, "statutoryHolidayWork": 0, "createdAt": "2026-09-17 12:58:36", "updatedAt": "2026-09-18 15:18:39", "breakTypes": [ { "id": 991, "description": "Break", "durationMinutes": 30, "start": "2026-09-17 08:58:00", "end": "2026-09-17 08:57:00" } ] } ], "meta": { "page": { "previous": null, "current": 1, "next": null, "pageCount": 2, "totalCount": 2, "totalPages": 1 } } }
Paid time is regularHours + overtimeHours + doubletimeHours, net of break. Not hoursApproved, which is gross clock time with the break still in it, and which is -1 while a punch awaits approval. The meta.page block is how paging works: 100 rows per page, follow next.
GET /analytics/summary/labour-actuals?company=22936&start=2026-09-17&end=2026-09-17&vacation=true&earnings-deductions=true HTTP 403 { "status": "failed", "message": "Insufficient permissions" }
One of only two endpoints in the whole API that carries labour dollars.
GET /analytics/summary/labour-scheduled?company=22936&start=2026-09-17&end=2026-09-17&vacation=true HTTP 403 { "status": "failed", "message": "Insufficient permissions" }
The other one.
GET /departments?company=22936 HTTP 403 { "status": "failed", "message": "Insufficient permissions" }
Department names only. Not blocking anything on its own.
From Push's own API documentation, not from our account. This is the shape we are waiting for - totalCosts and a per-department cost for each date.
GET /analytics/summary/labour-scheduled?company=12345&start=2022-02-01&end=2022-02-02&vacation=true HTTP 200 { "data": { "companyId": 12345, "totalCosts": 1233.23, "totalHours": 65.6, "labourActualByDate": [ { "date": "2022-02-01", "costs": 139.29, "hours": 5.71, "departmentId": 7328, "departmentName": "FOH-S" }, "..." ] } }
Raised with Push under the existing support ticket for the Sessions API credential. Decision on our side, September 18: we want exact rates from Push rather than approximating from a rate card, so this waits on the permission.
Worth reading before querying a figure, because several of these are not the obvious definition.
| Figure | How it is calculated |
|---|---|
| Worked hours | Regular plus overtime plus doubletime, net of break. Not Push's hoursApproved field, which is gross clock time with the break still in it, and which comes back as -1 while a punch is awaiting approval. Summing that field produces negative labour days. |
| Approved vs pending | Kept apart on purpose. A day nobody has signed off would otherwise read as a quiet day rather than an unapproved one. |
| Hours by hour of day | Each shift and each punch is spread across the hours it covers. Breaks come out proportionally, because Push gives the length of a break but not when it was taken. The buckets are scaled so they always add up to the day total. |
| Today | Shown as "day in progress" with no variance. Anyone currently on shift has no clock-out yet, so their hours count as zero - a live afternoon reads about 8% of schedule where a finished day reads about 88%. The count of people still clocked in is shown instead. |
| Franchises | Budget yes, actual no. A franchise is not on Push, so there is no spend to show. The budget is still real and a franchisee can be held to it. |
These are the ones that will prompt a question. None of them are a fault in the dashboard, and none are fixed by the cost permission.
| Store | What you will see | Why |
|---|---|---|
| Sarnia | No labour data at all | Mapped correctly and the company exists in Push, but it has never had a single punch or shift, going back through 2024. It trades normally in Cova, so the store is open - it is simply not being used in Push. Needs someone on the operations side to check the setup. |
| London (Wellington) | Nothing after September 9 | Fine until then, then stopped. Same category as Sarnia: a Push setup question, not a data question. |
| Coverdale | Absent entirely | Exists in Push but is not open in Cova yet, so there is nothing to map it to. It will appear on its own once it trades. |
| The 8 franchises | Budget, no actual | By design. Not on Push. |
Labour data is the most sensitive thing either dashboard touches, so the rule is that personal detail never leaves the point of collection.
The refresh runs on its own twice a day, early morning and early afternoon, so the page is never a day stale for anyone looking at it after lunch. Each run pulls the recent Push days, refreshes Cova sales, and pushes both to the server.
It is written to survive being unattended: a failure in the Push step still lets the sales side refresh, on the grounds that half fresh beats a page that quietly stopped updating. Every run is logged.