{
  "x-generator": "NSwag v14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))",
  "openapi": "3.0.0",
  "info": {
    "title": "HourSquare Public API",
    "description": "The HourSquare public API: record clock events and completed shifts, read time entries, and list employees for one company.\n\n**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 <access_token>`. **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.\n\n**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.\n\n**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`.\n\n**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**.\n\n**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[]`.\n\n**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`.\n\n**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.\n\n**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.\n\n**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.\n\n**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"
    }
  ],
  "paths": {
    "/api/public/v1/employees": {
      "get": {
        "tags": [
          "Employees"
        ],
        "summary": "List employees.",
        "description": "The company's employees (never deleted ones) with the fields an integrator needs to build its badge map:\nid, external id, name, e-mail, employment status, department and position ids. Ordered by id; pass\n`nextCursor` back as `cursor` while `hasMore` is true.\n\nRequires scope `employees.read` (52).",
        "operationId": "Employees_List",
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "description": "The `nextCursor` of the previous page.",
            "schema": {
              "type": "string",
              "nullable": true
            },
            "x-position": 1
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1 to 100. Default 25.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "nullable": true
            },
            "x-position": 2
          },
          {
            "name": "status",
            "in": "query",
            "description": "Only employees with this employment status: `active`, `inactive`, `on_leave`, `suspended`, `terminated`.",
            "schema": {
              "type": "string",
              "nullable": true
            },
            "x-position": 3
          }
        ],
        "responses": {
          "200": {
            "description": "One page of employees.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListEmployeesResponse"
                }
              }
            }
          },
          "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": [
              "employees.read"
            ]
          },
          {
            "BearerToken": []
          }
        ],
        "x-required-scope": "employees.read"
      }
    },
    "/api/public/v1/employees/{id}/external-id": {
      "put": {
        "tags": [
          "Employees"
        ],
        "summary": "Set an employee's external id.",
        "description": "Sets (or, with a null / blank value, clears) the badge / HRIS id this employee is matched by in\n`employee.externalId`. At most 64 characters, compared exactly as sent (case-sensitive), unique inside\nyour company: a value another employee already carries returns `409 already_exists` naming them.\n`X-HourSquare-Dry-Run: true` checks the value without writing.\n\nRequires scope `attendance.record` (86).",
        "operationId": "Employees_SetExternalId",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The employee's HourSquare id.",
            "schema": {
              "type": "string",
              "format": "guid"
            },
            "x-position": 1
          },
          {
            "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
            }
          }
        ],
        "requestBody": {
          "x-name": "request",
          "description": "The external id to store.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetEmployeeExternalIdRequest"
              }
            }
          },
          "required": true,
          "x-position": 2
        },
        "responses": {
          "200": {
            "description": "The employee's id and its external id as stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetEmployeeExternalIdResponse"
                }
              }
            }
          },
          "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/me": {
      "get": {
        "tags": [
          "Me"
        ],
        "summary": "Describe the current token.",
        "description": "Works with any valid token, key or person: which kind of principal it is, its id (and the key's\n`hsq_` public id), the company it belongs to, the scopes it may use, when it expires, and the quota\nit is charged against (`requestsPerMinute`, per pod — so \"approximately\"). No scope required.",
        "operationId": "Me_Get",
        "responses": {
          "200": {
            "description": "The token, described.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeResponse"
                }
              }
            }
          },
          "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 is not allowed here (`forbidden`).",
            "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": []
          },
          {
            "BearerToken": []
          }
        ]
      }
    },
    "/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\n`employee.id` and/or `employee.externalId`.\n            \n* `occurredAt` is RFC 3339 **with an offset** (`2026-09-14T08:59:12+04:00`), at most 5 minutes in the\n  future (clock skew) and 30 days in the past. It is converted into the employee's workplace time zone\n  (else the company's); when neither is configured the wall-clock you sent is kept. The response echoes\n  which (`timeZone`: an IANA id or `offset`) and the server-resolved `occurredAt`.\n* `clock_in` opens a session; `clock_out` completes the open session of that day (or the previous day's\n  still-open one — an overnight shift stays one entry on the day it started, `endsNextDay: true`) and\n  submits it for approval; `toggle` picks whichever applies and is the recommended type for dumb clocks.\n* Sessions of 24 hours or more are refused (`session_too_long`); record such shifts with `POST /time-entries`.\n* Send an `Idempotency-Key` to make retries safe. Without it, the same employee, type and caller\n  within 60 seconds of an existing entry returns that entry with `200` instead of creating a duplicate.\n* `X-HourSquare-Dry-Run: true` runs every check and resolution, writes nothing and returns the would-be\n  result with `dryRun: true` and status `200`.\n\nRequires 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\nstarted, and submits it for approval exactly like a shift logged in HourSquare.\n            \n* `clockIn` and `clockOut` are RFC 3339 **with an offset**; `clockOut` must be after `clockIn` and less\n  than 24 hours later; `clockIn` at most 30 days in the past, `clockOut` at most 5 minutes in the future.\n* The same time-zone rule as clock events applies (workplace zone, else company zone, else the wall-clock\n  you sent); `timeZone` in the response says which.\n* `breakMinutes` is recorded as one unpaid break in the middle of the shift. Not allowed on an overnight shift.\n* `Idempotency-Key` makes retries safe; `X-HourSquare-Dry-Run: true` validates without writing.\n\nRequires 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\ndates (`YYYY-MM-DD`) spanning at most 92 days; by default the last 30 days. Ordered by work date, then id;\npass `nextCursor` back as `cursor` while `hasMore` is true.\n\nRequires 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",
              "format": "guid",
              "nullable": true
            },
            "x-position": 1
          },
          {
            "name": "externalId",
            "in": "query",
            "description": "The employee's external id (send this or `employeeId`).",
            "schema": {
              "type": "string",
              "nullable": true
            },
            "x-position": 2
          },
          {
            "name": "from",
            "in": "query",
            "description": "First work date, inclusive. Default: `to` minus 30 days.",
            "schema": {
              "type": "string",
              "format": "date",
              "nullable": true
            },
            "x-position": 3
          },
          {
            "name": "to",
            "in": "query",
            "description": "Last work date, inclusive. Default: today.",
            "schema": {
              "type": "string",
              "format": "date",
              "nullable": true
            },
            "x-position": 4
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `nextCursor` of the previous page.",
            "schema": {
              "type": "string",
              "nullable": true
            },
            "x-position": 5
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1 to 100. Default 25.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "nullable": true
            },
            "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"
      }
    },
    "/api/public/v1/oauth/token": {
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "Exchange an API key for an access token.",
        "description": "OAuth 2.0 client credentials (RFC 6749 §4.4). Send `grant_type=client_credentials` as `application/x-www-form-urlencoded` and the key either as the `client_secret` field or as the HTTP Basic password (`Authorization: Basic base64(hsq_xxxxxxxx:<key>)`). The token lives 15 minutes (`expires_in`, seconds): cache it and request a new one when it elapses or on a 401. This endpoint is served by the identity service under the same host; it allows 60 requests per minute per key. Every response is `Cache-Control: no-store`.",
        "operationId": "OAuth_Token",
        "requestBody": {
          "description": "OAuth 2.0 client-credentials request (RFC 6749 §4.4.2).",
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "grant_type"
                ],
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "description": "Always `client_credentials`.",
                    "enum": [
                      "client_credentials"
                    ]
                  },
                  "client_id": {
                    "type": "string",
                    "description": "Optional. The key's public id, with or without the `hsq_` prefix. When present it must match the key."
                  },
                  "client_secret": {
                    "type": "string",
                    "description": "The whole key (`hsq_…`). Omit it when the key travels as the HTTP Basic password instead. Never send it in the query string."
                  }
                }
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The access token. `token_type` is always `Bearer`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthTokenResponse"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` (unreadable body, a secret in the query string, two authentication mechanisms, a repeated parameter) or `unsupported_grant_type` (anything but `client_credentials`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "`invalid_client`: unknown key, wrong secret, or a key that is revoked, expired, outside its IP allowlist, or whose company has API access switched off (`error_description` says which once possession is proved).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ListEmployeesResponse": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EmployeeListItem"
            }
          },
          "nextCursor": {
            "type": "string",
            "nullable": true
          },
          "hasMore": {
            "type": "boolean"
          }
        }
      },
      "EmployeeListItem": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "format": "guid"
          },
          "externalId": {
            "type": "string",
            "nullable": true
          },
          "firstName": {
            "type": "string",
            "nullable": true
          },
          "lastName": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "departmentId": {
            "type": "string",
            "format": "guid",
            "nullable": true
          },
          "positionId": {
            "type": "string",
            "format": "guid",
            "nullable": true
          }
        }
      },
      "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",
            "nullable": true
          },
          "instance": {
            "type": "string",
            "nullable": true
          },
          "code": {
            "type": "integer",
            "format": "int32"
          },
          "error": {
            "type": "string"
          },
          "correlationId": {
            "type": "string",
            "nullable": true
          },
          "errors": {
            "type": "array",
            "nullable": true,
            "items": {
              "$ref": "#/components/schemas/ProblemFieldError"
            }
          }
        }
      },
      "ProblemFieldError": {
        "type": "object",
        "description": "One validation failure inside Errors.",
        "additionalProperties": false,
        "properties": {
          "field": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "SetEmployeeExternalIdResponse": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "format": "guid"
          },
          "externalId": {
            "type": "string",
            "nullable": true
          },
          "dryRun": {
            "type": "boolean"
          }
        }
      },
      "SetEmployeeExternalIdRequest": {
        "type": "object",
        "description": "Body of PUT /api/public/v1/employees/{id}/external-id.",
        "additionalProperties": false,
        "properties": {
          "externalId": {
            "type": "string",
            "description": "The badge / HRIS id to match this employee by (at most 64 characters, case-sensitive, unique per company). Null or blank clears it.",
            "maxLength": 64,
            "nullable": true
          }
        }
      },
      "MeResponse": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "principal": {
            "type": "string"
          },
          "id": {
            "type": "string",
            "format": "guid"
          },
          "publicId": {
            "type": "string",
            "nullable": true
          },
          "companyId": {
            "type": "string",
            "format": "guid"
          },
          "scopes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MeScope"
            }
          },
          "tokenExpiresAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "rateLimit": {
            "$ref": "#/components/schemas/MeRateLimit"
          },
          "dryRunHeader": {
            "type": "string"
          }
        }
      },
      "MeScope": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string"
          },
          "permission": {
            "type": "integer",
            "format": "int32"
          },
          "label": {
            "type": "string"
          }
        }
      },
      "MeRateLimit": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "requestsPerMinute": {
            "type": "integer",
            "format": "int32"
          },
          "tokenRequestsPerMinute": {
            "type": "integer",
            "format": "int32"
          }
        }
      },
      "TimeEntryWriteResponse": {
        "type": "object",
        "description": "The 201 (200 for a dry run or a header-less dedupe hit) body of POST /time-clock/events and\nPOST /time-entries (plan §6). type is the EFFECTIVE type (toggle resolves to\nclock_in or clock_out; a completed shift is shift). workDate / time\nare the entry's stored wall-clock values in timeZone (an IANA id, or offset when the\nsent offset was kept), and occurredAt is the server-resolved instant. endsNextDay is true\nfor an overnight shift (the entry stays on the day it started). timeEntryId is null on a\ndry-run clock-in (nothing was created).",
        "additionalProperties": false,
        "properties": {
          "timeEntryId": {
            "type": "string",
            "format": "guid",
            "nullable": true
          },
          "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",
            "format": "time",
            "nullable": true
          },
          "endTime": {
            "type": "string",
            "format": "time",
            "nullable": true
          },
          "totalHours": {
            "type": "number",
            "format": "double",
            "nullable": true
          }
        }
      },
      "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",
            "nullable": true
          }
        }
      },
      "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",
            "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,
            "nullable": true
          },
          "note": {
            "type": "string",
            "description": "Optional note for the manager (at most 500 characters).",
            "maxLength": 500,
            "nullable": true
          }
        }
      },
      "EmployeeSelector": {
        "type": "object",
        "description": "How a public request names an employee (company API keys plan 2026-09-15, §6 \"Employee\nidentification\"): by HourSquare id, by the external (badge / HRIS) id, or both — every supplied\nselector must match, and both are resolved inside the caller's company only.",
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "description": "The employee's HourSquare id.",
            "format": "guid",
            "nullable": true
          },
          "externalId": {
            "type": "string",
            "description": "The external id set through the Hub or PUT /employees/{id}/external-id (case-sensitive, at most 64 characters).",
            "nullable": true
          }
        }
      },
      "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"
        ]
      },
      "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",
            "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,
            "nullable": true
          },
          "note": {
            "type": "string",
            "description": "Optional note for the manager (at most 500 characters).",
            "maxLength": 500,
            "nullable": true
          }
        }
      },
      "ListTimeEntriesResponse": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TimeEntryListItem"
            }
          },
          "nextCursor": {
            "type": "string",
            "nullable": true
          },
          "hasMore": {
            "type": "boolean"
          }
        }
      },
      "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",
            "format": "int32",
            "nullable": true
          },
          "status": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "approvedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OAuthTokenResponse": {
        "type": "object",
        "description": "RFC 6749 §5.1 token response, as Identity's token endpoint returns it (documentation-only type).",
        "additionalProperties": false,
        "properties": {
          "access_token": {
            "type": "string"
          },
          "token_type": {
            "type": "string"
          },
          "expires_in": {
            "type": "integer",
            "format": "int32"
          }
        }
      },
      "OAuthErrorResponse": {
        "type": "object",
        "description": "RFC 6749 §5.2 error response (documentation-only type).",
        "additionalProperties": false,
        "properties": {
          "error": {
            "type": "string"
          },
          "error_description": {
            "type": "string"
          }
        }
      }
    },
    "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"
      }
    }
  },
  "tags": [
    {
      "name": "OAuth",
      "description": "Turning an API key into a short-lived access token."
    }
  ]
}