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/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/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}: 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 # ---------------- 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 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] 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 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] properties: processedCount: type: integer sentCount: type: integer failedCount: type: integer