Browse docs
API Reference

Build on the Frontelio API.

A REST API over JSON. Bearer JWT for user-facing operations; per-tenant API keys for machine-to-machine. Rate-limited, versioned, and stable.

The Frontelio API is a conventional REST + JSON surface over the same domain as the web and mobile apps. Anything you can do in the UI you can do from the API — there is no separate admin-only or legacy surface. Resources are tenant-scoped: every authenticated request resolves a single tenant, and the platform never returns rows from other tenants.

We version the platform as a whole, not per-endpoint. Breaking changes are announced in the "What's new" section on the docs home at least one minor version before they land. Day-to-day API additions (new endpoints, new optional fields) are non-breaking and ship continuously.

Base URL

For all examples in these docs:

Base URL
https://api.frontelio.com/api/v1

Self-hosted installations will have their own base URL — but every example path below (e.g. /auth/login, /access/verify) is the suffix appended to your base.

Authentication

Every endpoint (with a handful of explicitly-public exceptions listed below) requires authentication. There are two authentication schemes:

  • Bearer JWT (user-facing). The primary scheme. Obtain by calling POST /auth/login with email + password. Token expires after 15 minutes; the mobile and web clients automatically refresh via POST /auth/refresh on a 401. Pass as Authorization: Bearer <accessToken>.
  • Per-tenant API key (machine-to-machine). Specifically for reader bridges and integrations. Mint in /admin/access → API keys. Format: mk_ + 32 random bytes. Pass as Authorization: Bearer <apiKey> — same header, different secret class. The API distinguishes them by prefix.

Public endpoints (no auth required, but rate-limited per IP): POST /auth/login, POST /auth/signup, POST /auth/refresh, POST /auth/invite/<code>/redeem, POST /access/visitors/<inviteCode>/redeem, POST /access/webhooks/<integrationId>.

Rate limits

We rate-limit per IP using @nestjs/throttler. The default limit is 120 requests per minute per IP, which is comfortable for normal app usage. A few hot endpoints have stricter caps:

EndpointLimitWindowWhy
POST /auth/login1060sBrute-force defence
POST /auth/signup560sAnti-spam new-tenant signups
POST /auth/refresh3060sReplay protection
POST /access/visitors/:c/redeem55 minMagic-link guessing defence
All other endpoints12060sPer-IP default

A throttled response is HTTP 429 Too Many Requests. If you need a higher limit for legitimate machine-to-machine traffic (a busy reader bridge, a high-volume webhook receiver), email support@frontelio.comwith your tenant ID and we'll raise it.

Error shape

Every 4xx / 5xx response follows the NestJS standard:

HTTP 400
{
  "statusCode": 400,
  "message": "outletId must be a valid UUID",
  "error": "Bad Request"
}

message is either a string (single error) or an array of strings (validation errors from class-validator). Always check the statusCodefield — it's the canonical signal. Common statuses:

  • 400 — your request body or query is malformed. Body of message tells you which field.
  • 401 — no token, expired token, or token signed by the wrong key (e.g. across tenant boundaries). Refresh and retry.
  • 403— authenticated but not authorized for this resource or action. Check the user's role in /users.
  • 404 — resource doesn't exist in your tenant. We deliberately return 404 (not 403) on cross-tenant lookups so attackers can't enumerate IDs across tenants.
  • 409 — write would violate a uniqueness constraint (duplicate email, duplicate outlet name within a company, etc.).
  • 429 — rate-limited (see above).
  • 500— server error. We've logged it; if you can repro, email us with the request ID from the response headers.

Pagination

Collection endpoints accept ?page and ?limit query params. Default limit=50; max limit=200. Response shape:

GET /users?page=1&limit=50
{
  "data": [ ... 50 user rows ... ],
  "meta": {
    "total": 137,
    "page": 1,
    "limit": 50,
    "pages": 3
  }
}

For large exports use ?limit=200 and paginate, or use the dedicated /admin/export/* endpoints that stream NDJSON without pagination overhead.

Explore the endpoints