mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 07:41:16 +00:00
The public booking page showed a capped flat preview of slots with no way to pick a service, day, or staff member. Replaces it with a stepped flow: pick service, optional staff preference, pick a day, pick a time. Backend: availability accepts serviceId/staffId/date filters (default response unchanged for the embed widget), a new public catalog endpoint exposes bookable services and active staff, and slots carry staffName. Slot generation and booking creation now honor tenant-wide and per-staff blocked days; appointment requests must fit inside a matching availability rule and reference tenant-owned, active staff and locations. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2038 lines
51 KiB
YAML
2038 lines
51 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: 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
|
|
"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
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [token, startsAt]
|
|
properties:
|
|
token:
|
|
type: string
|
|
startsAt:
|
|
type: string
|
|
format: date-time
|
|
endsAt:
|
|
type: string
|
|
format: date-time
|
|
responses:
|
|
"200":
|
|
description: Booking rescheduled
|
|
"403":
|
|
description: Invalid manage token
|
|
"409":
|
|
description: New slot conflicts
|
|
/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/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
|
|
# ---------------- 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
|
|
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
|
|
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
|
|
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]
|
|
properties:
|
|
processedCount:
|
|
type: integer
|
|
sentCount:
|
|
type: integer
|
|
failedCount:
|
|
type: integer
|