Skip to main content

Authentication

The API accepts three kinds of credentials:

CredentialWho uses itHow you get one
Staff tokenThe admin console, staff toolsLog in with a staff email and password (this page)
API keyServer-to-server integrationsCreate one in the admin console or with the API
Customer tokenThe customer portalSee Customer authentication

Public booking endpoints, such as availability and holds, need no credential, only the x-tenant-id header.

Staff login​

POST /api/auth/login
Content-Type: application/json
{
"email": "staff@example.com",
"password": "<your-password>",
"tenantId": "your-tenant-id"
}
FieldRequiredDescription
emailYesThe staff member's email.
passwordYesThe staff member's password.
tenantIdNoThe tenant to sign in to. Without it, you get the user's first tenant. Set it if the user belongs to more than one.

Login is not under /api/v1. The response:

{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 28800,
"refresh_token": "3b1f...",
"refresh_token_expires_in": 2592000,
"user": {
"id": "user-id",
"email": "staff@example.com",
"name": "Staff Member",
"tenant_id": "your-tenant-id",
"roles": ["ADMIN"],
"permissions": ["admin:read", "bookings:read"],
"is_platform_admin": false
}
}
FieldDescription
access_tokenSend it as Authorization: Bearer <access_token>.
expires_inAccess token lifetime in seconds: 8 hours.
refresh_tokenExchange it for a new access token. See Refreshing a token.
refresh_token_expires_inRefresh token lifetime in seconds: 30 days.
user.permissionsThe user's permissions at login. The API checks permissions again on every request, so this is a hint for your UI, not a guarantee. See Authorization.

A wrong email or password returns 401. After five failed attempts in a row, the account locks for a short time, and each further lockout lasts longer (1, 5, 15, 30, then 60 minutes).

Using the token​

curl https://api.sessiq.com/api/v1/admin/products \
-H "Authorization: Bearer $ACCESS_TOKEN"

The token carries its tenant, so x-tenant-id is optional. If you send it, it must match the token's tenant, or you get 403.

Refreshing a token​

When the access token expires, calls return 401. Exchange the refresh token for a new pair:

POST /api/auth/refresh
Content-Type: application/json
{ "refresh_token": "3b1f..." }

The response has the same shape as the login response. The refresh token rotates: each refresh returns a new one and the old one stops working, so store the new one every time. If the refresh token is expired or already used, you get 401. Log in again.

note

A refresh always signs you in to the user's first tenant, whatever tenantId you logged in with. If a user belongs to several tenants, log in again with tenantId instead of refreshing.

Logging out​

POST /api/auth/logout
Authorization: Bearer <access_token>

This revokes the access token and the user's refresh token.

Password reset​

EndpointBodyEffect
POST /api/auth/forgot-password{ "email": "staff@example.com" }Emails a reset link. The response is the same whether the account exists or not.
POST /api/auth/reset-password{ "token": "...", "newPassword": "<your-new-password>" }Sets the new password, clears any lockout and revokes the refresh token.

API keys​

API keys are for server-to-server integrations, such as a data warehouse pulling bookings every night. A key belongs to one tenant and carries a list of permissions. It never expires unless you give it an expiry date.

API keys need a Pro or Enterprise plan.

Where API keys work​

API keys are accepted only on these endpoints:

EndpointPermissionReturns
GET /api/v1/productsproducts:readActive products. Optional siteId and limit query params.
GET /api/v1/products/{id}products:readOne product.
GET /api/v1/admin/export/bookingsbookings:readBooking rows without personal data, as JSON or CSV.
GET /api/v1/admin/export/booking-deletionsbookings:readIDs of deleted bookings, as JSON or CSV.

Every other endpoint rejects API keys. Use a staff token for those.

Sending a key​

Send the key in either header:

x-api-key: bk_...
Authorization: Bearer bk_...

The key decides the tenant, so you don't need x-tenant-id.

curl "https://api.sessiq.com/api/v1/admin/export/bookings?dateFrom=2026-01-01&dateTo=2026-01-31&format=csv" \
-H "x-api-key: $SESSIQ_API_KEY"

Each booking row also says where the booking came from, without personal data:

FieldMeaning
attributionnull when nothing was captured (walk-in, phone, imported or older bookings). Otherwise the UTM tags (utmSource … utmId), has* flags for ad click IDs, the referrer's host (referrerHost) and landingPath.
captureSurfacewidget (brand sites) or web_app; null when attribution is null.
consentAnalytics, consentMarketingConsent at booking time. null when none was recorded; walk-in and phone bookings record false.

Raw click IDs and full referrer URLs are never exported. In CSV, attribution is split into attribution* columns (attributionUtmSource, attributionHasGclid, …). These fields don't change a booking's updatedAt, so to fill them for bookings you already pulled, re-read them once with dateFrom/dateTo.

Each row also says how the booking got to its status:

FieldMeaning
confirmedAtWhen the booking was first confirmed (below). null if it never was.
canceledAtWhen it was canceled. null unless status is canceled, and for a few canceled bookings whose cause wasn't recorded.
cancelOriginWho or what canceled it (below). null exactly when status is not canceled.
holdExpiredAtWhen the unpaid hold timed out. It stays set if a late payment later confirmed the booking, or staff closed it.

