Authentication
Three auth realms, API Keys, roles & permissions — everything you need to secure your Ordeliya API integration.
Overview
The Ordeliya API uses three completely isolated authentication realms. Tokens from one realm cannot be used in another. This separation ensures that a customer storefront token can never access restaurant admin endpoints, and vice versa.
| Realm | Audience Claim | Token Lifetime | Endpoint Prefix |
|---|---|---|---|
| Tenant | tenant | 15 min access / 7 day refresh | /auth/* |
| Customer | customer | 15 min access / 7 day refresh | /customer-auth/* |
| API Key | — | No expiry (until revoked) | Any tenant endpoint |
Realm 1 — Tenant Authentication
Tenant auth is used by restaurant owners, admins, and staff who manage the business through the dashboard or API.
POST /auth/login
Authenticate with email and password. Returns an access token (JWT) and sets a refresh token as an httpOnly cookie.
curl -X POST https://api.ordeliya.com/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "owner@myrestaurant.dk",
"password": "Str0ng!P@ssw0rd"
}'
Response 200 OK
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfNGs3bTJuOHYiLCJlbWFpbCI6Im93bmVyQG15cmVzdGF1cmFudC5kayIsInN0b3JlSWQiOiJzdG9yZV9yNGs3Iiwicm9sZSI6Ik9XTkVSIiwiYXVkIjoidGVuYW50IiwiaWF0IjoxNzEwNTEwMTgwLCJleHAiOjE3MTA1MTEwODB9.Hk7x9Qm2nR4vB1cT",
"user": {
"id": "usr_4k7m2n8v",
"email": "owner@myrestaurant.dk",
"name": "Anders Jensen",
"platformRole": null
},
"stores": [
{
"storeId": "store_r4k7",
"storeName": "Pizza Roma — Copenhagen",
"websiteId": "web_m3x9",
"websiteName": "Pizza Roma",
"websiteDomain": "pizzaroma.dk",
"role": "OWNER",
"countryCode": "DK",
"storeViews": [
{ "id": "sv_d1a2", "code": "da_dk", "locale": "da-DK", "currency": "DKK", "isDefault": true },
{ "id": "sv_e3n4", "code": "en_dk", "locale": "en-GB", "currency": "DKK", "isDefault": false }
]
},
{
"storeId": "store_h8n2",
"storeName": "Pizza Roma — Aarhus",
"websiteId": "web_m3x9",
"websiteName": "Pizza Roma",
"websiteDomain": "pizzaroma.dk",
"role": "OWNER",
"countryCode": "DK",
"storeViews": [
{ "id": "sv_f5g6", "code": "da_dk", "locale": "da-DK", "currency": "DKK", "isDefault": true }
]
}
]
}
}
The response also sets an httpOnly cookie:
Set-Cookie: refreshToken=rt_a1b2c3...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=604800
Error 401 Unauthorized
{
"success": false,
"error": {
"statusCode": 401,
"message": "Invalid email or password"
}
}
POST /auth/refresh
Exchange a refresh token for a new access token. The refresh token cookie is sent automatically by the browser.
curl -X POST https://api.ordeliya.com/auth/refresh \
-b "refreshToken=rt_a1b2c3..."
Response 200 OK
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs..."
}
}
The old refresh token is invalidated and a new one is set via cookie (rotation). If a revoked refresh token is reused, all sessions for that user are terminated (reuse detection).
GET /auth/me
Get the currently authenticated user and active store context.
curl https://api.ordeliya.com/auth/me \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
Response 200 OK
{
"success": true,
"data": {
"user": {
"id": "usr_4k7m2n8v",
"email": "owner@myrestaurant.dk",
"name": "Anders Jensen"
},
"store": {
"storeId": "store_r4k7",
"storeName": "Pizza Roma — Copenhagen",
"websiteId": "web_m3x9",
"role": "OWNER",
"countryCode": "DK",
"locale": "da-DK",
"currency": "DKK",
"timezone": "Europe/Copenhagen"
}
}
}
POST /auth/switch-store
Switch to a different store within the same Website. Returns a new access token scoped to the target store.
curl -X POST https://api.ordeliya.com/auth/switch-store \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"storeId": "store_h8n2",
"storeViewId": "sv_f5g6"
}'
Response 200 OK
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"store": {
"storeId": "store_h8n2",
"storeName": "Pizza Roma — Aarhus",
"role": "OWNER"
}
}
}
POST /auth/logout
Revoke the current session. The refresh token is invalidated immediately.
curl -X POST https://api.ordeliya.com/auth/logout \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
Response 200 OK
{
"success": true,
"data": {
"message": "Logged out successfully"
}
}
Realm 2 — Customer Authentication
Customer auth is used by end users on the storefront. Customers can only access their own data (orders, profile, addresses).
POST /customer-auth/login
curl -X POST https://api.ordeliya.com/customer-auth/login \
-H "Content-Type: application/json" \
-d '{
"storeId": "store_r4k7",
"email": "maria@example.dk",
"password": "customer-password"
}'
Response 200 OK
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"customer": {
"id": "cust_n3k7m2",
"email": "maria@example.dk",
"firstName": "Maria",
"lastName": "Nielsen",
"phone": "+4520123456",
"loyaltyPoints": 2450,
"totalOrders": 12
}
}
}
POST /customer-auth/register
curl -X POST https://api.ordeliya.com/customer-auth/register \
-H "Content-Type: application/json" \
-d '{
"storeId": "store_r4k7",
"email": "new.customer@example.dk",
"password": "SecurePass123!",
"firstName": "Lars",
"lastName": "Andersen",
"phone": "+4531234567"
}'
Response 201 Created
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"customer": {
"id": "cust_p8q9r0",
"email": "new.customer@example.dk",
"firstName": "Lars",
"lastName": "Andersen",
"phone": "+4531234567",
"loyaltyPoints": 0,
"totalOrders": 0
}
}
}
POST /customer-auth/social
Authenticate via Google, Apple, or Facebook. The token is the OAuth ID token from the provider.
curl -X POST https://api.ordeliya.com/customer-auth/social \
-H "Content-Type: application/json" \
-d '{
"storeId": "store_r4k7",
"provider": "google",
"token": "eyJhbGciOiJSUzI1NiIs..."
}'
Supported providers: google, apple, facebook
If the email is not registered, a new customer account is created automatically (JIT provisioning).
POST /customer-auth/refresh
Exchange a customer refresh token for a new access token.
curl -X POST https://api.ordeliya.com/customer-auth/refresh \
-b "customerRefreshToken=crt_x1y2z3..."
Response 200 OK
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs..."
}
}
POST /customer-auth/logout
Revoke the current customer session.
curl -X POST https://api.ordeliya.com/customer-auth/logout \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
Response 200 OK
{
"success": true,
"data": {
"message": "Logged out successfully"
}
}
Realm 3 — API Keys
API Keys provide long-lived, scoped access to tenant endpoints. They are ideal for server-to-server integrations, POS systems, and third-party apps.
Key Format
ord_live_sk_7f3a9b2c1d4e5f6a7b8c9d0e1f2a3b4c
│ │ │ └── 32-character hex secret
│ │ └────── "sk" = secret key
│ └─────────── "live" = production (vs "test" for sandbox)
└──────────────── "ord" = Ordeliya prefix
Creating API Keys
- Navigate to Settings → API Keys in the dashboard
- Click Create API Key
- Enter a descriptive name (e.g., "POS Integration — Main Branch")
- Select the required scopes (see table below)
- Click Create — the full key is displayed once
- Copy and store it securely (e.g., environment variable, secrets manager)
Using API Keys
API Keys are passed as Bearer tokens, just like JWTs:
curl https://api.ordeliya.com/orders \
-H "Authorization: Bearer ord_live_sk_7f3a9b2c1d4e5f6a7b8c9d0e1f2a3b4c"
API Key Scopes
| Scope | Allows | Endpoints |
|---|---|---|
read:orders | List and view orders | GET /orders, GET /orders/:id |
write:orders | Create and update orders | POST /orders, PATCH /orders/:id/status |
read:products | List and view products | GET /products, GET /products/:id |
write:products | Create, update, delete products | POST /products, PATCH /products/:id |
read:customers | List and view customers | GET /customers, GET /customers/:id |
read:analytics | View revenue and order analytics | GET /analytics/* |
read:reservations | List and view reservations | GET /reservations |
write:reservations | Create and update reservations | POST /reservations |
webhooks:manage | Create and manage webhooks | POST /webhooks, DELETE /webhooks/:id |
A key with read:orders cannot create orders. A key with write:products can read and write products (write implies read for the same resource).
Revoking API Keys
Revoke a compromised key immediately from Settings → API Keys → Revoke. Revocation is instant — all subsequent requests with that key return 401.
Roles & Permissions
Tenant users have one of four roles. Each role inherits all permissions of the roles below it.
| Permission | Owner | Admin | Manager | Staff | API Key | Customer |
|---|---|---|---|---|---|---|
| View orders | yes | yes | yes | yes | scoped | own only |
| Create/update orders | yes | yes | yes | yes | scoped | — |
| Cancel orders | yes | yes | yes | — | scoped | — |
| View products | yes | yes | yes | yes | scoped | — |
| Create/update products | yes | yes | yes | — | scoped | — |
| Delete products | yes | yes | — | — | scoped | — |
| View customers | yes | yes | yes | — | scoped | own only |
| View analytics | yes | yes | — | — | scoped | — |
| Manage settings | yes | yes | — | — | — | — |
| Manage users & roles | yes | — | — | — | — | — |
| Manage API keys | yes | — | — | — | — | — |
| Billing & subscription | yes | — | — | — | — | — |
"scoped" means the API Key can access the resource only if it has the corresponding scope.
JWT Structure
The access token is a standard JWT with the following claims:
{
"sub": "usr_4k7m2n8v",
"email": "owner@myrestaurant.dk",
"storeId": "store_r4k7",
"role": "OWNER",
"aud": "tenant",
"iat": 1710510180,
"exp": 1710511080
}
| Claim | Description |
|---|---|
sub | User ID |
email | User email |
storeId | Active store ID (token is scoped to this store) |
role | User role: OWNER, ADMIN, MANAGER, STAFF |
aud | Audience: tenant, platform-admin, or customer |
iat | Issued at (Unix timestamp) |
exp | Expires at (Unix timestamp, 15 minutes after iat) |
Do not decode and trust the JWT on the client side for authorization decisions. The server validates the token on every request.
Security Best Practices
- HTTPS only — All API communication must use HTTPS. HTTP requests are rejected.
- Never expose tokens in URLs — Use
Authorizationheader, never query parameters. - Rotate refresh tokens — Each refresh call invalidates the old token. Reuse of old tokens terminates all sessions.
- Use API Keys for server-to-server — Don't use JWTs for cron jobs or background processes.
- Minimum scopes — Only request the scopes your integration needs.
- Store secrets securely — Use environment variables or a secrets manager, never commit to source control.
- Monitor API Key usage — Check Settings → API Keys → Usage for anomalies.
Error Reference
| Status | Code | Meaning | Resolution |
|---|---|---|---|
401 | TOKEN_EXPIRED | JWT has expired | Call POST /auth/refresh to get a new token |
401 | TOKEN_INVALID | Malformed or tampered token | Re-authenticate with POST /auth/login |
401 | TOKEN_REVOKED | Refresh token was revoked | Re-authenticate with POST /auth/login |
401 | API_KEY_REVOKED | API key has been revoked | Create a new key in Settings → API Keys |
403 | INSUFFICIENT_ROLE | Role doesn't have permission | Use an account with a higher role |
403 | INSUFFICIENT_SCOPE | API key lacks required scope | Create a new key with the needed scope |
403 | STORE_MISMATCH | Token is scoped to a different store | Switch store with POST /auth/switch-store |
429 | RATE_LIMITED | Too many auth requests (max 10/min) | Wait for Retry-After seconds |