Rate limits, idempotency and errors
The parts of the API that make an integration behave well when things go wrong: what to do on 429, how to retry a write safely, how to test without writing, how errors look, how lists page, and which changes we promise never to make inside v1.
Rate limits
Each key may make 300 requests a minute on resource endpoints and 60 a minute on the token endpoint. Limits are counted per server instance, so treat them as approximate rather than exact. Every response carries the current state:
RateLimit-Policy: apikey;q=300;w=60
RateLimit: limit=300, remaining=298, reset=60
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 298
X-RateLimit-Reset: 60When you exceed the quota you get 429 with Retry-After: 60. Back off for that long, then continue; do not retry in a tight loop. A time clock that sends punches as they happen or a nightly sync should never come near these numbers, and the essentials panel in the reference shows the remaining quota of your last call.
HTTP/1.1 429 Too Many Requests
Retry-After: 60Idempotency
Networks drop responses. To make a write safe to retry, send an Idempotency-Key header, any unique string up to 128 characters (a UUID is ideal), on POST /time-clock/events and POST /time-entries.
- The first request with a key runs normally and its result is stored for 24 hours, including a 4xx error.
- A retry with the same key and the same body returns the stored status and body verbatim, with
Idempotent-Replayed: true. Nothing runs twice. - The same key with a different body is refused with 422
idempotency_key_reused. - A retry that arrives while the first request is still running gets 409
idempotency_in_flight; wait a moment and retry. - Keys are scoped to your company and your API key, so two integrations cannot collide.
HTTP/1.1 201 Created
Idempotent-Replayed: trueWithout the header there is still a safety net: a punch for the same employee, from the same key, within 60 seconds of an identical one is treated as a duplicate and returns the existing entry with 200 instead of creating a second one. That covers a double tap on a device. For anything you retry programmatically, send the header.
Dry run
Send X-HourSquare-Dry-Run: true on any write and the server runs every check and resolution, writes nothing, and answers 200 with "dryRun": true. Use it to validate a badge mapping, to test your error handling, or to see where a punch would land before going live. An absent header is a real write. The interactive reference sends the header by default, so a click there never changes data until you switch it off.
curl -s -X POST https://api.hoursquare.com/api/public/v1/time-clock/events \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'X-HourSquare-Dry-Run: true' \
-d '{"employee":{"externalId":"B-1042"},"type":"clock_in","occurredAt":"2026-09-14T08:59:12+02:00"}'Errors
Errors are RFC 9457 problem details, served as application/problem+json:
{
"type": "https://hoursquare.com/problems/already-clocked-in",
"title": "Already clocked in",
"status": 409,
"detail": "Employee B-1042 is already clocked in since 2026-09-14 08:59. Send clock_out first.",
"instance": "/api/public/v1/time-clock/events",
"code": 2904,
"error": "already_clocked_in",
"correlationId": "0HN7PQ4K2M3VL:00000007"
}{
"type": "https://hoursquare.com/problems/validation-failed",
"title": "Validation failed",
"status": 422,
"detail": "One or more fields are invalid.",
"instance": "/api/public/v1/time-clock/events",
"code": 1800,
"error": "validation_failed",
"correlationId": "0HN7PQ4K2M3VL:00000009",
"errors": [
{ "field": "occurredAt", "message": "Must be an RFC 3339 timestamp with a UTC offset." }
]
}| Field | Meaning |
|---|---|
type | A URL that identifies the class of problem |
title | Short and human-readable; English |
status | The HTTP status, repeated |
detail | What went wrong in this instance; the wording may change |
instance | The request path |
code | A numeric code; stable |
error | A stable snake_case string: the thing to match on |
correlationId | Quote it when you contact support |
errors[] | On 422, field and message per invalid field |
Authentication failures use the same shape: 401 invalid_token (with a WWW-Authenticate: Bearer header) when the token is missing, expired or invalid, and 403 insufficient_scope when it lacks a scope. As a rule: 404 not found, 409 conflicts with the current state, 422 invalid input, 403 not allowed, 429 rate limited, 500 our fault.
Pagination
Lists are cursor-based. Ask for up to limit items (1 to 100, default 25); the response has items, hasMore and nextCursor. Pass nextCursor back as cursor to get the next page. Cursors are opaque strings: do not parse or build them, and do not expect offsets.
curl -s "https://api.hoursquare.com/api/public/v1/employees?limit=100" \
-H "Authorization: Bearer $TOKEN"
# → { "items": [ … 100 … ], "nextCursor": "MDFKN1Q4…", "hasMore": true }
curl -s "https://api.hoursquare.com/api/public/v1/employees?limit=100&cursor=MDFKN1Q4…" \
-H "Authorization: Bearer $TOKEN"
# → { "items": [ … 37 … ], "nextCursor": null, "hasMore": false }Versioning and deprecation
The major version is in the path: /api/public/v1. Inside v1 we only make additive changes: new optional fields, new endpoints, new enum values (documented as "may grow"). Write your client to ignore fields it does not know and to tolerate new enum values.
A breaking change means a /v2. When that happens, v1 keeps working and answers with Deprecation and Sunset headers for at least six months before it is switched off, and the change is announced on this page.
Deprecation: true
Sunset: Sat, 01 May 2027 00:00:00 GMT
Link: <https://hoursquare.com/developers/reliability>; rel="deprecation"