@sintropix/api-contract.
Base URL
All routes are mounted under a global/api prefix (see app.setGlobalPrefix('api') in apps/erp-backend/src/main.ts). Sintropix has not published a stable customer-facing base URL; substitute your deployment’s origin.
@sintropix/api-contract version bumps and the frontend that consumes it.
Route Shape
Most business resources live under a single Entity:/api/entities (list and create), /api/fx-rates, /api/countries and /api/currencies, /api/onboarding/* (staff only), /api/agents/models, and /api/health.
{entityId} is always a UUID. The tenant guard rejects a request where the caller has no Entity Membership to that Entity with a 404; it never returns 403, so probing does not reveal whether an unknown Entity exists.
Authentication
Every non-anonymous request carries one of two credentials:- Session cookie issued by Better Auth after a browser sign-in. Same-site only (the SPA and API share a registrable domain).
- API key in the
x-api-keyheader. Keys are minted by the owner from the settings UI; the API management surface is barred to key holders (a request to/api/auth/*withx-api-keyreturns403).
POST, PUT, PATCH, DELETE) authenticated with an API key must also send a Declared Actor:
x-audit-actor is a self-reported software label (for example claude-opus-4.8) that lands verbatim in the Audit Trail. Omitting it on a key-authenticated mutation returns 400.
See Authentication for the full model, including read-only keys, the Erwin Key Lease, and staff versus client roles.
Request Format
Requests are plain JSON. IncludeContent-Type: application/json on any request with a body.
ParseUUIDPipe; a malformed id returns 400.
Response Format
Responses are the resource itself as JSON. There is no top-leveldata wrapper, no meta envelope, and no pagination block on non-paginated endpoints. The exact shape of every response is the Zod schema exported from @sintropix/api-contract and enforced by nestjs-zod’s @ZodSerializerDto at serialization time, so the compiled schema is the source of truth for field names and types.
A single resource comes back as an object:
/pages, plus a handful of reports) returns a { rows, nextCursor } object. nextCursor is null on the last page.
Pagination is opt-in per endpoint, not sitewide. Endpoints without
/pages in their path return every row and take no cursor argument. Check the individual reference page for the endpoint you are calling.Errors
Non-2xx responses are JSON. Application errors thrown asHttpException return the exception body; unhandled errors return "Internal server error". Every response, success or failure, carries a x-request-id header for support (REQUEST_ID_HEADER in @sintropix/observability).
Domain-level errors carry a stable string
code in the body — for example API_KEY_READ_ONLY_ERROR from apps/erp-backend/src/auth/error-codes.constants.ts. Match on code, not on the human-readable message.
Rate Limits
Better Auth’sapiKey plugin rate-limits each key at 5,000 requests per hour, sliding window (rateLimit in apps/erp-backend/src/auth/auth.ts). Session-cookie traffic is not rate-limited at the application layer. Exceeding the limit returns 429; there is no X-RateLimit-* header response contract.
CORS and Same-Site
The API enables CORS only for origins listed in theCORS_ORIGINS deployment env var, with credentials enabled. Browser clients must share a registrable domain with the API (for example app.sintropix.com and api.sintropix.com); the session cookie is same-site and Safari drops it across sites. Server-to-server API-key traffic is unaffected.