Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: HourSquare Public Attendance API
description: 'The HourSquare public API: record clock events and completed shifts, read time entries, and list employees for one company.
**Authentication.** Create an API key in HourSquare (Settings → API & integrations; you see it once). Exchange it for an access token at `https://api.hoursquare.com/api/public/v1/oauth/token` (OAuth 2.0 client credentials: `grant_type=client_credentials`, the key as `client_secret` or as the HTTP Basic password). Send the token as `Authorization: Bearer `. **Tokens live 15 minutes** — cache one and request a new one when it expires or on a 401. Never put a key or a token in a URL.
**Scopes.** A key carries the scopes it was created with; each operation below names the one it needs (`attendance.record`, `attendance.read`, `employees.read`, …). A person''s own HourSquare token may also call the operations their permissions cover. Calling without the scope returns `403 insufficient_scope`. When fetched with a token, this document lists only the operations that token may call.
**Rate limits.** 300 requests per minute per key on these resources and 60 per minute per key on the token endpoint, sliding windows, enforced per server instance (so approximately). Every response carries `RateLimit`, `RateLimit-Policy` and `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset`; a `429 rate_limited` carries `Retry-After`.
**Revocation.** Revoking a key stops new tokens immediately; tokens already issued keep working until they expire, so **within 15 minutes of Revoke every request with that key fails**.
**Errors.** Every error is `application/problem+json` (RFC 9457) with a stable snake_case `error` you can branch on (`employee_not_found`, `already_clocked_in`, `no_open_session`, `session_too_long`, `overlaps_existing_entry`, `day_locked`, `validation_failed`, `idempotency_key_reused`, `time_tracking_disabled`, `insufficient_scope`, `rate_limited`, …), the numeric `code`, a human `detail`, and a `correlationId` to quote to support. `validation_failed` (422) lists the offending fields in `errors[]`.
**Idempotency.** Send an `Idempotency-Key` header (unique per request, at most 128 characters) on writes to make retries safe: the same key replays the first response. Without it, the same employee, event type and caller within 60 seconds of an existing entry returns that entry with `200` instead of a duplicate; a later retry may see `409` instead of the original `201`.
**Dry run.** `X-HourSquare-Dry-Run: true` on any write validates and resolves everything, writes nothing, and returns the would-be result with `dryRun: true` and status 200. This document declares the header with default `true`, so a try-it panel never writes unless you flip it; the server treats an absent header as a real write.
**Time zones.** `occurredAt`, `clockIn` and `clockOut` are RFC 3339 timestamps with an explicit offset. They are recorded in the employee''s workplace time zone, else the company''s; when neither is configured the wall-clock you sent is kept. The response echoes `timeZone` (an IANA id or `offset`) and the server-resolved instant.
**Versioning.** The major version is in the path (`/api/public/v1`). Within v1 only additive changes happen: new optional fields, new endpoints, new enum values (documented as "may grow"). Breaking changes get `/v2`; v1 then carries `Deprecation` and `Sunset` headers for at least six months.
**Keep it safe.** Keys and tokens never belong in a URL, a browser page or a log; rotate a key when in doubt (the old one keeps working for the grace period you choose).'
version: v1
servers:
- url: https://api.hoursquare.com
description: HourSquare public API
tags:
- name: Attendance
paths:
/api/public/v1/time-clock/events:
post:
tags:
- Attendance
summary: Record a clock event
description: 'Records a `clock_in`, `clock_out` or `toggle` punch for one employee of your company, identified by
`employee.id` and/or `employee.externalId`.
* `occurredAt` is RFC 3339 **with an offset** (`2026-09-14T08:59:12+04:00`), at most 5 minutes in the
future (clock skew) and 30 days in the past. It is converted into the employee''s workplace time zone
(else the company''s); when neither is configured the wall-clock you sent is kept. The response echoes
which (`timeZone`: an IANA id or `offset`) and the server-resolved `occurredAt`.
* `clock_in` opens a session; `clock_out` completes the open session of that day (or the previous day''s
still-open one — an overnight shift stays one entry on the day it started, `endsNextDay: true`) and
submits it for approval; `toggle` picks whichever applies and is the recommended type for dumb clocks.
* Sessions of 24 hours or more are refused (`session_too_long`); record such shifts with `POST /time-entries`.
* Send an `Idempotency-Key` to make retries safe. Without it, the same employee, type and caller
within 60 seconds of an existing entry returns that entry with `200` instead of creating a duplicate.
* `X-HourSquare-Dry-Run: true` runs every check and resolution, writes nothing and returns the would-be
result with `dryRun: true` and status `200`.
Requires scope `attendance.record` (86).'
operationId: TimeClock_RecordEvent
parameters:
- name: X-HourSquare-Dry-Run
in: header
description: 'Validate without writing: every check and resolution runs, nothing is stored, and the response carries `dryRun: true` with status 200. Send `false` (or omit the header) to write for real.'
schema:
type: boolean
default: true
- name: Idempotency-Key
in: header
description: 'Optional, recommended. A unique key (at most 128 characters, e.g. a UUID) that makes a retry safe: the same key with the same body replays the first response verbatim (with `Idempotent-Replayed: true`), the same key with a different body is refused (`422 idempotency_key_reused`), and a key whose first request is still running answers `409 idempotency_in_flight`. Keys are kept for 24 hours.'
schema:
type: string
maxLength: 128
requestBody:
x-name: request
content:
application/json:
schema:
$ref: '#/components/schemas/TimeClockEventRequest'
required: true
x-position: 1
responses:
'201':
description: The event was recorded.
content:
application/json:
schema:
$ref: '#/components/schemas/TimeEntryWriteResponse'
'200':
description: Dry run, or a header-less dedupe hit (the existing entry).
content:
application/json:
schema:
$ref: '#/components/schemas/TimeEntryWriteResponse'
'404':
description: No such employee (or entry) in your company (`employee_not_found`, `not_found`).
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'409':
description: 'The request is well-formed but the current state refuses it — see `error`: `already_clocked_in`, `no_open_session`, `clock_out_before_clock_in`, `session_too_long`, `overlaps_existing_entry`, `day_locked`, `already_exists`, `business_rule_violation`, `idempotency_in_flight`.'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'422':
description: The request itself is wrong (`validation_failed`, with one `errors[]` item per field; or `idempotency_key_reused`).
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'401':
description: The bearer token is missing, invalid or expired (`invalid_token`). Request a new token from the token endpoint.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'403':
description: The token does not carry the required scope (`insufficient_scope`), or time tracking is switched off for the company (`time_tracking_disabled`).
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'429':
description: 'Rate limit exceeded (`rate_limited`): 300 requests per minute per key on this surface, approximately (per pod). Honour `Retry-After` and the `RateLimit` headers.'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
security:
- OAuth2:
- attendance.record
- BearerToken: []
x-required-scope: attendance.record
/api/public/v1/time-entries:
post:
tags:
- Attendance
summary: Record a completed shift
description: 'Records a shift that is already over — including an overnight one — as one time entry on the day it
started, and submits it for approval exactly like a shift logged in HourSquare.
* `clockIn` and `clockOut` are RFC 3339 **with an offset**; `clockOut` must be after `clockIn` and less
than 24 hours later; `clockIn` at most 30 days in the past, `clockOut` at most 5 minutes in the future.
* The same time-zone rule as clock events applies (workplace zone, else company zone, else the wall-clock
you sent); `timeZone` in the response says which.
* `breakMinutes` is recorded as one unpaid break in the middle of the shift. Not allowed on an overnight shift.
* `Idempotency-Key` makes retries safe; `X-HourSquare-Dry-Run: true` validates without writing.
Requires scope `attendance.record` (86).'
operationId: TimeEntries_Create
parameters:
- name: X-HourSquare-Dry-Run
in: header
description: 'Validate without writing: every check and resolution runs, nothing is stored, and the response carries `dryRun: true` with status 200. Send `false` (or omit the header) to write for real.'
schema:
type: boolean
default: true
- name: Idempotency-Key
in: header
description: 'Optional, recommended. A unique key (at most 128 characters, e.g. a UUID) that makes a retry safe: the same key with the same body replays the first response verbatim (with `Idempotent-Replayed: true`), the same key with a different body is refused (`422 idempotency_key_reused`), and a key whose first request is still running answers `409 idempotency_in_flight`. Keys are kept for 24 hours.'
schema:
type: string
maxLength: 128
requestBody:
x-name: request
content:
application/json:
schema:
$ref: '#/components/schemas/CreateTimeEntryRequest'
required: true
x-position: 1
responses:
'201':
description: The shift was recorded.
content:
application/json:
schema:
$ref: '#/components/schemas/TimeEntryWriteResponse'
'200':
description: Dry run — nothing was written.
content:
application/json:
schema:
$ref: '#/components/schemas/TimeEntryWriteResponse'
'404':
description: No such employee (or entry) in your company (`employee_not_found`, `not_found`).
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'409':
description: 'The request is well-formed but the current state refuses it — see `error`: `already_clocked_in`, `no_open_session`, `clock_out_before_clock_in`, `session_too_long`, `overlaps_existing_entry`, `day_locked`, `already_exists`, `business_rule_violation`, `idempotency_in_flight`.'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'422':
description: The request itself is wrong (`validation_failed`, with one `errors[]` item per field; or `idempotency_key_reused`).
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'401':
description: The bearer token is missing, invalid or expired (`invalid_token`). Request a new token from the token endpoint.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'403':
description: The token does not carry the required scope (`insufficient_scope`), or time tracking is switched off for the company (`time_tracking_disabled`).
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'429':
description: 'Rate limit exceeded (`rate_limited`): 300 requests per minute per key on this surface, approximately (per pod). Honour `Retry-After` and the `RateLimit` headers.'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
security:
- OAuth2:
- attendance.record
- BearerToken: []
x-required-scope: attendance.record
get:
tags:
- Attendance
summary: List an employee's time entries
description: 'Exactly one of `employeeId` / `externalId` selects the employee. `from` and `to` are inclusive work
dates (`YYYY-MM-DD`) spanning at most 92 days; by default the last 30 days. Ordered by work date, then id;
pass `nextCursor` back as `cursor` while `hasMore` is true.
Requires scope `attendance.read` (47).'
operationId: TimeEntries_List
parameters:
- name: employeeId
in: query
description: The employee's HourSquare id (send this or `externalId`).
schema:
type:
- string
- 'null'
format: guid
x-position: 1
- name: externalId
in: query
description: The employee's external id (send this or `employeeId`).
schema:
type:
- string
- 'null'
x-position: 2
- name: from
in: query
description: 'First work date, inclusive. Default: `to` minus 30 days.'
schema:
type:
- string
- 'null'
format: date
x-position: 3
- name: to
in: query
description: 'Last work date, inclusive. Default: today.'
schema:
type:
- string
- 'null'
format: date
x-position: 4
- name: cursor
in: query
description: The `nextCursor` of the previous page.
schema:
type:
- string
- 'null'
x-position: 5
- name: limit
in: query
description: Page size, 1 to 100. Default 25.
schema:
type:
- integer
- 'null'
format: int32
x-position: 6
responses:
'200':
description: One page of entries.
content:
application/json:
schema:
$ref: '#/components/schemas/ListTimeEntriesResponse'
'404':
description: No such employee (or entry) in your company (`employee_not_found`, `not_found`).
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'422':
description: The request itself is wrong (`validation_failed`, with one `errors[]` item per field; or `idempotency_key_reused`).
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'401':
description: The bearer token is missing, invalid or expired (`invalid_token`). Request a new token from the token endpoint.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'403':
description: The token does not carry the required scope (`insufficient_scope`), or time tracking is switched off for the company (`time_tracking_disabled`).
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'429':
description: 'Rate limit exceeded (`rate_limited`): 300 requests per minute per key on this surface, approximately (per pod). Honour `Retry-After` and the `RateLimit` headers.'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
security:
- OAuth2:
- attendance.read
- BearerToken: []
x-required-scope: attendance.read
components:
schemas:
ListTimeEntriesResponse:
type: object
additionalProperties: false
properties:
items:
type: array
items:
$ref: '#/components/schemas/TimeEntryListItem'
nextCursor:
type:
- string
- 'null'
hasMore:
type: boolean
TimeClockEventType:
type: string
description: "The type of a time-clock event (plan §6). toggle is the recommended type for\n dumb clocks: open session on that day (or the previous day) exists ? clock_out : clock_in."
x-enumNames:
- ClockIn
- ClockOut
- Toggle
enum:
- clock_in
- clock_out
- toggle
ProblemDetails:
type: object
description: "The wire shape. Code is the numeric ApiExceptionCodeTypes value,\n Error the stable snake_case string integrators branch on."
additionalProperties: false
properties:
type:
type: string
title:
type: string
status:
type: integer
format: int32
detail:
type:
- string
- 'null'
instance:
type:
- string
- 'null'
code:
type: integer
format: int32
error:
type: string
correlationId:
type:
- string
- 'null'
errors:
type:
- array
- 'null'
items:
$ref: '#/components/schemas/ProblemFieldError'
TimeEntryListItem:
type: object
additionalProperties: false
properties:
id:
type: string
format: guid
employee:
$ref: '#/components/schemas/EmployeeRef'
workDate:
type: string
format: date
startTime:
type: string
format: time
endTime:
type: string
format: time
endsNextDay:
type: boolean
totalHours:
type: number
format: double
breakMinutes:
type:
- integer
- 'null'
format: int32
status:
type: string
source:
type: string
approvedAt:
type:
- string
- 'null'
format: date-time
createdAt:
type: string
format: date-time
TimeEntryWriteResponse:
type: object
description: 'The 201 (200 for a dry run or a header-less dedupe hit) body of POST /time-clock/events and
POST /time-entries (plan §6). type is the EFFECTIVE type (toggle resolves to
clock_in or clock_out; a completed shift is shift). workDate / time
are the entry''s stored wall-clock values in timeZone (an IANA id, or offset when the
sent offset was kept), and occurredAt is the server-resolved instant. endsNextDay is true
for an overnight shift (the entry stays on the day it started). timeEntryId is null on a
dry-run clock-in (nothing was created).'
additionalProperties: false
properties:
timeEntryId:
type:
- string
- 'null'
format: guid
employee:
$ref: '#/components/schemas/EmployeeRef'
type:
type: string
workDate:
type: string
format: date
time:
type: string
format: time
timeZone:
type: string
occurredAt:
type: string
format: date-time
endsNextDay:
type: boolean
status:
type: string
dryRun:
type: boolean
startTime:
type:
- string
- 'null'
format: time
endTime:
type:
- string
- 'null'
format: time
totalHours:
type:
- number
- 'null'
format: double
EmployeeSelector:
type: object
description: 'How a public request names an employee (company API keys plan 2026-09-15, §6 "Employee
identification"): by HourSquare id, by the external (badge / HRIS) id, or both — every supplied
selector must match, and both are resolved inside the caller''s company only.'
additionalProperties: false
properties:
id:
type:
- string
- 'null'
description: The employee's HourSquare id.
format: guid
externalId:
type:
- string
- 'null'
description: The external id set through the Hub or PUT /employees/{id}/external-id (case-sensitive, at most 64 characters).
CreateTimeEntryRequest:
type: object
description: Body of POST /api/public/v1/time-entries — a completed shift, including overnight (plan §6).
additionalProperties: false
required:
- employee
- clockIn
- clockOut
properties:
employee:
description: The employee, by HourSquare id and/or external id.
oneOf:
- $ref: '#/components/schemas/EmployeeSelector'
clockIn:
type: string
description: 'When the shift started: RFC 3339 with an explicit offset. At most 30 days in the past.'
format: date-time
minLength: 1
clockOut:
type: string
description: 'When the shift ended: RFC 3339 with an explicit offset, after clockIn and less than 24 hours later. At most 5 minutes in the future.'
format: date-time
minLength: 1
breakMinutes:
type:
- integer
- 'null'
description: Unpaid break in minutes, recorded as one break in the middle of the shift. Not allowed on an overnight shift.
format: int32
maximum: 1440.0
minimum: 0.0
note:
type:
- string
- 'null'
description: Optional note for the manager (at most 500 characters).
maxLength: 500
TimeClockEventRequest:
type: object
description: Body of POST /api/public/v1/time-clock/events — a live punch (plan §6).
additionalProperties: false
required:
- employee
- type
- occurredAt
properties:
employee:
description: The employee, by HourSquare id and/or external id. At least one is required; both must match when both are sent.
oneOf:
- $ref: '#/components/schemas/EmployeeSelector'
type:
description: 'clock_in, clock_out or toggle (open session for that day exists ? clock_out : clock_in — the recommended type for dumb clocks).'
oneOf:
- $ref: '#/components/schemas/TimeClockEventType'
occurredAt:
type: string
description: 'When the punch happened: RFC 3339 with an explicit offset (2026-09-14T08:59:12+04:00). At most 5 minutes in the future and 30 days in the past.'
format: date-time
minLength: 1
deviceId:
type:
- string
- 'null'
description: 'Optional identifier of the device or bridge that sent the punch (at most 64 characters). Stored on the entry''s notes as [device: …].'
maxLength: 64
note:
type:
- string
- 'null'
description: Optional note for the manager (at most 500 characters).
maxLength: 500
ProblemFieldError:
type: object
description: One validation failure inside Errors.
additionalProperties: false
properties:
field:
type: string
message:
type: string
code:
type:
- string
- 'null'
EmployeeRef:
type: object
description: Every public response that names an employee carries this pair.
additionalProperties: false
properties:
id:
type: string
format: guid
externalId:
type:
- string
- 'null'
securitySchemes:
OAuth2:
type: oauth2
description: 'Client credentials (RFC 6749 §4.4): POST your API key as `client_secret` (or as the HTTP Basic password) to the token URL and use the returned token as a Bearer. Tokens live 15 minutes. Each operation lists the scope it requires; a key carries the scopes it was created with.'
flows:
clientCredentials:
tokenUrl: https://api.hoursquare.com/api/public/v1/oauth/token
scopes:
attendance.record: Record clock events and completed shifts; set employee external ids (permission 86)
attendance.read: Read time entries (permission 47)
attendance.reports: Read time reports (permission 49)
employees.read: Read employees (id, external id, name, email, status) (permission 52)
leave.read: Read leave balances and requests (permission 72)
BearerToken:
type: http
description: 'An access token from the token endpoint (`Authorization: Bearer <access_token>`), or a person''s own HourSquare session token: a person may call the read operations their permissions cover.'
scheme: bearer
bearerFormat: JWT
x-generator: NSwag v14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))