Skip to main content

API Conventions

Base URL and Versioning​

All routes are served under the /api prefix with URI versioning. The default version is v1, so a typical endpoint looks like:

GET /api/v1/shifts

A few infrastructure and public routes are version-neutral and have no /v1 segment:

RoutePurpose
/api/health, /api/health/ready, /api/health/backupLiveness, readiness and backup freshness
/api/calendar/:token.icsEmployee calendar subscription feed
/api/public-statsAggregate usage statistics for the marketing site

Note that the BI feed is versioned: it lives at /api/v1/bi/..., not /api/bi/....

In non-production environments the OpenAPI (Swagger) UI is available at /api/docs.

Authentication​

The web app authenticates with JWTs delivered as HTTP-only cookies:

  • POST /api/v1/auth/login issues a short-lived access token (15 minutes) and a refresh token (30 days).
  • POST /api/v1/auth/refresh rotates the refresh token. The token can be read from the cookie or sent in the body.
  • Every route requires a valid access token unless it is explicitly marked public. Machine-to-machine integrations (BI feed, public stats) use their own static bearer tokens instead — see the relevant pages.

Refresh-token rotation grace window​

Refresh tokens rotate on every use, and replaying an already-rotated token is normally treated as token theft (all of the user's sessions are revoked). Because an installed PWA and a browser tab share one cookie jar, two contexts can legitimately refresh at nearly the same moment. To avoid logging those users out:

  • A token that was revoked by rotation and is replayed within REFRESH_ROTATION_GRACE_SECONDS (default 60) is treated as a benign race: a fresh session is issued and nothing is revoked.
  • Replays outside the window, or replays of tokens revoked for other reasons (logout, password change, role change), still revoke every session.
  • Setting REFRESH_ROTATION_GRACE_SECONDS=0 restores strict rotation without a code change.

On startup the web client only logs the user out on a definitive 401; network errors and 5xx responses are retried instead.

Rate Limiting​

A global limit of 60 requests per 60 seconds applies to every route unless a route overrides it. The limit is tracked per caller by EmployeeThrottlerGuard:

  • Authenticated requests are counted per employee (or per user, for logins without an employee record). Many devices behind one venue's shared internet connection therefore no longer share a single bucket.
  • Unauthenticated requests are counted per real client IP. The API runs behind a reverse proxy with Fastify trustProxy enabled, so the IP comes from X-Forwarded-For.

Stricter per-route limits:

RoutesLimit
Auth (login, refresh, forgot-password, reset-password, accept-invite, change-password)5 / minute
Kiosk (/kiosk/*)10 / minute
Public stats10 / minute
Calendar ICS feed60 / minute

Booking webhooks are exempt from throttling. A throttled request returns 429 Too Many Requests.

Clock Events and Offline Sync​

Devices generate a UUID eventId for every clock event and may submit them later in a batch. POST /api/v1/clock-events/batch:

  • Returns a bare array of per-event results (not an object wrapper). Match results to your events by eventId, since the server may regroup them.
  • Deduplicates on eventId. A replayed event returns a duplicate result instead of creating a second record.
  • Processes each event in its own savepoint, so one rejected event does not roll back the others.
  • Enforces the same authorization as the single-event endpoint: employees may only submit their own events; supervisors and above may submit on behalf of others in scope.

Clients should retry network errors, 429 and 5xx with backoff, and treat 4xx responses and per-event errors as final. The web app shows those final failures to the employee.

Conventions Worth Knowing​

  • Timestamps are ISO 8601 with a timezone. Calendar-day logic (payroll days, BI buckets) uses Europe/Helsinki.
  • List limits are validated server-side (@Max on the query DTO, typically 100–500 depending on the resource). A limit above the maximum is rejected with 400, not silently clamped.
  • CSV exports use the Finnish convention: ; as the delimiter, , as the decimal separator and a UTF-8 BOM. The Netvisor workday CSV and the employee import template are exceptions.
  • Shift cancellation (POST /api/v1/shifts/:id/cancel) requires a category. Only employee_travelled_or_arrived qualifies for TES cancellation compensation.
  • Employee numbers are assigned by the server as sequential integers. Any client-supplied value is ignored.