Files
Bookra/apps/backend/openapi/bookra.openapi.yaml
T
Tomas DvorakandDevin 6c3b2bf6bf feat(calendar): read-only ICS feeds for tenant and staff
GET /v1/public/calendar/{token} serves an RFC 5545 calendar under a
token-as-secret URL (private Google/Outlook link model). Staff feeds
contain only that member's bookings; the tenant feed tags each event
with the staff member. Window is -30d to +180d; cancelled bookings map
to STATUS:CANCELLED, waitlisted/pending to TENTATIVE + TRANSPARENT.
Summaries resolve service or class-session titles, descriptions carry
localized contact labels, locations resolve to names.

Feed tokens live on tenants/staff (migration 00015) and are managed via
GET /v1/calendar/feed plus POST /v1/calendar/feed/rotate — rotating a
scope invalidates the old URL. ICS rendering is hand-rolled: TEXT
escaping, CRLF, 75-octet folding on rune boundaries.

Settings gains a "Kalendář (ICS)" card with copyable URLs and two-step
rotation per scope; demo mode points at the real seeded tokens.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 15:20:22 +02:00

2604 lines
66 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}/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/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
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"
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