Skip to main content

Customer authentication

Customer accounts let people see their bookings and check out faster. They are separate from staff accounts: a customer token only opens the customer endpoints on this page, never admin endpoints.

Customer accounts are per tenant. The same email can have separate accounts at two tenants.

How it fits together​

  1. The customer registers or logs in. Both return an access token and a refresh token.
  2. Your client sends Authorization: Bearer <access_token> on the /api/v1/customers/me... endpoints.
  3. When the access token expires (after 24 hours), the client exchanges the refresh token for a new pair.
  4. On logout, both tokens are revoked.

Every endpoint here shares the customers prefix, for example /api/v1/customers/login.

Response envelope​

Unlike most of the API, customer endpoints wrap their result:

{
"success": true,
"data": {},
"message": "Registration successful"
}

data holds the result, and message is present on some endpoints. Errors use the usual error format.

Register​

POST /api/v1/customers/register
x-tenant-id: your-tenant-id
Content-Type: application/json
{
"email": "customer@example.com",
"password": "<your-password>",
"firstName": "Aino",
"lastName": "Virtanen",
"phone": "+358401234567",
"marketingOptIn": false,
"smsOptIn": false,
"termsAccepted": true,
"privacyPolicyAccepted": true
}
FieldRequiredDescription
emailYesUnique within the tenant. Stored in lower case.
passwordYesAt least 8 characters, with an upper-case letter, a lower-case letter and a number.
termsAcceptedYesMust be true.
privacyPolicyAcceptedYesMust be true.
firstName, lastNameNoThe customer's name.
phoneNoPhone number, international format recommended.
marketingOptInNoMarketing email consent. Default false.
smsOptInNoSMS consent. Default false.
termsVersionNoThe version of the terms the customer accepted. Default 1.0.

The response is 201 with the tokens in data:

{
"success": true,
"data": {
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 86400,
"refresh_token": "9c2e...",
"refresh_token_expires_in": 7776000,
"customer": {
"id": "customer-id",
"email": "customer@example.com",
"firstName": "Aino",
"lastName": "Virtanen",
"emailVerified": false
}
},
"message": "Registration successful"
}

Sessiq emails a verification link. The customer is signed in straight away; emailVerified stays false until they click the link. Any failure, including an email that is already registered, returns 400 with the reason in detail.

Log in​

POST /api/v1/customers/login
x-tenant-id: your-tenant-id
Content-Type: application/json
{ "email": "customer@example.com", "password": "<your-password>" }

The response is 200 with the same data as registration. Any failure returns 401: wrong email or password, a disabled account, or a locked account. After five failed attempts in a row, the account locks for 15 minutes.

Refresh the token​

POST /api/v1/customers/auth/refresh
Content-Type: application/json
{ "refresh_token": "9c2e..." }

The response has the same data as login. Refresh tokens last 90 days and rotate: each refresh returns a new one and the old one stops working. An expired or reused refresh token returns 401. The customer must log in again.

Log out​

POST /api/v1/customers/logout
Authorization: Bearer <access_token>

Revokes the access token and the refresh token.

Profile​

EndpointDescription
GET /api/v1/customers/meThe signed-in customer's profile.
PUT /api/v1/customers/meUpdate profile fields.
PATCH /api/v1/customers/me/preferencesUpdate notification preferences.
DELETE /api/v1/customers/meDelete the account.

All of them need Authorization: Bearer <access_token>.

GET /api/v1/customers/me returns:

{
"success": true,
"data": {
"id": "customer-id",
"email": "customer@example.com",
"firstName": "Aino",
"lastName": "Virtanen",
"phone": "+358401234567",
"emailVerified": true,
"locale": "fi",
"marketingOptIn": false,
"smsOptIn": false,
"termsAcceptedAt": "2026-03-01T10:00:00.000Z",
"privacyPolicyAcceptedAt": "2026-03-01T10:00:00.000Z",
"termsVersion": "1.0",
"createdAt": "2026-03-01T10:00:00.000Z"
}
}

PUT /api/v1/customers/me takes any of firstName, lastName, phone, locale, marketingOptIn and smsOptIn, and returns the updated profile. Fields you leave out are not changed. To change the email address, use Change email.

PATCH /api/v1/customers/me/preferences takes any of marketingOptIn, smsOptIn, bookingConfirmationEmail, bookingReminderEmail, cancellationNotification and preferredChannel.

DELETE /api/v1/customers/me needs the password in the body, { "password": "<your-password>" }. It deletes the account and anonymises the customer's data. It fails with 400 while the customer has active bookings.

My bookings​

GET /api/v1/customers/me/bookings?status=confirmed&date_from=2026-06-01&date_to=2026-06-30
Authorization: Bearer <access_token>

All query parameters are optional. The list is not paginated.

ParameterDescription
statusOnly bookings with this status.
date_fromOnly bookings starting on or after this date.
date_toOnly bookings starting on or before this date.
{
"success": true,
"data": {
"bookings": [
{
"id": "booking-id",
"productName": "Escape room",
"lifecycleStatus": "confirmed",
"paymentStatus": "paid",
"startsAt": "2026-06-12T15:00:00.000Z",
"endsAt": "2026-06-12T16:00:00.000Z",
"partySize": 4,
"currency": "EUR",
"grandTotal": "120.00",
"addons": []
}
]
}
}

The example is shortened. Each booking also has site, product and slot IDs, the price breakdown and the contact details. Amounts are decimal strings. Read lifecycleStatus and paymentStatus; the older status field is kept only for compatibility.

Claim a guest booking​

A customer who booked without an account can attach that booking to their account afterwards:

POST /api/v1/customers/me/bookings/{bookingId}/link
Authorization: Bearer <access_token>

The booking's email must match the customer's email (403 if not). A booking already linked to another account returns 409.

Passwords​

EndpointAuthBody
POST /api/v1/customers/change-passwordBearer token{ "currentPassword": "...", "newPassword": "..." }
POST /api/v1/customers/forgot-passwordx-tenant-id{ "email": "..." }
POST /api/v1/customers/reset-passwordx-tenant-id{ "email": "...", "token": "...", "newPassword": "..." }
POST /api/v1/customers/verify-passwordBearer token{ "password": "..." }. Returns data.valid, true or false.

forgot-password emails a reset link and always returns success, so it can't be used to find out which emails have accounts. reset-password uses the token from that email. Use verify-password to confirm the customer's identity before a sensitive action in your UI.

Email verification​

EndpointBody
POST /api/v1/customers/verify-email{ "email": "...", "token": "..." }
POST /api/v1/customers/resend-verification{ "email": "..." }

Both need x-tenant-id. The token comes from the verification email and is valid for 24 hours. resend-verification always returns success.

Change email​

A change takes two steps, both with the customer's bearer token:

  1. POST /api/v1/customers/change-email with { "newEmail": "...", "password": "..." }. Sessiq emails a verification link to the new address.
  2. POST /api/v1/customers/confirm-email-change with { "token": "..." } from that email. The response is the updated profile.

The address changes only after step 2.

Rate limits​

Every endpoint on this page has the strict limit for authentication endpoints: 5 requests per minute per client, per endpoint. See Rate limits.