Time clock walkthrough
This is the end-to-end path for a hardware time clock, a kiosk or any script that records working time. It needs a key with attendance.record, plus attendance.read to reconcile.
1. Give employees a badge id
A punch identifies the employee either by HourSquare id or by an external id: the value your device already knows, such as a badge number, a PIN or a card UID. Set it once per employee.
curl -s -X PUT https://api.hoursquare.com/api/public/v1/employees/0f8fad5b-d9cb-469f-a165-70867728950e/external-id \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"externalId":"B-1042"}'External ids are unique within the company. If another employee already has B-1042 you get 409 already_exists, and the detail names who holds it. Send null to clear one. To find employee ids in the first place, list them (needs employees.read):
curl -s "https://api.hoursquare.com/api/public/v1/employees?limit=100&status=active" \
-H "Authorization: Bearer $TOKEN"2. Record a punch
POST /api/public/v1/time-clock/events takes one event. type is clock_in, clock_out or toggle; toggle clocks out when the employee has an open session and clocks in otherwise, which is what a single-button device wants.
curl -s -i -X POST https://api.hoursquare.com/api/public/v1/time-clock/events \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"employee": { "externalId": "B-1042" },
"type": "toggle",
"occurredAt": "2026-09-14T17:31:05+02:00",
"deviceId": "front-door-1"
}'HTTP/1.1 201 Created
Content-Type: application/json
RateLimit-Policy: apikey;q=300;w=60
RateLimit: limit=300, remaining=298, reset=60
{
"timeEntryId": "c1a4e0b2-5f7d-4a1e-9b3c-2d6f8e0a1b2c",
"employee": { "id": "0f8fad5b-d9cb-469f-a165-70867728950e", "externalId": "B-1042" },
"type": "clock_out",
"workDate": "2026-09-14",
"time": "17:31:05",
"timeZone": "Europe/Berlin",
"occurredAt": "2026-09-14T17:31:05+02:00",
"endsNextDay": false,
"status": "pending_approval",
"dryRun": false,
"startTime": "08:59:12",
"endTime": "17:31:05",
"totalHours": 8.53
}employee takes id or externalId; one is enough, and if you send both they must point at the same person. occurredAt is an RFC 3339 timestamp and must carry a UTC offset; a timestamp without one is rejected with 422. deviceId (up to 64 characters) and note (up to 500) are optional and are kept on the entry.
Time zones
The server places occurredAt in the employee's zone: the workplace's time zone if the employee has one, otherwise the company's, otherwise the wall-clock time exactly as you sent it. The response echoes the resolved workDate, time, timeZone and occurredAt, so you can see what was recorded. Send the real instant with its offset and let the server do the placing.
The response
| Field | Meaning |
|---|---|
timeEntryId | The entry created or completed; null on a dry run that would create one |
employee | id and externalId of the resolved employee |
type | The effective event: clock_in, clock_out or shift |
workDate, time, timeZone | Where the punch landed in the employee's zone |
occurredAt | The instant as the server resolved it |
endsNextDay | true when the shift crosses midnight |
status | The entry's real status: open, pending_approval, approved, draft or rejected |
startTime, endTime, totalHours | Filled once the session is complete |
dryRun | true when nothing was written |
Every key is always present, with null where it does not apply.
3. Completed and overnight shifts
When your system already knows both ends of a shift, post it in one call to POST /api/public/v1/time-entries. Overnight shifts are fine: the entry is dated by the clock-in day and endsNextDay is set.
curl -s -X POST https://api.hoursquare.com/api/public/v1/time-entries \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"employee": { "externalId": "B-1042" },
"clockIn": "2026-09-13T22:00:00+02:00",
"clockOut": "2026-09-14T06:00:00+02:00",
"note": "night shift"
}'breakMinutes records one unpaid break in the middle of the shift; it cannot be combined with an overnight shift. A shift of 24 hours or more is rejected, and the net working time must be at least 15 minutes.
4. Limits on backdating
occurredAtmay be at most 5 minutes in the future, to absorb clock skew between your device and our servers.- It may be at most 30 days in the past. Older corrections go through the Hub.
- A day the employee has already confirmed is locked: 409
day_locked. - A punch that lands inside an existing entry is refused: 409
overlaps_existing_entry.
5. Reconcile
With attendance.read, list what was recorded for an employee and compare it with your device log.
curl -s "https://api.hoursquare.com/api/public/v1/time-entries?externalId=B-1042&from=2026-09-01&to=2026-09-14&limit=50" \
-H "Authorization: Bearer $TOKEN"{
"items": [
{
"id": "c1a4e0b2-5f7d-4a1e-9b3c-2d6f8e0a1b2c",
"employee": { "id": "0f8fad5b-d9cb-469f-a165-70867728950e", "externalId": "B-1042" },
"workDate": "2026-09-14",
"startTime": "08:59:12",
"endTime": "17:31:05",
"endsNextDay": false,
"totalHours": 8.53,
"breakMinutes": 0,
"status": "pending_approval",
"source": "api",
"approvedAt": null,
"createdAt": "2026-09-14T06:59:14+00:00"
}
],
"nextCursor": "MjAyNi0wOS0xNHxjMWE0ZTBiMg",
"hasMore": true
}from and to are dates; the range may span up to 92 days and defaults to the last 30. Pages hold limit items (1 to 100, default 25); follow nextCursor until hasMore is false. Each item carries id, employee, workDate, startTime, endTime, endsNextDay, totalHours, breakMinutes, status, source, approvedAt and createdAt.
Error codes for these endpoints
All errors are application/problem+json; match on the error string, never on the text.
| Status | error | When |
|---|---|---|
| 404 | employee_not_found | No employee in your company matches id / externalId |
| 409 | already_clocked_in | A clock_in while a session is open |
| 409 | no_open_session | A clock_out with nothing to close |
| 409 | clock_out_before_clock_in | The clock-out instant is before the open session started |
| 409 | session_too_long | Closing would make the session 24 hours or longer; it stays open |
| 409 | overlaps_existing_entry | The punch or shift overlaps an entry that already exists |
| 409 | day_locked | The employee has confirmed that day |
| 409 | business_rule_violation | The employee has no workplace or no effective schedule |
| 409 | already_exists | The external id belongs to another employee |
| 409 | idempotency_in_flight | The same Idempotency-Key is still being processed |
| 422 | validation_failed | A field is missing or malformed; errors[] names it |
| 422 | idempotency_key_reused | The key was reused with a different request |
| 403 | time_tracking_disabled | Time tracking is switched off for the company |
| 403 | insufficient_scope | The token lacks the scope the operation needs |
| 429 | — | Quota exhausted; wait Retry-After seconds |