CNBS

OpenAPI spec

OpenAPI spec

Last updated 5/24/2026

CNBS OpenAPI 3.1 Specification

Endpoint Summary Table

#MethodPathTagAuth RequiredSummary
1POST/api/admin/onboarding/completeAdminYesComplete admin onboarding
2POST/api/ai/extract-colorsAIYesExtract color palette from image
3POST/api/ai/generate-heroAIYesGenerate hero section content
4POST/api/ai/generate-imageAIYesGenerate AI image
5POST/api/ai/generate-promotionAIYesGenerate promotional content
6POST/api/ai/save-generated-imageAIYesSave a generated AI image
7GET/api/analytics/customersAnalyticsYesGet customer analytics
8GET/api/analytics/dashboard/performanceAnalyticsYesGet dashboard performance metrics
9GET/api/analytics/dashboard/real-timeAnalyticsYesGet real-time dashboard data
10GET/api/analytics/dashboardAnalyticsYesGet analytics dashboard summary
11GET/api/analytics/dashboard/trendsAnalyticsYesGet dashboard trend data
12GET/api/analytics/insightsAnalyticsYesGet AI-generated analytics insights
13GET/api/analytics/inventory-insightsAnalyticsYesGet inventory analytics insights
14GET/api/analytics/inventoryAnalyticsYesGet inventory analytics
15GET/api/analytics/performanceAnalyticsYesGet performance analytics
16GET/api/analytics/predictionsAnalyticsYesGet predictive analytics
17GET/api/analytics/reports/complianceAnalyticsYesGet compliance report
18GET/api/analytics/reports/inventoryAnalyticsYesGet inventory report
19GET/api/analytics/reportsAnalyticsYesList available reports
20GET/api/analytics/reports/salesAnalyticsYesGet sales report
21GET/api/analytics/revenueAnalyticsYesGet revenue analytics
22GET/api/analytics/vitalsAnalyticsYesGet system vitals
23GET/api/associatesAssociatesYesList associates
24POST/api/associatesAssociatesYesCreate associate
25GET/api/associates/{associateId}AssociatesYesGet associate by ID
26PATCH/api/associates/{associateId}AssociatesYesUpdate associate
27DELETE/api/associates/{associateId}AssociatesYesDelete associate
28GET/api/auth/check-roleAuthYesCheck current user role
29GET/api/auth/check-user-contextAuthYesCheck user organization context
30POST/api/auth/employeeAuthNoAuthenticate employee (PIN)
31POST/api/backupOperationsYesTrigger data backup
32GET/api/canonical/brandsCanonicalYesList canonical cannabis brands
33GET/api/canonical/categoriesCanonicalYesList canonical product categories
34GET/api/canonical/effectsCanonicalYesList canonical cannabis effects
35POST/api/careers/applyCareersNoSubmit a job application
36GET/api/cash-drawerOperationsYesGet cash drawer status
37POST/api/cash-drawerOperationsYesRecord cash drawer event
38POST/api/clerk/add-to-organizationClerkYesAdd user to Clerk organization
39POST/api/clerk/sync/customerClerkYesSync customer record to Clerk
40POST/api/clerk/sync/membership/addClerkYesAdd organization membership
41POST/api/clerk/sync/membership/removeClerkYesRemove organization membership
42POST/api/clerk/sync/organizationClerkYesSync organization to Clerk
43POST/api/clerk/syncClerkYesFull Clerk sync (webhook)
44POST/api/clerk/sync/userClerkYesSync user record to Clerk

openapi: 3.1.0

info:
  title: CNBS – Cannabis Business System API
  version: 0.1.0
  description: |
    The CNBS API powers a multi-tenant cannabis dispensary platform (dispensary-pos)
    built on Next.js 15. It covers point-of-sale operations, inventory management,
    analytics, regulatory compliance, AI-assisted content generation, and Clerk-based
    identity synchronisation across organisations.

    **Authentication** – Every protected endpoint requires a valid Clerk session token
    supplied as a Bearer token in the `Authorization` header.  A small number of
    public endpoints (employee PIN login, career applications) are explicitly exempt.

    **Multi-tenancy** – Most resources are scoped to an `orgId` (Clerk organisation
    ID). Callers without a matching organisation context will receive `403 Forbidden`.

    **Versioning** – The API is currently at v0.1 (Spark stage). Breaking changes will
    be communicated via changelog entries in `docs/ROADMAP.md`.
  contact:
    name: CNBS Engineering
    url: https://github.com/midwestco/cnbs
    email: engineering@cnbs.io
  license:
    name: Proprietary
    identifier: LicenseRef-CNBS-1.0

servers:
  - url: https://app.cnbs.io
    description: Production
  - url: https://staging.cnbs.io
    description: Staging
  - url: http://localhost:3000
    description: Local development

