HOURSQUARE · EST 2026 HR you run yourself.
Developer guide Guide 3 of 5

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.

sh
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):

sh
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.

sh
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"
  }'
Response
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

FieldMeaning
timeEntryIdThe entry created or completed; null on a dry run that would create one
employeeid and externalId of the resolved employee
typeThe effective event: clock_in, clock_out or shift
workDate, time, timeZoneWhere the punch landed in the employee's zone
occurredAtThe instant as the server resolved it
endsNextDaytrue when the shift crosses midnight
statusThe entry's real status: open, pending_approval, approved, draft or rejected
startTime, endTime, totalHoursFilled once the session is complete
dryRuntrue 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.

sh
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

  • occurredAt may 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.

sh
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"
Response
{
  "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.

StatuserrorWhen
404employee_not_foundNo employee in your company matches id / externalId
409already_clocked_inA clock_in while a session is open
409no_open_sessionA clock_out with nothing to close
409clock_out_before_clock_inThe clock-out instant is before the open session started
409session_too_longClosing would make the session 24 hours or longer; it stays open
409overlaps_existing_entryThe punch or shift overlaps an entry that already exists
409day_lockedThe employee has confirmed that day
409business_rule_violationThe employee has no workplace or no effective schedule
409already_existsThe external id belongs to another employee
409idempotency_in_flightThe same Idempotency-Key is still being processed
422validation_failedA field is missing or malformed; errors[] names it
422idempotency_key_reusedThe key was reused with a different request
403time_tracking_disabledTime tracking is switched off for the company
403insufficient_scopeThe token lacks the scope the operation needs
429Quota exhausted; wait Retry-After seconds
Next guideRate limits, idempotency and errors

Ready when you are

Try HourSquare for your team.

Sign up in under a minute. No card. Free for teams up to 10.

Free up to 10 employees · GDPR-native · EU-hosted