A hold that times out has status expired. It is never canceled. cancelOrigin is one of:

ValueMeaning
checkout_customerThe customer left checkout and released the hold. An abandoned checkout.
customerThe customer canceled a confirmed booking.
adminStaff canceled the booking or its hold.
resource_conflictThe customer chose to cancel when an add-on was no longer available.
poa_auto_cancelA pay-on-arrival booking was canceled because payment was overdue.
part_paid_timeoutA part-paid booking was canceled because it was never fully paid.
compensationRefunding the customer failed after all retries.
systemAnother automatic job canceled it.
unknownThe cause wasn't recorded (rare; mostly add-on conflicts before November 2026).

These fields are worked out from the booking's history when you read the export, so they cover older bookings too and don't change updatedAt. To fill them for bookings you already pulled, re-read them once with dateFrom/dateTo and includeCancelled=true.

A booking whose status is confirmed, arrived, completed or no_show always has a confirmedAt. When no confirmation was recorded (staff-created bookings and some online bookings from before November 2026), it falls back to the first of: when the booking was created in Sessiq, paidAt, createdAt. For any other status confirmedAt is null unless a confirmation was recorded, so a paid hold that expired and was refunded has confirmedAt null.

Deleted bookings​

Most bookings are never deleted: they end up canceled or expired and stay in the export. The rare hard delete (a duplicate import cleaned up, a draft group booking) is listed in GET /api/v1/admin/export/booking-deletions:

Query paramMeaning
deletedSinceRequired. ISO-8601. Deletions at or after this time.
deletedUntilOptional. ISO-8601. Deletions at or before this time.
page, pageSize, formatAs for /bookings (pageSize max 1000; format=csv).

Each row is { id, siteId, deletedAt }, oldest first. To keep a warehouse in sync, run both pulls with the same watermark W:

curl "https://api.sessiq.com/api/v1/admin/export/bookings?updatedSince=$W&includeCancelled=true" -H "x-api-key: $SESSIQ_API_KEY"
curl "https://api.sessiq.com/api/v1/admin/export/booking-deletions?deletedSince=$W" -H "x-api-key: $SESSIQ_API_KEY"

Then delete the listed IDs. An ID you never pulled can appear; ignore it. If a deleted ID is created again, it leaves this list and comes back in /bookings. The list starts in November 2026; deletions before that aren't in it.

Managing keys​

You need the settings:write permission (Owner or Admin) to manage keys. In the admin console, use the API Keys page under Settings. The API endpoints are:

EndpointDescription
GET /api/v1/admin/api-keysList keys. The key itself is never returned, only its keyPrefix.
POST /api/v1/admin/api-keysCreate a key. The response is the only time you see the full key.
GET /api/v1/admin/api-keys/permissionsList the permissions you can put on a key.
GET /api/v1/admin/api-keys/{id}One key's details: status, permissions, rate limit, lastUsedAt, usageCount.
GET /api/v1/admin/api-keys/{id}/usageRequest log for the key. days query param, default 7.
DELETE /api/v1/admin/api-keys/{id}Revoke the key. Optional body { "reason": "..." }.

Create a key:

curl -X POST https://api.sessiq.com/api/v1/admin/api-keys \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Warehouse export",
"permissions": ["bookings:read"],
"expiresAt": "2027-12-31T23:59:59Z"
}'
FieldRequiredDescription
nameYesA label so you can recognise the key later.
permissionsYesAt least one permission from the list below.
ipWhitelistNoClient IP addresses allowed to use the key. Exact addresses only; ranges are not supported.
rateLimitNoRequests allowed per window. Default 1000.
rateLimitWindowNoWindow length in seconds, at least 60. Default 3600.
expiresAtNoISO 8601 expiry time. Without it, the key lasts until you revoke it.

The response includes key (for example bk_ followed by 64 hex characters). Store it in your secrets manager right away. Sessiq stores only a hash and can't show it again.

Key permissions​

API keys have their own permission list, separate from staff permissions:

PermissionDescription
bookings:readView booking details
bookings:writeCreate and modify bookings
bookings:cancelCancel existing bookings
availability:readCheck slot availability
products:readView product catalog
products:writeCreate and update products
customers:readView customer information
customers:writeCreate and update customers
reports:readAccess analytics and reports

Only bookings:read and products:read unlock an endpoint today (see Where API keys work). A key holding bookings:* counts as holding every bookings: permission.

Key errors​

StatusCause
401Missing, unknown, revoked or expired key, or the key lacks the permission the endpoint needs.
403The key's rate limit is used up, the request came from an IP address not on the key's list, or the tenant account is not active.
403On create: the plan doesn't include API access, or a permission name is not on the list.

Good practice​

  • Keep tokens and keys on the server. Never put a staff token or an API key with write permissions in browser code.
  • Give each integration its own key with only the permissions it needs, so you can revoke one without breaking the others.
  • Check GET /api/v1/admin/api-keys/{id}/usage if you suspect a key has leaked, then revoke it.