# ---------------------------------------------------------------------------
# Security
# ---------------------------------------------------------------------------
security:
  - ClerkBearerAuth: []

# ---------------------------------------------------------------------------
# Tags
# ---------------------------------------------------------------------------
tags:
  - name: Admin
    description: Onboarding and administrative setup operations
  - name: AI
    description: |
      AI-powered content and image generation endpoints backed by
      `@anthropic-ai/sdk` and `openai` (see `next.config.ts`).
  - name: Analytics
    description: |
      Business intelligence: sales revenue, inventory insights, customer
      segmentation, compliance reports, and real-time dashboard feeds.
  - name: Associates
    description: Dispensary employee (budtender/associate) management
  - name: Auth
    description: Authentication helpers – role checks and employee PIN login
  - name: Canonical
    description: |
      Read-only reference data (brands, categories, effects) shared across
      organisations for consistent product taxonomy.
  - name: Careers
    description: Public-facing job application intake
  - name: Clerk
    description: Clerk identity-platform synchronisation webhooks and utilities
  - name: Operations
    description: Physical store operations – cash drawer management and backups

# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
paths:

  # ── Admin ─────────────────────────────────────────────────────────────────

  /api/admin/onboarding/complete:
    post:
      operationId: completeAdminOnboarding
      tags: [Admin]
      summary: Complete admin onboarding
      description: |
        Marks the onboarding flow as complete for the calling administrator.
        Persists organisation metadata and triggers initial data seeding such
        as default product categories and tax configurations.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminOnboardingCompleteRequest'
      responses:
        '200':
          description: Onboarding completed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOnboardingCompleteResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ── AI ────────────────────────────────────────────────────────────────────

  /api/ai/extract-colors:
    post:
      operationId: aiExtractColors
      tags: [AI]
      summary: Extract color palette from image
      description: |
        Analyses a supplied image URL or base64 payload and returns a structured
        color palette (primary, secondary, accent) suitable for storefront
        theming. Backed by Anthropic Claude vision capabilities.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExtractColorsRequest'
      responses:
        '200':
          description: Color palette extracted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExtractColorsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/ai/generate-hero:
    post:
      operationId: aiGenerateHero
      tags: [AI]
      summary: Generate hero section content
      description: |
        Generates headline copy, subheadline, and a call-to-action label for the
        dispensary storefront hero banner. Accepts brand tone parameters and
        optional seasonal context.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateHeroRequest'
      responses:
        '200':
          description: Hero content generated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateHeroResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/ai/generate-image:
    post:
      operationId: aiGenerateImage
      tags: [AI]
      summary: Generate AI image
      description: |
        Generates a product or marketing image via the OpenAI Images API.
        Returns the generated image as a URL.  Caller should subsequently
        call `/api/ai/save-generated-image` to persist the asset to Supabase
        Storage.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateImageRequest'
      responses:
        '200':
          description: Image generated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateImageResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/ai/generate-promotion:
    post:
      operationId: aiGeneratePromotion
      tags: [AI]
      summary: Generate promotional content
      description: |
        Creates promotional copy for banners, push notifications, or email
        campaigns. Accepts product IDs and desired discount information to
        produce contextually relevant marketing language.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GeneratePromotionRequest'
      responses:
        '200':
          description: Promotion content generated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GeneratePromotionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/ai/save-generated-image:
    post:
      operationId: aiSaveGeneratedImage
      tags: [AI]
      summary: Save a generated AI image
      description: |
        Downloads the OpenAI-hosted image URL and persists it to Supabase
        Storage under the organisation's asset bucket. Returns the permanent
        Supabase public URL.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SaveGeneratedImageRequest'
      responses:
        '200':
          description: Image saved to storage
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SaveGeneratedImageResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ── Analytics ─────────────────────────────────────────────────────────────

  /api/analytics/customers:
    get:
      operationId: getCustomerAnalytics
      tags: [Analytics]
      summary: Get customer analytics
      description: |
        Returns aggregated customer metrics including new vs. returning visitor
        ratios, average order value, purchase frequency, and top customer
        segments for the authenticated organisation.
      parameters:
        - $ref: '#/components/parameters/DateFrom'
        - $ref: '#/components/parameters/DateTo'
        - $ref: '#/components/parameters/OrgIdQuery'
      responses:
        '200':
          description: Customer analytics data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerAnalyticsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/analytics/dashboard:
    get:
      operationId: getAnalyticsDashboard
      tags: [Analytics]
      summary: Get analytics dashboard summary
      description: |
        Returns a rolled-up summary of key performance indicators: total
        revenue, transaction count, average basket size, top-selling products,
        and compliance status for the current period.
      parameters:
        - $ref: '#/components/parameters/DateFrom'
        - $ref: '#/components/parameters/DateTo'
        - $ref: '#/components/parameters/OrgIdQuery'
      responses:
        '200':
          description: Dashboard summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DashboardSummaryResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/analytics/dashboard/performance:
    get:
      operationId: getDashboardPerformance
      tags: [Analytics]
      summary: Get dashboard performance metrics
      description: |
        Returns server-side performance metrics for the dashboard itself
        (API latency p50/p95/p99, cache hit-rate, database query counts)
        alongside application KPIs.
      parameters:
        - $ref: '#/components/parameters/OrgIdQuery'
      responses:
        '200':
          description: Performance metrics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DashboardPerformanceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/analytics/dashboard/real-time:
    get:
      operationId: getDashboardRealTime
      tags: [Analytics]
      summary: Get real-time dashboard data
      description: |
        Streams live metrics via polling or Server-Sent Events: current active
        POS sessions, transactions in the last 15 minutes, and live inventory
        depletion rates.  Backed by Supabase Realtime.
      parameters:
        - $ref: '#/components/parameters/OrgIdQuery'
      responses:
        '200':
          description: Real-time dashboard snapshot
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RealTimeDashboardResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/analytics/dashboard/trends:
    get:
      operationId: getDashboardTrends
      tags: [Analytics]
      summary: Get dashboard trend data
      description: |
        Returns time-series data points for revenue, transaction volume, and
        customer acquisition across configurable granularities (hourly, daily,
        weekly, monthly).
      parameters:
        - $ref: '#/components/parameters/DateFrom'
        - $ref: '#/components/parameters/DateTo'
        - $ref: '#/components/parameters/OrgIdQuery'
        - name: granularity
          in: query
          required: false
          schema:
            type: string
            enum: [hourly, daily, weekly, monthly]
            default: daily
          description: Time bucket granularity for trend series
      responses:
        '200':
          description: Trend time-series data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TrendsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/analytics/insights:
    get:
      operationId: getAnalyticsInsights
      tags: [Analytics]
      summary: Get AI-generated analytics insights
      description: |
        Runs AI analysis over recent sales and inventory data to surface
        actionable insights such as slow-moving SKUs, reorder recommendations,
        and demand forecast alerts.
      parameters:
        - $ref: '#/components/parameters/OrgIdQuery'
      responses:
        '200':
          description: AI insights
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsInsightsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/analytics/inventory-insights:
    get:
      operationId: getInventoryInsights
      tags: [Analytics]
      summary: Get inventory analytics insights
      description: |
        Returns deep-dive inventory metrics: stock turnover ratio, days on
        hand by category, shrinkage rate, and compliance variance between
        physical counts and METRC-reported quantities.
      parameters:
        - $ref: '#/components/parameters/DateFrom'
        - $ref: '#/components/parameters/DateTo'
        - $ref: '#/components/parameters/OrgIdQuery'
      responses:
        '200':
          description: Inventory insights
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InventoryInsightsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/analytics/inventory:
    get:
      operationId: getInventoryAnalytics
      tags: [Analytics]
      summary: Get inventory analytics
      description: |
        Aggregated inventory analytics including on-hand quantities by category,
        product velocity, pending transfers, and low-stock alerts.
      parameters:
        - $ref: '#/components/parameters/OrgIdQuery'
      responses:
        '200':
          description: Inventory analytics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InventoryAnalyticsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/analytics/performance:
    get:
      operationId: getPerformanceAnalytics
      tags: [Analytics]
      summary: Get performance analytics
      description: |
        Employee and POS-station performance metrics including transactions per
        hour, average transaction time, upsell rate, and return rate by
        associate.
      parameters:
        - $ref: '#/components/parameters/DateFrom'
        - $ref: '#/components/parameters/DateTo'
        - $ref: '#/components/parameters/OrgIdQuery'
      responses:
        '200':
          description: Performance analytics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PerformanceAnalyticsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/analytics/predictions:
    get:
      operationId: getAnalyticsPredictions
      tags: [Analytics]
      summary: Get predictive analytics
      description: |
        Machine-learning–driven demand forecasts, projected revenue for the
        next 7/30 days, and reorder point predictions by SKU.
      parameters:
        - $ref: '#/components/parameters/OrgIdQuery'
        - name: horizon
          in: query
          required: false
          schema:
            type: integer
            enum: [7, 30, 90]
            default: 30
          description: Forecast horizon in days
      responses:
        '200':
          description: Predictive analytics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PredictionsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/analytics/reports:
    get:
      operationId: listReports
      tags: [Analytics]
      summary: List available reports
      description: |
        Returns metadata for all saved and scheduled reports available to the
        organisation, including report type, last run time, and download URL.
      parameters:
        - $ref: '#/components/parameters/OrgIdQuery'
      responses:
        '200':
          description: List of reports
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportsListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/analytics/reports/compliance:
    get:
      operationId: getComplianceReport
      tags: [Analytics]
      summary: Get compliance report
      description: |
        Generates or retrieves a compliance report containing METRC transfer
        manifest reconciliation, purchase limit adherence, and state-mandated
        sales data for the specified date range.
      parameters:
        - $ref: '#/components/parameters/DateFrom'
        - $ref: '#/components/parameters/DateTo'
        - $ref: '#/components/parameters/OrgIdQuery'
      responses:
        '200':
          description: Compliance report
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ComplianceReportResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/analytics/reports/inventory:
    get:
      operationId: getInventoryReport
      tags: [Analytics]
      summary: Get inventory report
      description: |
        Returns a paginated inventory snapshot report with on-hand quantities,
        unit cost, retail value, and days-on-hand, filterable by category and
        vendor.
      parameters:
        - $ref: '#/components/parameters/DateFrom'
        - $ref: '#/components/parameters/DateTo'
        - $ref: '#/components/parameters/OrgIdQuery'
        - name: category
          in: query
          required: false
          schema:
            type: string
          description: Filter by canonical product category
      responses:
        '200':
          description: Inventory report
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InventoryReportResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/analytics/reports/sales:
    get:
      operationId: getSalesReport
      tags: [Analytics]
      summary: Get sales report
      description: |
        Returns a detailed sales report broken down by product, category,
        payment method, and associate.  Supports CSV export via the `format`
        query parameter.
      parameters:
        - $ref: '#/components/parameters/DateFrom'
        - $ref: '#/components/parameters/DateTo'
        - $ref: '#/components/parameters/OrgIdQuery'
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum: [json, csv]
            default: json
          description: Response format
      responses:
        '200':
          description: Sales report
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SalesReportResponse'
            text/csv:
              schema:
                type: string
                format: binary
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/analytics/revenue:
    get:
      operationId: getRevenueAnalytics
      tags: [Analytics]
      summary: Get revenue analytics
      description: |
        Gross revenue, net revenue (after discounts and returns), tax collected,
        and margin metrics for the organisation and specified period.
      parameters:
        - $ref: '#/components/parameters/DateFrom'
        - $ref: '#/components/parameters/DateTo'
        - $ref: '#/components/parameters/OrgIdQuery'
      responses:
        '200':
          description: Revenue analytics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RevenueAnalyticsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/analytics/vitals:
    get:
      operationId: getAnalyticsVitals
      tags: [Analytics]
      summary: Get system vitals
      description: |
        System health metrics: database connection pool utilisation, Supabase
        Realtime connection count, cache hit ratio, and last successful METRC
        sync timestamp.
      parameters:
        - $ref: '#/components/parameters/OrgIdQuery'
      responses:
        '200':
          description: System vitals
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VitalsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ── Associates ────────────────────────────────────────────────────────────

  /api/associates:
    get:
      operationId: listAssociates
      tags: [Associates]
      summary: List associates
      description: |
        Returns all budtenders and associates belonging to the authenticated
        organisation, including role, active status, and assigned POS station.
      parameters:
        - $ref: '#/components/parameters/OrgIdQuery'
        - name: active
          in: query
          required: false
          schema:
            type: boolean
          description: Filter to active (true) or inactive (false) associates only
      responses:
        '200':
          description: List of associates
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssociatesListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      operationId: createAssociate
      tags: [Associates]
      summary: Create associate
      description: |
        Creates a new associate record and optionally sends a Clerk invitation
        to the supplied email address.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAssociateRequest'
      responses:
        '200':
          description: Associate created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Associate'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /api/associates/{associateId}:
    parameters:
      - name: associateId
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: UUID of the associate record
    get:
      operationId: getAssociate
      tags: [Associates]
      summary: Get associate by ID
      description: Returns the full profile of a single associate.
      responses:
        '200':
          description: Associate record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Associate'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
    patch:
      operationId: updateAssociate
      tags: [Associates]
      summary: Update associate
      description: Partially updates an associate record.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateAssociateRequest'
      responses:
        '200':
          description: Updated associate record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Associate'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
    delete:
      operationId: deleteAssociate
      tags: [Associates]
      summary: Delete associate
      description: |
        Soft-deletes the associate record, revokes Clerk organisation membership,
        and marks any open POS sessions as closed.
      responses:
        '200':
          description: Associate deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  # ── Auth ──────────────────────────────────────────────────────────────────

  /api/auth/check-role:
    get:
      operationId: checkRole
      tags: [Auth]
      summary: