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]
          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: |
        Checkout launch response supporting both Stripe and Paddle providers.
        For Stripe: checkoutUrl is returned (redirect-based checkout).
        For Paddle: priceId, customerId, customerEmail, customData are returned (client-side checkout).
      properties:
        checkoutUrl:
          type: string
          format: uri
          description: Stripe checkout URL (redirect the user to this URL)
        priceId:
          type: string
          description: Paddle price ID for client-side checkout
        customerId:
          type: string
          description: Paddle customer ID
        customerEmail:
          type: string
          format: email
          description: Customer email for Paddle checkout
        successRedirectUrl:
          type: string
          format: uri
          description: URL to redirect after successful checkout
        cancelRedirectUrl:
          type: string
          format: uri
          description: URL to redirect after cancelled checkout
        customData:
          type: object
          additionalProperties:
            type: string
          description: Custom metadata for Paddle 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
