Authentication
BookFlow issues JSON Web Tokens (JWTs) signed with HS256. Every protected endpoint expects the token in an Authorization: Bearer <token> header. There are three distinct JWT audiences and each has its own login route and target endpoints.
Token format
- Algorithm:
HS256(HMAC-SHA-256) - Secret: a single
JWT_SECRETbound to the Worker - Owner/staff token lifetime: 24 hours
- Customer token lifetime: 24 hours
- Super admin token lifetime: 4 hours
- Owner session record (KV): 30 days
- Customer session record (KV): 30 days
- Super admin session record (KV): 8 hours
- Owner/staff tokens do not set an
audclaim. - Customer tokens have
aud: "customer". - Super admin tokens have
aud: "super_admin".
/api/auth/refresh endpoint that re-signs the same payload for 24h, but it does not change the claim set or the user.)The Authorization header
Every authenticated request must carry the token like this:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Missing or malformed headers return 401.
1. Owner / staff tokens
Owners and staff sign in via POST /api/auth/login. The returned token targets the tenant the user is a member of; the first membership is used as the default tenantId. All /api/businesses/*, /api/services/*, /api/staff/*, /api/bookings/*, /api/customers/*, and /api/payments/* endpoints accept this token.
Request
/api/auth/loginSign in an owner or staff member. Rate limited to 10 / 60s per IP.
emailstringrequiredThe owner's email address.
passwordstringrequiredPlain-text password (8+ chars on register).
Response (200)
{
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"sessionId": "a1b2c3...",
"user": {
"id": "u_abc",
"email": "owner@example.com",
"firstName": "Alex",
"lastName": "Rivera",
"role": "customer"
},
"memberships": [
{
"tenantId": "tn_xxx",
"role": "owner",
"slug": "demo-salon",
"businessName": "Demo Salon"
}
]
}
}Note: the user.role on login comes from the users.role column (which is set to 'customer' at registration), not from the tenant_members.role. The actual access role for the active tenant is memberships[0].role.
Examples
# bash
curl -X POST https://booking-api.netwit.ca/api/auth/login \
-H "content-type: application/json" \
-d '{
"email": "owner@yourbusiness.com",
"password": "your-password"
}'
# Use the token
curl https://booking-api.netwit.ca/api/businesses/me \
-H "authorization: Bearer $TOKEN"# Node
const r1 = await fetch("https://booking-api.netwit.ca/api/auth/login", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
email: "owner@yourbusiness.com",
password: "your-password",
}),
});
const { data } = await r1.json();
const token = data.token;
const r2 = await fetch("https://booking-api.netwit.ca/api/businesses/me", {
headers: { authorization: "Bearer " + token },
});
console.log(await r2.json());# Python
import requests
BASE = "https://booking-api.netwit.ca"
r = requests.post(f"{BASE}/api/auth/login", json={
"email": "owner@yourbusiness.com",
"password": "your-password",
})
token = r.json()["data"]["token"]
me = requests.get(f"{BASE}/api/businesses/me", headers={
"authorization": f"Bearer {token}"
}).json()
print(me)2. Customer tokens
End customers (the people who book appointments) sign in via POST /api/customer-auth/login. Their token is not tenant-scoped — a single customer can have bookings with many tenants. The aud claim is explicitly customer, which the auth middleware uses to keep this space isolated from owner tokens.
Request
/api/customer-auth/loginSign in an existing customer. Rate limited to 10 / 60s per IP.
emailstringrequiredThe customer's email address.
passwordstringrequiredPlain-text password.
Response (200)
{
"data": {
"token": "eyJ...",
"sessionId": "...",
"customer": {
"id": "cu_abc",
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Doe",
"phone": "+16042068169",
"defaultTimezone": "America/Vancouver",
"emailVerified": false,
"marketingOptIn": false,
"createdAt": "2026-08-15T18:00:00Z",
"lastLoginAt": "2026-08-15T18:00:00Z"
}
}
}Examples
# bash
curl -X POST https://booking-api.netwit.ca/api/customer-auth/login \
-H "content-type: application/json" \
-d '{"email":"jane@example.com","password":"..."}'3. Super admin tokens
/api/admin/loginSign in as a super admin. The JWT TTL is 4 hours and the KV session record expires after 8 hours.
The token targets the platform-wide admin API (/api/admin/*). It carries aud: "super_admin", plus email and name claims for audit log rendering.
Audience confusion is a 401
If you send a customer token to an owner endpoint (or vice versa), you'll get a 401, not a 200 with partial data. The Worker enforces the audience strictly to prevent cross-tenant data leaks.
Common errors
401with body{ "error": "unauthorized" }— no header, signature doesn't verify, orauddoesn't match the route's expected audience.401withcode: "invalid_credentials"— wrong email or password.409withcode: "email_taken"— registration with an email that already exists.409withcode: "slug_taken"— registration with a business slug that already exists.429with body{ "error": "rate_limited" }and aRetry-Afterheader — too many login attempts.
For local development
Run wrangler dev from apps/api. The Worker serves on http://localhost:8787. Set JWT_SECRET in your .dev.vars file (it's the same secret as production).
Email hello@netwit.ca or call +1-604-206-8169. NetWit responds in 1 business day.