Developers

Authentication

DevelopersAuthentication

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_SECRET bound 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 aud claim.
  • Customer tokens have aud: "customer".
  • Super admin tokens have aud: "super_admin".
There is no refresh endpoint
JWTs last 24 hours. If your token expires, log in again. The KV session record lives 30 days, but the JWT is the only thing the Worker verifies on every request — so a new token is required every day. (Owner login also exposes a /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

POST/api/auth/login

Sign in an owner or staff member. Rate limited to 10 / 60s per IP.

emailstringrequired

The owner's email address.

passwordstringrequired

Plain-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

POST/api/customer-auth/login

Sign in an existing customer. Rate limited to 10 / 60s per IP.

emailstringrequired

The customer's email address.

passwordstringrequired

Plain-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

Internal only
The super_admin audience is for NetWit platform operators. It is not exposed to tenants or customers. To get access, contact hello@netwit.ca.
POST/api/admin/login

Sign 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

  • 401 with body &lcub; "error": "unauthorized" &rcub; — no header, signature doesn't verify, or aud doesn't match the route's expected audience.
  • 401 with code: "invalid_credentials" — wrong email or password.
  • 409 with code: "email_taken" — registration with an email that already exists.
  • 409 with code: "slug_taken" — registration with a business slug that already exists.
  • 429 with body &lcub; "error": "rate_limited" &rcub; and a Retry-After header — 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).

Need a human?

Email hello@netwit.ca or call +1-604-206-8169. NetWit responds in 1 business day.