mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 07:41:16 +00:00
Utilization now measures booked minutes against each member's availability-rule capacity in the tenant timezone, replacing the confirmed-share proxy. StaffMetric exposes booked/capacity minutes. New endpoints: GET /v1/export/staff-report.csv (reuses StaffMetrics) and GET /v1/export/service-report.csv (per-service bookings/cancelled/ revenue, class bookings rolled up). Export buttons on the staff performance panel and services page. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2985 lines
78 KiB
YAML
2985 lines
78 KiB
YAML
openapi: 3.0.3
|
|
info:
|
|
title: Bookra API
|
|
version: 0.2.0
|
|
description: >
|
|
Remote-first booking API for Bookra. The Go backend owns business rules,
|
|
scheduling logic, tenant isolation, and Stripe-backed plan enforcement.
|
|
servers:
|
|
- url: http://localhost:8080
|
|
tags:
|
|
- name: Health
|
|
- name: Auth
|
|
- name: Admin
|
|
- name: Public Booking
|
|
- name: Dashboard
|
|
- name: Tenant
|
|
- name: Catalog
|
|
- name: Billing
|
|
- name: Calendar
|
|
- name: SMS
|
|
- name: Jobs
|
|
paths:
|
|
/healthz:
|
|
get:
|
|
tags: [Health]
|
|
operationId: getHealth
|
|
responses:
|
|
"200":
|
|
description: Service health
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/HealthResponse"
|
|
/v1/meta/config:
|
|
get:
|
|
tags: [Health]
|
|
operationId: getPublicConfig
|
|
responses:
|
|
"200":
|
|
description: Public runtime configuration
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/PublicConfig"
|
|
/v1/public/widget.js:
|
|
get:
|
|
tags: [Public Booking]
|
|
operationId: getWidgetScript
|
|
responses:
|
|
"200":
|
|
description: Embeddable widget script
|
|
content:
|
|
application/javascript:
|
|
schema:
|
|
type: string
|
|
# ---------------- Auth ----------------
|
|
/v1/auth/register:
|
|
post:
|
|
tags: [Auth]
|
|
operationId: register
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [email, password]
|
|
properties:
|
|
email:
|
|
type: string
|
|
format: email
|
|
password:
|
|
type: string
|
|
minLength: 8
|
|
name:
|
|
type: string
|
|
responses:
|
|
"201":
|
|
description: User created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AuthResponse"
|
|
"400":
|
|
description: Invalid request
|
|
"409":
|
|
description: Email already registered
|
|
/v1/auth/login:
|
|
post:
|
|
tags: [Auth]
|
|
operationId: login
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [email, password]
|
|
properties:
|
|
email:
|
|
type: string
|
|
format: email
|
|
password:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: Login succeeded
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AuthResponse"
|
|
"401":
|
|
description: Invalid credentials
|
|
/v1/auth/magic-link:
|
|
post:
|
|
tags: [Auth]
|
|
operationId: requestMagicLink
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [email]
|
|
properties:
|
|
email:
|
|
type: string
|
|
format: email
|
|
responses:
|
|
"200":
|
|
description: Magic link issued (always 200 to prevent enumeration)
|
|
/v1/auth/verify:
|
|
post:
|
|
tags: [Auth]
|
|
operationId: verifyMagicLink
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [token]
|
|
properties:
|
|
token:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: Token verified
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AuthResponse"
|
|
"401":
|
|
description: Invalid or expired token
|
|
/v1/auth/refresh:
|
|
post:
|
|
tags: [Auth]
|
|
operationId: refreshTokens
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [refreshToken]
|
|
properties:
|
|
refreshToken:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: Tokens rotated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/AuthResponse"
|
|
"401":
|
|
description: Invalid refresh token
|
|
# ---------------- Admin ----------------
|
|
/v1/admin/stats:
|
|
get:
|
|
tags: [Admin]
|
|
operationId: getAdminStats
|
|
security:
|
|
- adminAuth: []
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Platform statistics
|
|
"403":
|
|
description: Forbidden
|
|
/v1/admin/tenants:
|
|
get:
|
|
tags: [Admin]
|
|
operationId: listAdminTenants
|
|
security:
|
|
- adminAuth: []
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: All tenants
|
|
"403":
|
|
description: Forbidden
|
|
/v1/admin/users:
|
|
get:
|
|
tags: [Admin]
|
|
operationId: listAdminUsers
|
|
security:
|
|
- adminAuth: []
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: All users
|
|
"403":
|
|
description: Forbidden
|
|
/v1/admin/users/{userID}/role:
|
|
put:
|
|
tags: [Admin]
|
|
operationId: updateUserRole
|
|
security:
|
|
- adminAuth: []
|
|
- bearerAuth: []
|
|
parameters:
|
|
- in: path
|
|
name: userID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [role]
|
|
properties:
|
|
role:
|
|
type: string
|
|
enum: [owner, staff, admin]
|
|
responses:
|
|
"200":
|
|
description: Role updated
|
|
"403":
|
|
description: Forbidden
|
|
/v1/admin/trigger-trial-emails:
|
|
post:
|
|
tags: [Admin]
|
|
operationId: triggerTrialEmails
|
|
security:
|
|
- adminAuth: []
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Trial emails triggered
|
|
"403":
|
|
description: Forbidden
|
|
# ---------------- Public Booking ----------------
|
|
/v1/public/tenants/{tenantSlug}/availability:
|
|
get:
|
|
tags: [Public Booking]
|
|
operationId: getPublicAvailability
|
|
parameters:
|
|
- in: path
|
|
name: tenantSlug
|
|
required: true
|
|
schema:
|
|
type: string
|
|
- in: query
|
|
name: serviceId
|
|
required: false
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
description: Restrict appointment slots to a single service
|
|
- in: query
|
|
name: staffId
|
|
required: false
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
description: Restrict appointment slots to a single staff member
|
|
- in: query
|
|
name: date
|
|
required: false
|
|
schema:
|
|
type: string
|
|
format: date
|
|
description: Return all slots for a single tenant-local day (YYYY-MM-DD) instead of the capped preview
|
|
responses:
|
|
"200":
|
|
description: Availability for a tenant booking page
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/PublicAvailabilityResponse"
|
|
/v1/public/tenants/{tenantSlug}/catalog:
|
|
get:
|
|
tags: [Public Booking]
|
|
operationId: getPublicCatalog
|
|
parameters:
|
|
- in: path
|
|
name: tenantSlug
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: Bookable services and active staff for a tenant booking page
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/PublicCatalogResponse"
|
|
/v1/public/bookings:
|
|
post:
|
|
tags: [Public Booking]
|
|
operationId: createBooking
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CreateBookingRequest"
|
|
responses:
|
|
"201":
|
|
description: Booking created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CreateBookingResponse"
|
|
"409":
|
|
description: Slot no longer available
|
|
/v1/public/bookings/{reference}:
|
|
get:
|
|
tags: [Public Booking]
|
|
operationId: getBookingByReference
|
|
parameters:
|
|
- in: path
|
|
name: reference
|
|
required: true
|
|
schema:
|
|
type: string
|
|
- in: query
|
|
name: token
|
|
required: true
|
|
schema:
|
|
type: string
|
|
description: Manage token issued at booking time
|
|
responses:
|
|
"200":
|
|
description: Booking details
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CustomerBookingView"
|
|
"403":
|
|
description: Invalid manage token
|
|
"404":
|
|
description: Booking not found
|
|
/v1/public/bookings/{reference}/reschedule:
|
|
post:
|
|
tags: [Public Booking]
|
|
operationId: rescheduleBooking
|
|
parameters:
|
|
- in: path
|
|
name: reference
|
|
required: true
|
|
schema:
|
|
type: string
|
|
- in: query
|
|
name: token
|
|
required: true
|
|
schema:
|
|
type: string
|
|
description: Manage token issued at booking time
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [newStartsAt, newEndsAt]
|
|
properties:
|
|
newStartsAt:
|
|
type: string
|
|
format: date-time
|
|
newEndsAt:
|
|
type: string
|
|
format: date-time
|
|
reason:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: Booking rescheduled
|
|
"400":
|
|
description: Invalid new window, non-reschedulable status, or class booking
|
|
"403":
|
|
description: Invalid manage token
|
|
"409":
|
|
description: New slot conflicts or booking already cancelled
|
|
/v1/public/bookings/{reference}/sessions:
|
|
get:
|
|
tags: [Public Booking]
|
|
operationId: listSessionMoveTargets
|
|
summary: Upcoming sessions a class booking can move to
|
|
description: >-
|
|
Class bookings cannot reschedule to arbitrary windows — they move
|
|
between sessions of the same template. Returns upcoming same-template
|
|
sessions with live capacity info.
|
|
parameters:
|
|
- in: path
|
|
name: reference
|
|
required: true
|
|
schema:
|
|
type: string
|
|
- in: query
|
|
name: token
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: Move targets with capacity info
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
sessions:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/SessionMoveTarget"
|
|
"400":
|
|
description: Not a live class booking
|
|
"403":
|
|
description: Invalid manage token
|
|
"404":
|
|
description: Booking not found
|
|
"409":
|
|
description: Booking already cancelled
|
|
/v1/public/bookings/{reference}/move:
|
|
post:
|
|
tags: [Public Booking]
|
|
operationId: moveBookingToSession
|
|
summary: Move a class booking to another session of the same template
|
|
description: >-
|
|
Retargets the booking to the target session. Confirmed bookings free
|
|
their old seat (draining its waitlist); waitlisted bookings release
|
|
their queue position. The booking is confirmed when the target has a
|
|
free seat, otherwise it joins the target's waitlist.
|
|
parameters:
|
|
- in: path
|
|
name: reference
|
|
required: true
|
|
schema:
|
|
type: string
|
|
- in: query
|
|
name: token
|
|
required: true
|
|
schema:
|
|
type: string
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [classSessionId]
|
|
properties:
|
|
classSessionId:
|
|
type: string
|
|
format: uuid
|
|
responses:
|
|
"200":
|
|
description: Booking moved; status is the resulting booking state
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
status:
|
|
type: string
|
|
enum: [confirmed, waitlisted]
|
|
"400":
|
|
description: Not a live class booking, or target is not an upcoming same-template session
|
|
"403":
|
|
description: Invalid manage token
|
|
"404":
|
|
description: Booking not found
|
|
"409":
|
|
description: Booking already cancelled
|
|
/v1/public/bookings/{reference}/cancel:
|
|
post:
|
|
tags: [Public Booking]
|
|
operationId: cancelBooking
|
|
parameters:
|
|
- in: path
|
|
name: reference
|
|
required: true
|
|
schema:
|
|
type: string
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [token]
|
|
properties:
|
|
token:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: Booking cancelled
|
|
"403":
|
|
description: Invalid manage token
|
|
/v1/public/contact:
|
|
post:
|
|
tags: [Public Booking]
|
|
operationId: submitContactForm
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name, email, message]
|
|
properties:
|
|
name:
|
|
type: string
|
|
email:
|
|
type: string
|
|
format: email
|
|
message:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: Message accepted
|
|
# ---------------- Dashboard ----------------
|
|
/v1/dashboard/summary:
|
|
get:
|
|
tags: [Dashboard]
|
|
operationId: getDashboardSummary
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Tenant dashboard summary
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/DashboardSummary"
|
|
"401":
|
|
description: Unauthorized
|
|
# ---------------- Tenant ----------------
|
|
/v1/tenants/bootstrap:
|
|
get:
|
|
tags: [Tenant]
|
|
operationId: getTenantBootstrap
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Tenant bootstrap payload for the authenticated user
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/TenantBootstrap"
|
|
"401":
|
|
description: Unauthorized
|
|
/v1/tenants/onboard:
|
|
post:
|
|
tags: [Tenant]
|
|
operationId: onboardTenant
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/OnboardTenantRequest"
|
|
responses:
|
|
"201":
|
|
description: Tenant created for authenticated user
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/TenantBootstrap"
|
|
"400":
|
|
description: Invalid request
|
|
"401":
|
|
description: Unauthorized
|
|
"409":
|
|
description: Conflict
|
|
/v1/tenants/brand:
|
|
put:
|
|
tags: [Tenant]
|
|
operationId: updateTenantBrand
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateBrandRequest"
|
|
responses:
|
|
"200":
|
|
description: Brand profile updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/BrandProfile"
|
|
"401":
|
|
description: Unauthorized
|
|
/v1/tenants/email-templates:
|
|
get:
|
|
tags: [Tenant]
|
|
operationId: listEmailTemplates
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Tenant email templates
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [templates]
|
|
properties:
|
|
templates:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/EmailTemplate"
|
|
"401":
|
|
description: Unauthorized
|
|
/v1/tenants/email-template:
|
|
put:
|
|
tags: [Tenant]
|
|
operationId: updateEmailTemplate
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateEmailTemplateRequest"
|
|
responses:
|
|
"200":
|
|
description: Template saved
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/EmailTemplate"
|
|
"400":
|
|
description: Invalid request
|
|
"401":
|
|
description: Unauthorized
|
|
# ---------------- Catalog ----------------
|
|
/v1/catalog/locations:
|
|
get:
|
|
tags: [Catalog]
|
|
operationId: listLocations
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Tenant locations
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/Location"
|
|
"401":
|
|
description: Unauthorized
|
|
post:
|
|
tags: [Catalog]
|
|
operationId: createLocation
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpsertLocationRequest"
|
|
responses:
|
|
"201":
|
|
description: Location created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Location"
|
|
"400":
|
|
description: Invalid request
|
|
"403":
|
|
description: Plan limit reached
|
|
/v1/catalog/locations/{locationID}:
|
|
put:
|
|
tags: [Catalog]
|
|
operationId: updateLocation
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/locationID"
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpsertLocationRequest"
|
|
responses:
|
|
"200":
|
|
description: Location updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Location"
|
|
"404":
|
|
description: Location not found
|
|
delete:
|
|
tags: [Catalog]
|
|
operationId: deleteLocation
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/locationID"
|
|
responses:
|
|
"200":
|
|
description: Location deleted
|
|
"404":
|
|
description: Location not found
|
|
/v1/catalog/services:
|
|
get:
|
|
tags: [Catalog]
|
|
operationId: listServices
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Tenant services
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/Service"
|
|
"401":
|
|
description: Unauthorized
|
|
post:
|
|
tags: [Catalog]
|
|
operationId: createService
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpsertServiceRequest"
|
|
responses:
|
|
"201":
|
|
description: Service created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Service"
|
|
"400":
|
|
description: Invalid request
|
|
/v1/catalog/services/{serviceID}:
|
|
put:
|
|
tags: [Catalog]
|
|
operationId: updateService
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/serviceID"
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpsertServiceRequest"
|
|
responses:
|
|
"200":
|
|
description: Service updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Service"
|
|
"400":
|
|
description: Invalid request
|
|
"404":
|
|
description: Service not found
|
|
delete:
|
|
tags: [Catalog]
|
|
operationId: deleteService
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/serviceID"
|
|
responses:
|
|
"200":
|
|
description: Service deleted
|
|
"404":
|
|
description: Service not found
|
|
/v1/catalog/staff:
|
|
get:
|
|
tags: [Catalog]
|
|
operationId: listStaff
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Tenant staff
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/StaffMember"
|
|
"401":
|
|
description: Unauthorized
|
|
post:
|
|
tags: [Catalog]
|
|
operationId: createStaff
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpsertStaffRequest"
|
|
responses:
|
|
"201":
|
|
description: Staff member created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/StaffMember"
|
|
"400":
|
|
description: Invalid request
|
|
"403":
|
|
description: Plan limit reached
|
|
/v1/catalog/staff/{staffID}:
|
|
put:
|
|
tags: [Catalog]
|
|
operationId: updateStaff
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/staffID"
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpsertStaffRequest"
|
|
responses:
|
|
"200":
|
|
description: Staff member updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/StaffMember"
|
|
"400":
|
|
description: Invalid request
|
|
"404":
|
|
description: Staff member not found
|
|
delete:
|
|
tags: [Catalog]
|
|
operationId: deleteStaff
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/staffID"
|
|
responses:
|
|
"200":
|
|
description: Staff member deleted
|
|
"404":
|
|
description: Staff member not found
|
|
/v1/catalog/bookings:
|
|
post:
|
|
tags: [Catalog]
|
|
operationId: createDashboardBooking
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/DashboardBookingRequest"
|
|
responses:
|
|
"201":
|
|
description: Booking created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpcomingBooking"
|
|
"400":
|
|
description: Invalid request
|
|
"409":
|
|
description: Conflict with existing booking
|
|
/v1/catalog/bookings/{bookingID}/cancel:
|
|
post:
|
|
tags: [Catalog]
|
|
operationId: cancelDashboardBooking
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- in: path
|
|
name: bookingID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
responses:
|
|
"200":
|
|
description: Booking cancelled; freed class seats promote the waitlist
|
|
"400":
|
|
description: Booking already cancelled
|
|
"404":
|
|
description: Booking not found
|
|
/v1/catalog/bookings/{bookingID}/promote:
|
|
post:
|
|
tags: [Catalog]
|
|
operationId: promoteDashboardBooking
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- in: path
|
|
name: bookingID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
requestBody:
|
|
required: false
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/PromoteBookingRequest"
|
|
responses:
|
|
"200":
|
|
description: Waitlisted booking confirmed; customer notified
|
|
"400":
|
|
description: Booking is not on a waitlist
|
|
"404":
|
|
description: Booking not found
|
|
"409":
|
|
description: Session at capacity; retry with allowOverCapacity to override
|
|
/v1/catalog/blocked-days:
|
|
get:
|
|
tags: [Catalog]
|
|
operationId: listBlockedDays
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Blocked days
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/BlockedDay"
|
|
post:
|
|
tags: [Catalog]
|
|
operationId: createBlockedDay
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpsertBlockedDayRequest"
|
|
responses:
|
|
"201":
|
|
description: Blocked day created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/BlockedDay"
|
|
/v1/catalog/blocked-days/{blockedDayID}:
|
|
put:
|
|
tags: [Catalog]
|
|
operationId: updateBlockedDay
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/blockedDayID"
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpsertBlockedDayRequest"
|
|
responses:
|
|
"200":
|
|
description: Blocked day updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/BlockedDay"
|
|
"404":
|
|
description: Not found
|
|
delete:
|
|
tags: [Catalog]
|
|
operationId: deleteBlockedDay
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/blockedDayID"
|
|
responses:
|
|
"200":
|
|
description: Blocked day deleted
|
|
"404":
|
|
description: Not found
|
|
/v1/catalog/customers:
|
|
get:
|
|
tags: [Catalog]
|
|
operationId: listCustomers
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- in: query
|
|
name: query
|
|
schema:
|
|
type: string
|
|
- in: query
|
|
name: limit
|
|
schema:
|
|
type: integer
|
|
default: 50
|
|
responses:
|
|
"200":
|
|
description: Tenant customers
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/Customer"
|
|
post:
|
|
tags: [Catalog]
|
|
operationId: createCustomer
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpsertCustomerRequest"
|
|
responses:
|
|
"201":
|
|
description: Customer created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Customer"
|
|
/v1/catalog/customers/{customerID}:
|
|
put:
|
|
tags: [Catalog]
|
|
operationId: updateCustomer
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/customerID"
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpsertCustomerRequest"
|
|
responses:
|
|
"200":
|
|
description: Customer updated
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Customer"
|
|
"404":
|
|
description: Not found
|
|
delete:
|
|
tags: [Catalog]
|
|
operationId: deleteCustomer
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- $ref: "#/components/parameters/customerID"
|
|
responses:
|
|
"200":
|
|
description: Customer deleted
|
|
"404":
|
|
description: Not found
|
|
/v1/catalog/working-hours:
|
|
get:
|
|
tags: [Catalog]
|
|
operationId: listWorkingHours
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Weekly working hours
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/WorkingHours"
|
|
/v1/catalog/working-hours/{dayOfWeek}:
|
|
put:
|
|
tags: [Catalog]
|
|
operationId: updateWorkingHours
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- in: path
|
|
name: dayOfWeek
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
minimum: 0
|
|
maximum: 6
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateWorkingHoursRequest"
|
|
responses:
|
|
"200":
|
|
description: Working hours updated
|
|
"400":
|
|
description: Invalid request
|
|
/v1/catalog/staff/{staffID}/working-hours:
|
|
get:
|
|
tags: [Catalog]
|
|
operationId: listStaffWorkingHours
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- in: path
|
|
name: staffID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
responses:
|
|
"200":
|
|
description: Staff member's weekly working hours
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/WorkingHours"
|
|
"404":
|
|
description: Staff member not found
|
|
/v1/catalog/staff/{staffID}/working-hours/{dayOfWeek}:
|
|
put:
|
|
tags: [Catalog]
|
|
operationId: updateStaffWorkingHours
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- in: path
|
|
name: staffID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
- in: path
|
|
name: dayOfWeek
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
minimum: 0
|
|
maximum: 6
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpdateWorkingHoursRequest"
|
|
responses:
|
|
"200":
|
|
description: Staff working hours updated
|
|
"400":
|
|
description: Invalid request
|
|
"404":
|
|
description: Staff member not found
|
|
/v1/catalog/class-templates:
|
|
get:
|
|
tags: [Catalog]
|
|
operationId: listClassTemplates
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Class templates
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/ClassTemplate"
|
|
post:
|
|
tags: [Catalog]
|
|
operationId: createClassTemplate
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpsertClassTemplateRequest"
|
|
responses:
|
|
"201":
|
|
description: Class template created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ClassTemplate"
|
|
"400":
|
|
description: Invalid request
|
|
/v1/catalog/class-templates/{templateID}:
|
|
put:
|
|
tags: [Catalog]
|
|
operationId: updateClassTemplate
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- in: path
|
|
name: templateID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpsertClassTemplateRequest"
|
|
responses:
|
|
"200":
|
|
description: Updated class template
|
|
"400":
|
|
description: Invalid input
|
|
"404":
|
|
description: Template not found
|
|
delete:
|
|
tags: [Catalog]
|
|
operationId: deleteClassTemplate
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- in: path
|
|
name: templateID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
responses:
|
|
"200":
|
|
description: Class template deleted
|
|
"404":
|
|
description: Class template not found
|
|
/v1/catalog/class-sessions:
|
|
get:
|
|
tags: [Catalog]
|
|
operationId: listClassSessions
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Upcoming class sessions
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/ClassSession"
|
|
post:
|
|
tags: [Catalog]
|
|
operationId: createClassSession
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CreateClassSessionRequest"
|
|
responses:
|
|
"201":
|
|
description: Class session created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ClassSession"
|
|
"400":
|
|
description: Invalid request
|
|
"404":
|
|
description: Template or location not found
|
|
/v1/catalog/class-sessions/{sessionID}:
|
|
delete:
|
|
tags: [Catalog]
|
|
operationId: deleteClassSession
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- in: path
|
|
name: sessionID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
responses:
|
|
"200":
|
|
description: Class session deleted
|
|
"404":
|
|
description: Class session not found
|
|
/v1/catalog/class-sessions/{sessionID}/roster:
|
|
get:
|
|
tags: [Catalog]
|
|
operationId: getSessionRoster
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- in: path
|
|
name: sessionID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
responses:
|
|
"200":
|
|
description: Confirmed attendees and FIFO waitlist for the session
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SessionRoster"
|
|
"404":
|
|
description: Class session not found
|
|
# ---------------- Calendar ----------------
|
|
/v1/calendar/feed:
|
|
get:
|
|
tags: [Calendar]
|
|
operationId: getCalendarFeedInfo
|
|
security:
|
|
- bearerAuth: []
|
|
summary: Feed tokens for the tenant and every staff member
|
|
description: >-
|
|
Returns the read-only ICS feed token for the whole tenant plus one per
|
|
staff member. Tokens are secrets — anyone holding a feed URL can read
|
|
that calendar, same model as private Google/Outlook calendar links.
|
|
responses:
|
|
"200":
|
|
description: Current feed tokens
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CalendarFeedInfo"
|
|
"404":
|
|
description: Tenant membership not found
|
|
/v1/calendar/feed/rotate:
|
|
post:
|
|
tags: [Calendar]
|
|
operationId: rotateCalendarFeedToken
|
|
security:
|
|
- bearerAuth: []
|
|
summary: Invalidate a feed URL and issue a fresh token
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [scope]
|
|
properties:
|
|
scope:
|
|
type: string
|
|
enum: [tenant, staff]
|
|
staffId:
|
|
type: string
|
|
format: uuid
|
|
description: Required when scope is staff; must belong to the tenant
|
|
responses:
|
|
"200":
|
|
description: Newly issued token and feed path
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CalendarFeedEntry"
|
|
"400":
|
|
description: Invalid scope or foreign staff member
|
|
"404":
|
|
description: Tenant membership not found
|
|
/v1/activity:
|
|
get:
|
|
tags: [Activity]
|
|
operationId: listActivityEvents
|
|
security:
|
|
- bearerAuth: []
|
|
summary: Recent tenant activity feed
|
|
description: >-
|
|
Audit feed of booking lifecycle events — created, cancelled,
|
|
rescheduled, moved, promoted — with the actor (staff member, customer,
|
|
or system) for each change. Newest first.
|
|
parameters:
|
|
- name: limit
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
default: 50
|
|
maximum: 100
|
|
responses:
|
|
"200":
|
|
description: Activity events, newest first
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ActivityFeedResponse"
|
|
"404":
|
|
description: Tenant membership not found
|
|
/v1/activity/read:
|
|
post:
|
|
tags: [Activity]
|
|
operationId: markActivityRead
|
|
security:
|
|
- bearerAuth: []
|
|
summary: Mark all activity events as read
|
|
description: >-
|
|
Persists the caller's read cursor — activity events at or before the
|
|
returned timestamp count as read for this user on subsequent loads.
|
|
responses:
|
|
"200":
|
|
description: Read cursor persisted
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [readAt]
|
|
properties:
|
|
readAt:
|
|
type: string
|
|
format: date-time
|
|
"404":
|
|
description: Tenant membership not found
|
|
/v1/metrics/staff:
|
|
get:
|
|
tags: [Metrics]
|
|
operationId: getStaffMetrics
|
|
security:
|
|
- bearerAuth: []
|
|
summary: Per-staff appointment metrics over a trailing window
|
|
description: >-
|
|
Bookings, completions, cancellations, utilization, and revenue per
|
|
staff member. Only appointment bookings carry a staff assignment —
|
|
class attendance is excluded by design. Every active staff member
|
|
appears, including zero rows.
|
|
parameters:
|
|
- name: days
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
default: 30
|
|
maximum: 365
|
|
responses:
|
|
"200":
|
|
description: Per-staff metrics
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/StaffMetricsResponse"
|
|
"404":
|
|
description: Tenant membership not found
|
|
/v1/export/bookings.csv:
|
|
get:
|
|
tags: [Export]
|
|
operationId: exportBookingsCsv
|
|
security:
|
|
- bearerAuth: []
|
|
summary: Export tenant bookings as CSV
|
|
description: >-
|
|
Full booking export for accounting and backup — UTF-8 with BOM so
|
|
Czech diacritics open correctly in Excel. Optional `from`/`to`
|
|
(YYYY-MM-DD) bounds the export window; `to` is inclusive.
|
|
parameters:
|
|
- name: from
|
|
in: query
|
|
schema:
|
|
type: string
|
|
format: date
|
|
- name: to
|
|
in: query
|
|
schema:
|
|
type: string
|
|
format: date
|
|
responses:
|
|
"200":
|
|
description: CSV file download
|
|
content:
|
|
text/csv:
|
|
schema:
|
|
type: string
|
|
"404":
|
|
description: Tenant membership not found
|
|
/v1/export/customers.csv:
|
|
get:
|
|
tags: [Export]
|
|
operationId: exportCustomersCsv
|
|
security:
|
|
- bearerAuth: []
|
|
summary: Export tenant customers as CSV
|
|
description: Full customer list export — UTF-8 with BOM for Excel.
|
|
responses:
|
|
"200":
|
|
description: CSV file download
|
|
content:
|
|
text/csv:
|
|
schema:
|
|
type: string
|
|
"404":
|
|
description: Tenant membership not found
|
|
/v1/export/staff-report.csv:
|
|
get:
|
|
tags: [Export]
|
|
operationId: exportStaffReportCsv
|
|
security:
|
|
- bearerAuth: []
|
|
summary: Export per-staff performance report as CSV
|
|
description: >-
|
|
Summarized per-staff metrics — bookings, completed, cancelled, booked
|
|
and capacity minutes, capacity-based utilization, revenue — over a
|
|
trailing window. Same aggregation as `GET /v1/metrics/staff`.
|
|
parameters:
|
|
- name: days
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
default: 30
|
|
maximum: 365
|
|
responses:
|
|
"200":
|
|
description: CSV file download
|
|
content:
|
|
text/csv:
|
|
schema:
|
|
type: string
|
|
"404":
|
|
description: Tenant membership not found
|
|
/v1/export/service-report.csv:
|
|
get:
|
|
tags: [Export]
|
|
operationId: exportServiceReportCsv
|
|
security:
|
|
- bearerAuth: []
|
|
summary: Export per-service booking report as CSV
|
|
description: >-
|
|
Bookings, cancellations, and revenue aggregated per catalog service
|
|
over a trailing window. Class bookings roll into a final
|
|
"class sessions" row when present.
|
|
parameters:
|
|
- name: days
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
default: 30
|
|
maximum: 365
|
|
responses:
|
|
"200":
|
|
description: CSV file download
|
|
content:
|
|
text/csv:
|
|
schema:
|
|
type: string
|
|
"404":
|
|
description: Tenant membership not found
|
|
/v1/public/calendar/{token}:
|
|
get:
|
|
tags: [Calendar]
|
|
operationId: getCalendarFeed
|
|
summary: Read-only ICS calendar feed
|
|
description: >-
|
|
Token-as-secret calendar subscription endpoint. `{token}` may carry a
|
|
`.ics` suffix, which is ignored. The feed covers bookings from 30 days
|
|
back to 180 days ahead; cancelled bookings appear with
|
|
STATUS:CANCELLED and waitlisted entries as TENTATIVE/TRANSPARENT.
|
|
parameters:
|
|
- in: path
|
|
name: token
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: iCalendar document
|
|
content:
|
|
text/calendar:
|
|
schema:
|
|
type: string
|
|
"404":
|
|
description: Unknown or rotated feed token
|
|
# ---------------- Billing ----------------
|
|
/v1/billing/subscription:
|
|
get:
|
|
tags: [Billing]
|
|
operationId: getSubscriptionSnapshot
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Current subscription snapshot and entitlements
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SubscriptionSnapshot"
|
|
"401":
|
|
description: Unauthorized
|
|
/v1/billing/checkout:
|
|
post:
|
|
tags: [Billing]
|
|
operationId: createBillingCheckout
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CheckoutSessionRequest"
|
|
responses:
|
|
"200":
|
|
description: Stripe checkout session
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CheckoutLaunchResponse"
|
|
"400":
|
|
description: Invalid request
|
|
"401":
|
|
description: Unauthorized
|
|
"503":
|
|
description: Billing provider unavailable
|
|
/v1/billing/refresh:
|
|
post:
|
|
tags: [Billing]
|
|
operationId: refreshSubscriptionSnapshot
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Refreshed snapshot
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SubscriptionSnapshot"
|
|
"401":
|
|
description: Unauthorized
|
|
"503":
|
|
description: Billing provider unavailable
|
|
/v1/billing/portal:
|
|
post:
|
|
tags: [Billing]
|
|
operationId: createBillingPortalSession
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Stripe customer portal session
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/PortalSessionResponse"
|
|
"400":
|
|
description: Billing customer missing
|
|
"401":
|
|
description: Unauthorized
|
|
"503":
|
|
description: Billing provider unavailable
|
|
/v1/webhooks/stripe:
|
|
post:
|
|
tags: [Billing]
|
|
operationId: handleStripeWebhook
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
responses:
|
|
"200":
|
|
description: Webhook accepted
|
|
"400":
|
|
description: Invalid signature or payload
|
|
# ---------------- SMS ----------------
|
|
/v1/sms/settings:
|
|
get:
|
|
tags: [SMS]
|
|
operationId: getSmsSettings
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: SMS settings and current-month usage
|
|
post:
|
|
tags: [SMS]
|
|
operationId: updateSmsSettings
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
enabled:
|
|
type: boolean
|
|
sender:
|
|
type: string
|
|
monthlyLimit:
|
|
type: integer
|
|
responses:
|
|
"200":
|
|
description: SMS settings saved
|
|
/v1/sms/send:
|
|
post:
|
|
tags: [SMS]
|
|
operationId: sendSms
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [to, message]
|
|
properties:
|
|
to:
|
|
type: string
|
|
message:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: SMS queued
|
|
/v1/sms/usage:
|
|
get:
|
|
tags: [SMS]
|
|
operationId: getSmsUsage
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- in: query
|
|
name: month
|
|
schema:
|
|
type: string
|
|
example: "2026-09"
|
|
responses:
|
|
"200":
|
|
description: Usage for the month
|
|
/v1/sms/history:
|
|
get:
|
|
tags: [SMS]
|
|
operationId: getSmsHistory
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Recent SMS logs
|
|
/v1/sms/invoices:
|
|
get:
|
|
tags: [SMS]
|
|
operationId: listSmsInvoices
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Monthly invoice reports
|
|
# ---------------- Jobs ----------------
|
|
/v1/internal/jobs/reminders/dispatch:
|
|
post:
|
|
tags: [Jobs]
|
|
operationId: dispatchReminderJobs
|
|
security:
|
|
- jobRunnerKey: []
|
|
requestBody:
|
|
required: false
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/DispatchReminderJobsRequest"
|
|
responses:
|
|
"200":
|
|
description: Reminder jobs dispatched
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/DispatchReminderJobsResponse"
|
|
"401":
|
|
description: Unauthorized
|
|
/v1/internal/jobs/sms/invoices:
|
|
post:
|
|
tags: [Jobs]
|
|
operationId: generateSmsInvoices
|
|
security:
|
|
- jobRunnerKey: []
|
|
responses:
|
|
"200":
|
|
description: Monthly SMS invoices generated
|
|
"401":
|
|
description: Unauthorized
|
|
components:
|
|
securitySchemes:
|
|
bearerAuth:
|
|
type: http
|
|
scheme: bearer
|
|
bearerFormat: JWT
|
|
jobRunnerKey:
|
|
type: apiKey
|
|
in: header
|
|
name: X-Bookra-Job-Key
|
|
adminAuth:
|
|
type: apiKey
|
|
in: header
|
|
name: X-Admin-Key
|
|
description: Requires matching X-Admin-Email header as well.
|
|
parameters:
|
|
locationID:
|
|
in: path
|
|
name: locationID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
blockedDayID:
|
|
in: path
|
|
name: blockedDayID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
customerID:
|
|
in: path
|
|
name: customerID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
serviceID:
|
|
in: path
|
|
name: serviceID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
staffID:
|
|
in: path
|
|
name: staffID
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
schemas:
|
|
HealthResponse:
|
|
type: object
|
|
required: [status, environment]
|
|
properties:
|
|
status:
|
|
type: string
|
|
example: ok
|
|
environment:
|
|
type: string
|
|
example: staging
|
|
databaseConfigured:
|
|
type: boolean
|
|
PublicConfig:
|
|
type: object
|
|
required: [environment, neonAuthEnabled, apiUrl, demoMode]
|
|
properties:
|
|
environment:
|
|
type: string
|
|
neonAuthEnabled:
|
|
type: boolean
|
|
apiUrl:
|
|
type: string
|
|
format: uri
|
|
demoMode:
|
|
type: boolean
|
|
AuthResponse:
|
|
type: object
|
|
required: [accessToken, refreshToken]
|
|
properties:
|
|
accessToken:
|
|
type: string
|
|
refreshToken:
|
|
type: string
|
|
expiresIn:
|
|
type: integer
|
|
user:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
email:
|
|
type: string
|
|
name:
|
|
type: string
|
|
TimeSlot:
|
|
type: object
|
|
required: [startsAt, endsAt, mode]
|
|
properties:
|
|
serviceId:
|
|
type: string
|
|
format: uuid
|
|
nullable: true
|
|
classSessionId:
|
|
type: string
|
|
format: uuid
|
|
nullable: true
|
|
staffId:
|
|
type: string
|
|
format: uuid
|
|
nullable: true
|
|
staffName:
|
|
type: string
|
|
nullable: true
|
|
locationId:
|
|
type: string
|
|
format: uuid
|
|
nullable: true
|
|
startsAt:
|
|
type: string
|
|
format: date-time
|
|
endsAt:
|
|
type: string
|
|
format: date-time
|
|
mode:
|
|
type: string
|
|
enum: [appointment, class]
|
|
label:
|
|
type: string
|
|
remainingCapacity:
|
|
type: integer
|
|
nullable: true
|
|
PublicAvailabilityResponse:
|
|
type: object
|
|
required: [tenantSlug, timezone, locale, slots]
|
|
properties:
|
|
tenantSlug:
|
|
type: string
|
|
timezone:
|
|
type: string
|
|
locale:
|
|
type: string
|
|
enum: [cs, en]
|
|
slots:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/TimeSlot"
|
|
PublicCatalogResponse:
|
|
type: object
|
|
required: [tenantSlug, services, staff]
|
|
properties:
|
|
tenantSlug:
|
|
type: string
|
|
services:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/PublicService"
|
|
staff:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/PublicStaff"
|
|
PublicService:
|
|
type: object
|
|
required: [id, name, durationMinutes, priceCents]
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
name:
|
|
type: string
|
|
durationMinutes:
|
|
type: integer
|
|
priceCents:
|
|
type: integer
|
|
PublicStaff:
|
|
type: object
|
|
required: [id, displayName]
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
displayName:
|
|
type: string
|
|
CreateBookingRequest:
|
|
type: object
|
|
required: [tenantSlug, bookingMode, customerName, customerEmail, startsAt, endsAt]
|
|
properties:
|
|
tenantSlug:
|
|
type: string
|
|
bookingMode:
|
|
type: string
|
|
enum: [appointment, class]
|
|
serviceId:
|
|
type: string
|
|
format: uuid
|
|
classSessionId:
|
|
type: string
|
|
format: uuid
|
|
staffId:
|
|
type: string
|
|
format: uuid
|
|
locationId:
|
|
type: string
|
|
format: uuid
|
|
customerName:
|
|
type: string
|
|
customerEmail:
|
|
type: string
|
|
format: email
|
|
notes:
|
|
type: string
|
|
startsAt:
|
|
type: string
|
|
format: date-time
|
|
endsAt:
|
|
type: string
|
|
format: date-time
|
|
CreateBookingResponse:
|
|
type: object
|
|
required: [bookingId, reference, status]
|
|
properties:
|
|
bookingId:
|
|
type: string
|
|
format: uuid
|
|
reference:
|
|
type: string
|
|
status:
|
|
type: string
|
|
enum: [confirmed, waitlisted]
|
|
manageToken:
|
|
type: string
|
|
description: Token required to view/reschedule/cancel the booking
|
|
DashboardKPI:
|
|
type: object
|
|
required: [code, label, value]
|
|
properties:
|
|
code:
|
|
type: string
|
|
label:
|
|
type: string
|
|
value:
|
|
type: string
|
|
change:
|
|
type: string
|
|
trend:
|
|
type: string
|
|
enum: [up, down, neutral]
|
|
DashboardSummary:
|
|
type: object
|
|
required: [tenantName, tenantSlug, locale, timezone, planCode, publicBookingUrl, setupCompletion, kpis]
|
|
properties:
|
|
tenantName:
|
|
type: string
|
|
tenantSlug:
|
|
type: string
|
|
locale:
|
|
type: string
|
|
timezone:
|
|
type: string
|
|
planCode:
|
|
type: string
|
|
publicBookingUrl:
|
|
type: string
|
|
setupCompletion:
|
|
type: integer
|
|
kpis:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/DashboardKPI"
|
|
upcomingBookings:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/UpcomingBooking"
|
|
widgetSnippets:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/WidgetSnippet"
|
|
tracking:
|
|
$ref: "#/components/schemas/TrackingStatus"
|
|
UpcomingBooking:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
reference:
|
|
type: string
|
|
customerName:
|
|
type: string
|
|
customerEmail:
|
|
type: string
|
|
service:
|
|
type: string
|
|
location:
|
|
type: string
|
|
startsAt:
|
|
type: string
|
|
format: date-time
|
|
endsAt:
|
|
type: string
|
|
format: date-time
|
|
status:
|
|
type: string
|
|
label:
|
|
type: string
|
|
notes:
|
|
type: string
|
|
classSessionId:
|
|
type: string
|
|
format: uuid
|
|
waitlistPosition:
|
|
type: integer
|
|
description: 1-based queue spot for waitlisted class bookings; absent otherwise
|
|
CustomerBookingView:
|
|
type: object
|
|
properties:
|
|
reference:
|
|
type: string
|
|
customerName:
|
|
type: string
|
|
customerEmail:
|
|
type: string
|
|
service:
|
|
type: string
|
|
businessName:
|
|
type: string
|
|
tenantSlug:
|
|
type: string
|
|
serviceId:
|
|
type: string
|
|
format: uuid
|
|
staffId:
|
|
type: string
|
|
format: uuid
|
|
classSessionId:
|
|
type: string
|
|
format: uuid
|
|
description: Present for session-bound bookings, which cannot be rescheduled to arbitrary times
|
|
startsAt:
|
|
type: string
|
|
format: date-time
|
|
endsAt:
|
|
type: string
|
|
format: date-time
|
|
location:
|
|
type: string
|
|
status:
|
|
type: string
|
|
notes:
|
|
type: string
|
|
waitlistPosition:
|
|
type: integer
|
|
description: 1-based queue spot for waitlisted class bookings; absent otherwise
|
|
SessionMoveTarget:
|
|
type: object
|
|
properties:
|
|
sessionId:
|
|
type: string
|
|
format: uuid
|
|
title:
|
|
type: string
|
|
startsAt:
|
|
type: string
|
|
format: date-time
|
|
endsAt:
|
|
type: string
|
|
format: date-time
|
|
location:
|
|
type: string
|
|
capacity:
|
|
type: integer
|
|
confirmedCount:
|
|
type: integer
|
|
waitlistCount:
|
|
type: integer
|
|
remaining:
|
|
type: integer
|
|
description: Free confirmed seats; 0 or negative means the move lands on the waitlist
|
|
CalendarFeedEntry:
|
|
type: object
|
|
properties:
|
|
token:
|
|
type: string
|
|
description: Feed secret — embed in the subscription URL
|
|
feedPath:
|
|
type: string
|
|
description: API-relative path ending in .ics; prefix with the API base URL
|
|
StaffCalendarFeedEntry:
|
|
type: object
|
|
properties:
|
|
staffId:
|
|
type: string
|
|
format: uuid
|
|
displayName:
|
|
type: string
|
|
token:
|
|
type: string
|
|
feedPath:
|
|
type: string
|
|
CalendarFeedInfo:
|
|
type: object
|
|
properties:
|
|
tenant:
|
|
$ref: "#/components/schemas/CalendarFeedEntry"
|
|
staff:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/StaffCalendarFeedEntry"
|
|
ActivityEvent:
|
|
type: object
|
|
required: [id, actorKind, action, entityType, summary, createdAt]
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
actorKind:
|
|
type: string
|
|
enum: [staff, customer, system]
|
|
description: Who performed the change — dashboard user, self-service customer, or an automatic transition
|
|
actorName:
|
|
type: string
|
|
action:
|
|
type: string
|
|
description: Dotted verb, e.g. booking.created, booking.cancelled, booking.promoted
|
|
entityType:
|
|
type: string
|
|
entityId:
|
|
type: string
|
|
format: uuid
|
|
summary:
|
|
type: string
|
|
description: Human-readable label, e.g. "Alice Johnson — Relaxation massage"
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
ActivityFeedResponse:
|
|
type: object
|
|
required: [events, unreadCount]
|
|
properties:
|
|
events:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/ActivityEvent"
|
|
unreadCount:
|
|
type: integer
|
|
description: Events newer than the caller's persisted read cursor
|
|
readAt:
|
|
type: string
|
|
format: date-time
|
|
description: Caller's read cursor; absent until the first mark-read
|
|
StaffMetric:
|
|
type: object
|
|
required: [staffId, displayName, bookings, completed, cancelled, bookedMinutes, capacityMinutes, utilizationPercent, revenueCents]
|
|
properties:
|
|
staffId:
|
|
type: string
|
|
format: uuid
|
|
displayName:
|
|
type: string
|
|
bookings:
|
|
type: integer
|
|
completed:
|
|
type: integer
|
|
cancelled:
|
|
type: integer
|
|
bookedMinutes:
|
|
type: integer
|
|
description: Sum of non-cancelled booking durations in the window
|
|
capacityMinutes:
|
|
type: integer
|
|
description: Sum of the member's availability-rule minutes across the window, evaluated on local dates in the tenant timezone
|
|
utilizationPercent:
|
|
type: integer
|
|
description: bookedMinutes / capacityMinutes as a percentage; 0 when the member has no availability rules
|
|
revenueCents:
|
|
type: integer
|
|
format: int64
|
|
description: Sum of linked service prices for non-cancelled bookings
|
|
StaffMetricsResponse:
|
|
type: object
|
|
required: [from, to, staff]
|
|
properties:
|
|
from:
|
|
type: string
|
|
format: date-time
|
|
to:
|
|
type: string
|
|
format: date-time
|
|
staff:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/StaffMetric"
|
|
DashboardBookingRequest:
|
|
type: object
|
|
required: [customerName, customerEmail, startsAt]
|
|
properties:
|
|
customerName:
|
|
type: string
|
|
customerEmail:
|
|
type: string
|
|
format: email
|
|
customerPhone:
|
|
type: string
|
|
serviceId:
|
|
type: string
|
|
format: uuid
|
|
locationId:
|
|
type: string
|
|
format: uuid
|
|
startsAt:
|
|
type: string
|
|
format: date-time
|
|
notes:
|
|
type: string
|
|
WidgetSnippet:
|
|
type: object
|
|
properties:
|
|
kind:
|
|
type: string
|
|
code:
|
|
type: string
|
|
TrackingStatus:
|
|
type: object
|
|
properties:
|
|
provider:
|
|
type: string
|
|
connected:
|
|
type: boolean
|
|
siteId:
|
|
type: string
|
|
message:
|
|
type: string
|
|
BrandProfile:
|
|
type: object
|
|
properties:
|
|
name:
|
|
type: string
|
|
siteUrl:
|
|
type: string
|
|
logoUrl:
|
|
type: string
|
|
primaryColor:
|
|
type: string
|
|
UpdateBrandRequest:
|
|
type: object
|
|
properties:
|
|
name:
|
|
type: string
|
|
siteUrl:
|
|
type: string
|
|
logoUrl:
|
|
type: string
|
|
primaryColor:
|
|
type: string
|
|
EmailTemplate:
|
|
type: object
|
|
properties:
|
|
type:
|
|
type: string
|
|
enum: [confirmation, reminder, reschedule, cancellation]
|
|
subject:
|
|
type: string
|
|
bodyText:
|
|
type: string
|
|
isEnabled:
|
|
type: boolean
|
|
UpdateEmailTemplateRequest:
|
|
type: object
|
|
required: [type]
|
|
properties:
|
|
type:
|
|
type: string
|
|
enum: [confirmation, reminder, reschedule, cancellation]
|
|
subject:
|
|
type: string
|
|
body:
|
|
type: string
|
|
isEnabled:
|
|
type: boolean
|
|
TenantBootstrap:
|
|
type: object
|
|
required: [tenantId, tenantName, preset, locale, timezone, currentUser]
|
|
properties:
|
|
tenantId:
|
|
type: string
|
|
format: uuid
|
|
tenantName:
|
|
type: string
|
|
tenantSlug:
|
|
type: string
|
|
preset:
|
|
type: string
|
|
locale:
|
|
type: string
|
|
timezone:
|
|
type: string
|
|
planCode:
|
|
type: string
|
|
onboardingCompleted:
|
|
type: boolean
|
|
brand:
|
|
$ref: "#/components/schemas/BrandProfile"
|
|
currentUser:
|
|
type: object
|
|
required: [subject, role]
|
|
properties:
|
|
subject:
|
|
type: string
|
|
email:
|
|
type: string
|
|
format: email
|
|
name:
|
|
type: string
|
|
role:
|
|
type: string
|
|
OnboardTenantRequest:
|
|
type: object
|
|
required: [name, slug, preset, locale, timezone]
|
|
properties:
|
|
name:
|
|
type: string
|
|
slug:
|
|
type: string
|
|
preset:
|
|
type: string
|
|
enum: [salon, clinic, massage, repair, studio]
|
|
locale:
|
|
type: string
|
|
enum: [cs, en]
|
|
timezone:
|
|
type: string
|
|
brand:
|
|
$ref: "#/components/schemas/BrandProfile"
|
|
locationName:
|
|
type: string
|
|
bookingDefaults:
|
|
$ref: "#/components/schemas/BookingDefaultsRequest"
|
|
availabilityBlocks:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/AvailabilityBlockRequest"
|
|
teamInvites:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/TeamInviteRequest"
|
|
BookingDefaultsRequest:
|
|
type: object
|
|
properties:
|
|
serviceName:
|
|
type: string
|
|
durationMinutes:
|
|
type: integer
|
|
bufferBeforeMinutes:
|
|
type: integer
|
|
bufferAfterMinutes:
|
|
type: integer
|
|
cancelWindowHours:
|
|
type: integer
|
|
AvailabilityBlockRequest:
|
|
type: object
|
|
required: [dayOfWeek, startsLocal, endsLocal]
|
|
properties:
|
|
dayOfWeek:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 7
|
|
startsLocal:
|
|
type: string
|
|
example: "09:00"
|
|
endsLocal:
|
|
type: string
|
|
example: "17:00"
|
|
busy:
|
|
type: boolean
|
|
TeamInviteRequest:
|
|
type: object
|
|
required: [email]
|
|
properties:
|
|
email:
|
|
type: string
|
|
format: email
|
|
role:
|
|
type: string
|
|
Location:
|
|
type: object
|
|
required: [id, name]
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
name:
|
|
type: string
|
|
type:
|
|
type: string
|
|
enum: [room, private, hall]
|
|
capacity:
|
|
type: integer
|
|
address:
|
|
type: string
|
|
UpsertLocationRequest:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name:
|
|
type: string
|
|
type:
|
|
type: string
|
|
enum: [room, private, hall]
|
|
capacity:
|
|
type: integer
|
|
address:
|
|
type: string
|
|
Service:
|
|
type: object
|
|
required: [id, tenantId, name, durationMinutes, bufferBeforeMinutes, bufferAfterMinutes, priceCents]
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
tenantId:
|
|
type: string
|
|
format: uuid
|
|
name:
|
|
type: string
|
|
durationMinutes:
|
|
type: integer
|
|
description: Appointment length in minutes (1-1440)
|
|
bufferBeforeMinutes:
|
|
type: integer
|
|
description: Blocked time before the appointment (0-480)
|
|
bufferAfterMinutes:
|
|
type: integer
|
|
description: Blocked time after the appointment (0-480)
|
|
priceCents:
|
|
type: integer
|
|
description: Price in the tenant's currency minor units
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
UpsertServiceRequest:
|
|
type: object
|
|
required: [name, durationMinutes]
|
|
properties:
|
|
name:
|
|
type: string
|
|
durationMinutes:
|
|
type: integer
|
|
bufferBeforeMinutes:
|
|
type: integer
|
|
bufferAfterMinutes:
|
|
type: integer
|
|
priceCents:
|
|
type: integer
|
|
StaffMember:
|
|
type: object
|
|
required: [id, tenantId, displayName, active]
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
tenantId:
|
|
type: string
|
|
format: uuid
|
|
locationId:
|
|
type: string
|
|
format: uuid
|
|
nullable: true
|
|
displayName:
|
|
type: string
|
|
active:
|
|
type: boolean
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
UpsertStaffRequest:
|
|
type: object
|
|
required: [displayName]
|
|
properties:
|
|
displayName:
|
|
type: string
|
|
locationId:
|
|
type: string
|
|
format: uuid
|
|
nullable: true
|
|
description: Omit to keep the current assignment; empty string clears it
|
|
active:
|
|
type: boolean
|
|
BlockedDay:
|
|
type: object
|
|
required: [id, date, type]
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
date:
|
|
type: string
|
|
format: date-time
|
|
type:
|
|
type: string
|
|
enum: [closure, holiday, maintenance]
|
|
reason:
|
|
type: string
|
|
UpsertBlockedDayRequest:
|
|
type: object
|
|
required: [date, type]
|
|
properties:
|
|
date:
|
|
type: string
|
|
format: date-time
|
|
type:
|
|
type: string
|
|
enum: [closure, holiday, maintenance]
|
|
reason:
|
|
type: string
|
|
Customer:
|
|
type: object
|
|
required: [id, name, email]
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
name:
|
|
type: string
|
|
email:
|
|
type: string
|
|
format: email
|
|
phone:
|
|
type: string
|
|
notes:
|
|
type: string
|
|
bookingsCount:
|
|
type: integer
|
|
lastBookingAt:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
UpsertCustomerRequest:
|
|
type: object
|
|
required: [name, email]
|
|
properties:
|
|
name:
|
|
type: string
|
|
email:
|
|
type: string
|
|
format: email
|
|
phone:
|
|
type: string
|
|
notes:
|
|
type: string
|
|
WorkingHours:
|
|
type: object
|
|
properties:
|
|
dayOfWeek:
|
|
type: integer
|
|
open:
|
|
type: string
|
|
example: "09:00"
|
|
close:
|
|
type: string
|
|
example: "18:00"
|
|
isOpen:
|
|
type: boolean
|
|
UpdateWorkingHoursRequest:
|
|
type: object
|
|
properties:
|
|
open:
|
|
type: string
|
|
example: "09:00"
|
|
close:
|
|
type: string
|
|
example: "18:00"
|
|
isOpen:
|
|
type: boolean
|
|
ClassTemplate:
|
|
type: object
|
|
required: [id, tenantId, title, durationMinutes, capacity, createdAt]
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
tenantId:
|
|
type: string
|
|
format: uuid
|
|
title:
|
|
type: string
|
|
durationMinutes:
|
|
type: integer
|
|
capacity:
|
|
type: integer
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
UpsertClassTemplateRequest:
|
|
type: object
|
|
required: [title, durationMinutes, capacity]
|
|
properties:
|
|
title:
|
|
type: string
|
|
durationMinutes:
|
|
type: integer
|
|
capacity:
|
|
type: integer
|
|
ClassSession:
|
|
type: object
|
|
required: [id, tenantId, templateId, templateTitle, startsAt, endsAt, capacity, bookedCount, waitlistCount]
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
tenantId:
|
|
type: string
|
|
format: uuid
|
|
templateId:
|
|
type: string
|
|
format: uuid
|
|
templateTitle:
|
|
type: string
|
|
locationId:
|
|
type: string
|
|
format: uuid
|
|
locationName:
|
|
type: string
|
|
startsAt:
|
|
type: string
|
|
format: date-time
|
|
endsAt:
|
|
type: string
|
|
format: date-time
|
|
capacity:
|
|
type: integer
|
|
bookedCount:
|
|
type: integer
|
|
waitlistCount:
|
|
type: integer
|
|
SessionRosterEntry:
|
|
type: object
|
|
required: [bookingId, reference, customerName, customerEmail, status, createdAt]
|
|
properties:
|
|
bookingId:
|
|
type: string
|
|
format: uuid
|
|
reference:
|
|
type: string
|
|
customerName:
|
|
type: string
|
|
customerEmail:
|
|
type: string
|
|
customerPhone:
|
|
type: string
|
|
status:
|
|
type: string
|
|
waitlistPosition:
|
|
type: integer
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
SessionRoster:
|
|
type: object
|
|
required: [sessionId, templateTitle, startsAt, endsAt, capacity, confirmed, waitlist]
|
|
properties:
|
|
sessionId:
|
|
type: string
|
|
format: uuid
|
|
templateTitle:
|
|
type: string
|
|
locationName:
|
|
type: string
|
|
startsAt:
|
|
type: string
|
|
format: date-time
|
|
endsAt:
|
|
type: string
|
|
format: date-time
|
|
capacity:
|
|
type: integer
|
|
confirmed:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/SessionRosterEntry"
|
|
waitlist:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/SessionRosterEntry"
|
|
PromoteBookingRequest:
|
|
type: object
|
|
properties:
|
|
allowOverCapacity:
|
|
type: boolean
|
|
description: Confirm even when the session is full (deliberate overbooking)
|
|
CreateClassSessionRequest:
|
|
type: object
|
|
required: [templateId, startsAt]
|
|
properties:
|
|
templateId:
|
|
type: string
|
|
format: uuid
|
|
locationId:
|
|
type: string
|
|
format: uuid
|
|
startsAt:
|
|
type: string
|
|
format: date-time
|
|
capacity:
|
|
type: integer
|
|
PlanEntitlements:
|
|
type: object
|
|
required: [maxLocations, maxStaff, emailReminders, advancedReporting, widgetEmbedding, umamiTracking]
|
|
properties:
|
|
maxLocations:
|
|
type: integer
|
|
maxStaff:
|
|
type: integer
|
|
maxBookingsMonth:
|
|
type: integer
|
|
emailReminders:
|
|
type: boolean
|
|
widgetEmbedding:
|
|
type: boolean
|
|
umamiTracking:
|
|
type: boolean
|
|
advancedReporting:
|
|
type: boolean
|
|
SubscriptionSnapshot:
|
|
type: object
|
|
required: [tenantId, provider, customerId, subscriptionId, status, planCode, currency, priceId, cancelAtPeriodEnd, entitlements, displayPrices, trialDays, checkoutUrlAvailable, syncAvailable, portalAvailable]
|
|
properties:
|
|
tenantId:
|
|
type: string
|
|
format: uuid
|
|
provider:
|
|
type: string
|
|
example: stripe
|
|
customerId:
|
|
type: string
|
|
subscriptionId:
|
|
type: string
|
|
status:
|
|
type: string
|
|
planCode:
|
|
type: string
|
|
currency:
|
|
type: string
|
|
enum: [czk, usd, eur]
|
|
priceId:
|
|
type: string
|
|
cancelAtPeriodEnd:
|
|
type: boolean
|
|
currentPeriodStart:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
currentPeriodEnd:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
paymentMethodBrand:
|
|
type: string
|
|
paymentMethodLast4:
|
|
type: string
|
|
entitlements:
|
|
$ref: "#/components/schemas/PlanEntitlements"
|
|
displayPrices:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/PlanDisplayPrice"
|
|
trialDays:
|
|
type: integer
|
|
lastSyncedAt:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
checkoutUrlAvailable:
|
|
type: boolean
|
|
syncAvailable:
|
|
type: boolean
|
|
portalAvailable:
|
|
type: boolean
|
|
CheckoutSessionRequest:
|
|
type: object
|
|
required: [planCode]
|
|
properties:
|
|
planCode:
|
|
type: string
|
|
enum: [starter, pro, business]
|
|
description: The plan to subscribe to
|
|
currency:
|
|
type: string
|
|
enum: [czk, usd]
|
|
description: Currency for the subscription
|
|
billingInterval:
|
|
type: string
|
|
enum: [monthly, yearly]
|
|
default: monthly
|
|
description: Billing interval. Yearly gets 17% discount.
|
|
PlanDisplayPrice:
|
|
type: object
|
|
required: [currency, amountCents, formatted]
|
|
properties:
|
|
currency:
|
|
type: string
|
|
enum: [czk, usd]
|
|
amountCents:
|
|
type: integer
|
|
description: Monthly price in cents
|
|
formatted:
|
|
type: string
|
|
description: Formatted monthly price string
|
|
yearlyAmountCents:
|
|
type: integer
|
|
description: Yearly price in cents (17% discount)
|
|
yearlyFormatted:
|
|
type: string
|
|
description: Formatted yearly price string
|
|
yearlySavings:
|
|
type: string
|
|
description: Description of yearly savings
|
|
yearlySavingsPercent:
|
|
type: integer
|
|
description: Percentage saved with yearly billing
|
|
CheckoutLaunchResponse:
|
|
type: object
|
|
description: Stripe checkout launch payload — redirect the user to checkoutUrl.
|
|
properties:
|
|
checkoutUrl:
|
|
type: string
|
|
format: uri
|
|
description: Stripe checkout URL (redirect the user to this URL)
|
|
successRedirectUrl:
|
|
type: string
|
|
format: uri
|
|
description: URL to redirect after successful checkout
|
|
cancelRedirectUrl:
|
|
type: string
|
|
format: uri
|
|
description: URL to redirect after cancelled checkout
|
|
PortalSessionResponse:
|
|
type: object
|
|
required: [url]
|
|
properties:
|
|
url:
|
|
type: string
|
|
format: uri
|
|
DispatchReminderJobsRequest:
|
|
type: object
|
|
properties:
|
|
limit:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 200
|
|
DispatchReminderJobsResponse:
|
|
type: object
|
|
required: [processedCount, sentCount, failedCount, skippedCount]
|
|
properties:
|
|
processedCount:
|
|
type: integer
|
|
sentCount:
|
|
type: integer
|
|
failedCount:
|
|
type: integer
|
|
skippedCount:
|
|
type: integer
|