Authentication
The API accepts three kinds of credentials:
| Credential | Who uses it | How you get one |
|---|---|---|
| Staff token | The admin console, staff tools | Log in with a staff email and password (this page) |
| API key | Server-to-server integrations | Create one in the admin console or with the API |
| Customer token | The customer portal | See 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"
}
| Field | Required | Description |
|---|---|---|
email | Yes | The staff member's email. |
password | Yes | The staff member's password. |
tenantId | No | The 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
}
}
| Field | Description |
|---|---|
access_token | Send it as Authorization: Bearer <access_token>. |
expires_in | Access token lifetime in seconds: 8 hours. |
refresh_token | Exchange it for a new access token. See Refreshing a token. |
refresh_token_expires_in | Refresh token lifetime in seconds: 30 days. |
user.permissions | The 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.
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
| Endpoint | Body | Effect |
|---|---|---|
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:
| Endpoint | Permission | Returns |
|---|---|---|
GET /api/v1/products | products:read | Active products. Optional siteId and limit query params. |
GET /api/v1/products/{id} | products:read | One product. |
GET /api/v1/admin/export/bookings | bookings:read | Booking rows without personal data, as JSON or CSV. |
GET /api/v1/admin/export/booking-deletions | bookings:read | IDs 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:
| Field | Meaning |
|---|---|
attribution | null 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. |
captureSurface | widget (brand sites) or web_app; null when attribution is null. |
consentAnalytics, consentMarketing | Consent 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:
| Field | Meaning |
|---|---|
confirmedAt | When the booking was first confirmed (below). null if it never was. |
canceledAt | When it was canceled. null unless status is canceled, and for a few canceled bookings whose cause wasn't recorded. |
cancelOrigin | Who or what canceled it (below). null exactly when status is not canceled. |
holdExpiredAt | When 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:
| Value | Meaning |
|---|---|
checkout_customer | The customer left checkout and released the hold. An abandoned checkout. |
customer | The customer canceled a confirmed booking. |
admin | Staff canceled the booking or its hold. |
resource_conflict | The customer chose to cancel when an add-on was no longer available. |
poa_auto_cancel | A pay-on-arrival booking was canceled because payment was overdue. |
part_paid_timeout | A part-paid booking was canceled because it was never fully paid. |
compensation | Refunding the customer failed after all retries. |
system | Another automatic job canceled it. |
unknown | The 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 param | Meaning |
|---|---|
deletedSince | Required. ISO-8601. Deletions at or after this time. |
deletedUntil | Optional. ISO-8601. Deletions at or before this time. |
page, pageSize, format | As 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:
| Endpoint | Description |
|---|---|
GET /api/v1/admin/api-keys | List keys. The key itself is never returned, only its keyPrefix. |
POST /api/v1/admin/api-keys | Create a key. The response is the only time you see the full key. |
GET /api/v1/admin/api-keys/permissions | List 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}/usage | Request 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"
}'
| Field | Required | Description |
|---|---|---|
name | Yes | A label so you can recognise the key later. |
permissions | Yes | At least one permission from the list below. |
ipWhitelist | No | Client IP addresses allowed to use the key. Exact addresses only; ranges are not supported. |
rateLimit | No | Requests allowed per window. Default 1000. |
rateLimitWindow | No | Window length in seconds, at least 60. Default 3600. |
expiresAt | No | ISO 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:
| Permission | Description |
|---|---|
bookings:read | View booking details |
bookings:write | Create and modify bookings |
bookings:cancel | Cancel existing bookings |
availability:read | Check slot availability |
products:read | View product catalog |
products:write | Create and update products |
customers:read | View customer information |
customers:write | Create and update customers |
reports:read | Access 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
| Status | Cause |
|---|---|
401 | Missing, unknown, revoked or expired key, or the key lacks the permission the endpoint needs. |
403 | The 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. |
403 | On 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}/usageif you suspect a key has leaked, then revoke it.