Response envelope
Successful responses carry a literalsuccess: true alongside the payload:
success field is typed as the literal true in the schema rather than as a
boolean, so a generated client can discriminate on it without a runtime check.
Errors
Errors return a plain object with a message:- Routes never throw. An unhandled condition becomes a structured error response, not a stack trace or an HTML error page.
- Raw database objects are never returned. Every response is projected to a DTO, so internal fields, tokens and unrelated columns cannot leak through an over-broad select.
DTOs, not entities
Response shapes are named DTOs —EmployeeOverviewDTO, PayslipDTO,
CompensationViewDTO — generated from the Zod output schemas. They are a deliberate
projection of the domain model, and they are the compatibility surface. The database
schema behind them can change without breaking a client, and does.
Result size
The portal contract is self-scoped — every response covers one person — so its list endpoints take nolimit parameter and return no pagination block today. Do not build a
client that expects one; if pagination is introduced it will arrive as added fields,
per the versioning rule below.
Underneath, results are bounded regardless: every findMany in an API route must carry
a take or a cursor, enforced in CI rather than left to review. The platform’s
internal list endpoints — the ones behind the dashboard, not this contract — clamp a
limit parameter and return a pagination: { total, limit, offset, hasMore } block.
Path parameter validation
Path parameters are constrained in the schema, not only in the handler. A payslip period, for example, is validated against^\d{4}-\d{2}$ and a malformed value returns
400 with a message naming the expected YYYY-MM shape.
Idempotency
The platform supports anIdempotency-Key header on cost-bearing operations — the
ones that move money or produce an irreversible external side effect, such as creating a
payment batch. A retry after a network blip replays the stored response instead of
producing a second batch. Keys are scoped per route so the same key from two endpoints
cannot collide, and are retained long enough to cover any realistic retry window.
No endpoint in the portal contract is cost-bearing in that sense, so the header has no effect
here. It is documented because it is the platform-wide convention, and because a future portal
write that does carry an external side effect will honour it.
Locale
Content is returned in the caller’s locale where the content is translatable. Clients do not need to request a language — the session carries it.Versioning
The employee endpoints are versioned in their path (/api/v1/portal/…). The two session
endpoints sit at /api/auth/… and are not part of that versioning. Within a version:
- Fields may be added to a response.
- Fields are not removed and their types do not change.
- New endpoints may appear.
The specification is the contract. It is generated from the same schemas the server validates
against and checked byte-for-byte in CI, so anything documented here is enforced at runtime rather
than asserted in prose.