Authentication endpoints
Sign up, log in, refresh tokens, and redeem invite codes. The four endpoints every Frontelio client needs to know.
All authentication endpoints live under /auth. They are public by design — they don't require an existing token, because they're how clients get one in the first place. Each endpoint has a per-IP rate limit to deter brute-force and abuse.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /auth/login | Public · 10/min/IP | Exchange email + password for a JWT pair. |
| POST | /auth/refresh | Public · 30/min/IP | Trade an expiring refresh token for a fresh JWT pair. |
| POST | /auth/signup | Public · 5/min/IP | Create a new tenant + owner. Optional invite code for sibling-tenant signups. |
| POST | /auth/invite/:code/redeem | Public · 5/5min/IP | Redeem a magic-link invite to join an existing tenant. |
| GET | /auth/me | Bearer JWT | Returns the authenticated user's profile + permissions. |
| POST | /auth/switch-tenant | Bearer JWT | Issue a fresh JWT for a different tenant the user belongs to. |
| GET | /auth/memberships | Bearer JWT | List every tenant the caller has access to. |
| PATCH | /auth/me/password | Bearer JWT · 10/min/IP | Self-service password change. Requires the current password. |
POST /auth/login
Exchange a user identifier and password for a JWT pair. The identifier can be the user's email or their linkedIdentityKey for cross-tenant accounts. Throttled at 10/min/IP for brute-force defence.
Request
{
"identifier": "owner@cafejoud.ae",
"password": "••••••••"
}Response · 200 OK
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "user_abc123",
"email": "owner@cafejoud.ae",
"fullName": "Mariam Al-Hashimi",
"roles": ["TENANT_OWNER"],
"tenantId": "tenant_xyz"
}
}Token lifetimes: accessToken is valid for 15 minutes; refreshToken is valid for 30 days. Persist both client-side — the access token in memory or a short-TTL store, the refresh token in secure storage (Keychain on iOS, Keystore on Android, an HttpOnly cookie or IndexedDB on web).
POST /auth/refresh
When the 15-minute access token expires (401 from any authenticated endpoint), exchange the refresh token for a fresh pair. Refresh tokens are single-use — each refresh issues a new refresh token alongside the new access token, and the old refresh token is revoked.
Request
{
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}Response · 200 OK
{
"accessToken": "eyJhbGciOiJIUzI1NiIs... (new)",
"refreshToken": "eyJhbGciOiJIUzI1NiIs... (new)"
}POST /auth/signup
Create a new tenant. There are two paths through the same endpoint, distinguished by whether the request includes a code field:
- Self-service signup (no code) — anyone can start a brand-new company from zero. Provisions a tenant + owner + full role/permission matrix + a default outlet, then logs them in.
- Invite signup (code present) — consume an
FCO-INVcode, which can pre-parent the new tenant under an existing TenantGroup. Used to add a sibling company into a group you already own.
Either way the response logs the new owner in immediately. Throttled 5/min/IP to deter spray-signups and DB-pressure abuse.
Request
{
"tenantName": "Café Joud",
"ownerEmail": "owner@cafejoud.ae",
"ownerFullName": "Mariam Al-Hashimi",
"password": "••••••••",
"seedDemoData": true,
"code": "FCO-INV-XYZ789" // optional — sibling-tenant join
}Response · 200 OK
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"user": { ... TENANT_OWNER user ... },
"tenant": {
"id": "tenant_abc",
"name": "Café Joud",
"createdAt": "2026-05-28T08:32:10.123Z"
},
"owner": { ... same as user, for convenience ... },
"demoCredentials": [ // only present when seedDemoData=true
{ "email": "aisha.khan@demo.local", "role": "STAFF", "password": "Demo2026!" },
{ "email": "omar.farouk@demo.local", "role": "OUTLET_MANAGER", "password": "Demo2026!" },
...
]
}POST /auth/invite/:code/redeem
Magic-link onboarding for users joining an existing tenant. The 6-character invite code is generated either in the bulk-invite flow at /users → Bulk invite or as one of the printable QR codes in the onboarding wizard.
Request
{
"fullName": "Layla Hassan",
"password": "••••••••",
"phone": "+971501234567" // optional
}Response · 200 OK
Same shape as /auth/login — the new user is immediately authenticated and can call the rest of the API.
GET /auth/me
Returns the current user's profile, roles, and resolved permission set. Useful as a session-validity probe and to render role-aware UI.
Authorization: Bearer <accessToken>
200 OK
{
"id": "user_abc",
"email": "owner@cafejoud.ae",
"fullName": "Mariam Al-Hashimi",
"roles": ["TENANT_OWNER"],
"permissions": ["tenant.manage", "user.invite", ... ],
"tenant": {
"id": "tenant_xyz",
"name": "Café Joud",
"planCode": "GROWTH"
},
"primaryOutletId": "outlet_main"
}Multi-tenant tokens
A single user identity can belong to multiple tenants (managed by an owner with several companies, or a contractor working for more than one Frontelio customer). Each tenant gets its own access token, scoped to that tenant only.
GET /auth/memberships— list every tenant the caller has access to.POST /auth/switch-tenant— issue a new JWT scoped to a different tenant. Body:{ "tenantId": "tenant_other" }. Returns the same shape as/auth/loginplus aswitchedToconfirmation block.
Quick curl example
# 1. Log in
ACCESS=$(curl -sX POST https://api.frontelio.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"identifier":"owner@cafejoud.ae","password":"YourPass1!"}' \
| jq -r .accessToken)
# 2. Verify the token works
curl -sX GET https://api.frontelio.com/api/v1/auth/me \
-H "Authorization: Bearer $ACCESS"
# 3. Refresh when the access token expires
curl -sX POST https://api.frontelio.com/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken":"...your refresh token..."}'Continue to the /access API reference for the Frontelio Access endpoints.