Files
Bookra/apps/backend/openapi/bookra.openapi.yaml
T
Tomas DvorakandDevin cdad7875e1 fix(reminders): lifecycle-aware jobs and dispatch skip guard
The reminder pipeline fired regardless of what happened to the booking
after creation: cancelled customers got reminded about bookings that
weren't happening, reschedules kept the old reminder time, and
waitlisted bookings armed jobs too. Pending jobs are now cancelled on
both cancel paths, deleted and re-armed on reschedule, and dispatch
joins bookings.status to skip anything not confirmed. Reschedule also
gained the same availability-window and blocked-day validation as
booking creation, closing a 3am-reschedule hole via the manage URL.

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

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

2304 lines
58 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/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, skippedCount]
properties:
processedCount:
type: integer
sentCount:
type: integer
failedCount:
type: integer
skippedCount:
type: integer