openapi: 3.0.3 info: title: Bookra API version: 0.1.0 description: > Remote-first booking API for Bookra. The Go backend owns business rules, scheduling logic, tenant isolation, and Paddle-backed plan enforcement. servers: - url: http://localhost:8080 tags: - name: Health - name: Public Booking - name: Dashboard - name: Tenant - name: Billing - 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/tenants/{tenantSlug}/availability: get: tags: [Public Booking] operationId: getPublicAvailability parameters: - in: path name: tenantSlug required: true schema: type: string responses: "200": description: Availability for a tenant booking page content: application/json: schema: $ref: "#/components/schemas/PublicAvailabilityResponse" /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" /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 /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/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: Paddle checkout launch payload 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: Paddle 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/paddle: post: tags: [Billing] operationId: handlePaddleWebhook requestBody: required: true content: application/json: schema: type: object responses: "200": description: Webhook accepted /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 components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT jobRunnerKey: type: apiKey in: header name: X-Bookra-Job-Key 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 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 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" 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] DashboardKPI: type: object required: [code, label, value] properties: code: type: string label: type: string value: type: string DashboardSummary: type: object required: [tenantName, tenantSlug, locale, timezone, planCode, publicBookingUrl, setupCompletion, kpis] properties: tenantName: type: string tenantSlug: type: string locale: type: string timezone: type: string planCode: type: string publicBookingUrl: type: string setupCompletion: type: integer kpis: type: array items: $ref: "#/components/schemas/DashboardKPI" upcomingBookings: type: array items: $ref: "#/components/schemas/UpcomingBooking" widgetSnippets: type: array items: $ref: "#/components/schemas/WidgetSnippet" tracking: $ref: "#/components/schemas/TrackingStatus" UpcomingBooking: type: object properties: reference: type: string customerName: type: string customerEmail: type: string startsAt: type: string format: date-time endsAt: type: string format: date-time status: type: string label: 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 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 PlanEntitlements: type: object required: [maxLocations, maxStaff, emailReminders, advancedReporting, widgetEmbedding, umamiTracking] properties: maxLocations: type: integer maxStaff: 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: paddle 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] currency: type: string enum: [czk, usd] PlanDisplayPrice: type: object required: [currency, amountCents, formatted] properties: currency: type: string enum: [czk, usd] amountCents: type: integer formatted: type: string CheckoutLaunchResponse: type: object required: [priceId, successRedirectUrl, cancelRedirectUrl, customData] properties: priceId: type: string customerId: type: string customerEmail: type: string format: email successRedirectUrl: type: string format: uri cancelRedirectUrl: type: string format: uri customData: type: object additionalProperties: type: string 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